Repository: hyperfield/ai-file-sorter
Branch: main
Commit: 630b46a1c682
Files: 1196
Total size: 8.3 MB
Directory structure:
gitextract_jgso0h_k/
├── .clang-format
├── .github/
│ └── workflows/
│ └── build.yml
├── .gitignore
├── .gitmodules
├── CHANGELOG.md
├── LICENSE
├── README.md
├── TESTS.md
├── TRADEMARKS.md
├── app/
│ ├── CMakeLists.txt
│ ├── Makefile
│ ├── build_windows.ps1
│ ├── include/
│ │ ├── AppInfo.hpp
│ │ ├── CategorizationDialog.hpp
│ │ ├── CategorizationProgressDialog.hpp
│ │ ├── CategorizationService.hpp
│ │ ├── CategorizationServiceTestAccess.hpp
│ │ ├── CategorizationSession.hpp
│ │ ├── CategoryLanguage.hpp
│ │ ├── ConsistencyPassService.hpp
│ │ ├── CustomApiDialog.hpp
│ │ ├── CustomLLMDialog.hpp
│ │ ├── DatabaseManager.hpp
│ │ ├── DialogUtils.hpp
│ │ ├── DocumentTextAnalyzer.hpp
│ │ ├── DryRunPreviewDialog.hpp
│ │ ├── EmbeddedEnv.hpp
│ │ ├── ErrorMessages.hpp
│ │ ├── FileScanner.hpp
│ │ ├── GeminiClient.hpp
│ │ ├── GgmlRuntimePaths.hpp
│ │ ├── ILLMClient.hpp
│ │ ├── ImageRenameMetadataService.hpp
│ │ ├── IniConfig.hpp
│ │ ├── LLMClient.hpp
│ │ ├── LLMDownloader.hpp
│ │ ├── LLMErrors.hpp
│ │ ├── LLMSelectionDialog.hpp
│ │ ├── LLMSelectionDialogTestAccess.hpp
│ │ ├── Language.hpp
│ │ ├── LlamaModelParams.hpp
│ │ ├── LlavaImageAnalyzer.hpp
│ │ ├── LlmCatalog.hpp
│ │ ├── LocalLLMClient.hpp
│ │ ├── LocalLLMTestAccess.hpp
│ │ ├── Logger.hpp
│ │ ├── MainApp.hpp
│ │ ├── MainAppEditActions.hpp
│ │ ├── MainAppHelpActions.hpp
│ │ ├── MainAppTestAccess.hpp
│ │ ├── MainAppUiBuilder.hpp
│ │ ├── MediaRenameMetadataService.hpp
│ │ ├── MovableCategorizedFile.hpp
│ │ ├── ResultsCoordinator.hpp
│ │ ├── Settings.hpp
│ │ ├── SuitabilityBenchmarkDialog.hpp
│ │ ├── SupportCodeManager.hpp
│ │ ├── TestHooks.hpp
│ │ ├── TranslationManager.hpp
│ │ ├── Types.hpp
│ │ ├── UiTranslator.hpp
│ │ ├── UndoManager.hpp
│ │ ├── UpdateArchiveExtractor.hpp
│ │ ├── UpdateFeed.hpp
│ │ ├── UpdateInstaller.hpp
│ │ ├── UpdateInstallerTestAccess.hpp
│ │ ├── Updater.hpp
│ │ ├── UpdaterBuildConfig.hpp
│ │ ├── UpdaterLaunchOptions.hpp
│ │ ├── UpdaterLiveTestConfig.hpp
│ │ ├── UpdaterTestAccess.hpp
│ │ ├── Utils.hpp
│ │ ├── Version.hpp
│ │ ├── WhitelistManagerDialog.hpp
│ │ ├── WhitelistStore.hpp
│ │ ├── app_version.hpp
│ │ ├── constants.hpp
│ │ └── external/
│ │ └── dotenv.h
│ ├── includePaths.txt
│ ├── lib/
│ │ ├── CategorizationDialog.cpp
│ │ ├── CategorizationProgressDialog.cpp
│ │ ├── CategorizationService.cpp
│ │ ├── CategorizationSession.cpp
│ │ ├── ConsistencyPassService.cpp
│ │ ├── CustomApiDialog.cpp
│ │ ├── CustomLLMDialog.cpp
│ │ ├── DatabaseManager.cpp
│ │ ├── DialogUtils.cpp
│ │ ├── DocumentTextAnalyzer.cpp
│ │ ├── DryRunPreviewDialog.cpp
│ │ ├── EmbeddedEnv.cpp
│ │ ├── FileScanner.cpp
│ │ ├── GeminiClient.cpp
│ │ ├── GgmlRuntimePaths.cpp
│ │ ├── ImageRenameMetadataService.cpp
│ │ ├── IniConfig.cpp
│ │ ├── LLMClient.cpp
│ │ ├── LLMDownloader.cpp
│ │ ├── LLMSelectionDialog.cpp
│ │ ├── LlavaImageAnalyzer.cpp
│ │ ├── LlmCatalog.cpp
│ │ ├── LocalLLMClient.cpp
│ │ ├── Logger.cpp
│ │ ├── MainApp.cpp
│ │ ├── MainAppEditActions.cpp
│ │ ├── MainAppHelpActions.cpp
│ │ ├── MainAppUiBuilder.cpp
│ │ ├── MediaRenameMetadataService.cpp
│ │ ├── MovableCategorizedFile.cpp
│ │ ├── PugixmlBundle.cpp
│ │ ├── ResultsCoordinator.cpp
│ │ ├── Settings.cpp
│ │ ├── SuitabilityBenchmarkDialog.cpp
│ │ ├── SupportCodeManager.cpp
│ │ ├── TranslationManager.cpp
│ │ ├── UiTranslator.cpp
│ │ ├── UndoManager.cpp
│ │ ├── UpdateArchiveExtractor.cpp
│ │ ├── UpdateFeed.cpp
│ │ ├── UpdateInstaller.cpp
│ │ ├── Updater.cpp
│ │ ├── UpdaterLiveTestConfig.cpp
│ │ ├── Utils.cpp
│ │ ├── Version.cpp
│ │ ├── WhitelistManagerDialog.cpp
│ │ └── WhitelistStore.cpp
│ ├── main.cpp
│ ├── resources/
│ │ ├── app.qrc
│ │ ├── certs/
│ │ │ └── cacert.pem
│ │ ├── i18n/
│ │ │ ├── aifilesorter_de.ts
│ │ │ ├── aifilesorter_es.ts
│ │ │ ├── aifilesorter_fr.ts
│ │ │ ├── aifilesorter_it.ts
│ │ │ ├── aifilesorter_ko.ts
│ │ │ ├── aifilesorter_nl.ts
│ │ │ └── aifilesorter_tr.ts
│ │ ├── images/
│ │ │ ├── app_icon_128.xcf
│ │ │ ├── app_icon_128_magnified.xcf
│ │ │ ├── app_icon_256.xcf
│ │ │ ├── app_icon_256_magnified.xcf
│ │ │ ├── app_icon_350.xcf
│ │ │ ├── app_icon_512.xcf
│ │ │ ├── app_icon_arrows_more_colours.xcf
│ │ │ ├── logo.xcf
│ │ │ ├── logo_inscription.xcf
│ │ │ ├── qn_logo.xcf
│ │ │ └── qn_logo_200.xcf
│ │ └── windows/
│ │ ├── app_icon.rc.in
│ │ └── version.rc.in
│ ├── scripts/
│ │ ├── README.md
│ │ ├── build_llama_linux.sh
│ │ ├── build_llama_macos.sh
│ │ ├── build_llama_windows.ps1
│ │ ├── collect_linux_diagnostics.sh
│ │ ├── collect_macos_diagnostics.sh
│ │ ├── collect_windows_diagnostics.ps1
│ │ ├── gen_run_wrapper.py
│ │ ├── generate_icon.ps1
│ │ ├── package_deb.sh
│ │ ├── rebuild_and_test.sh
│ │ ├── run_aifilesorter.sh.in
│ │ ├── vendor_doc_deps.ps1
│ │ └── vendor_doc_deps.sh
│ ├── startapp_linux.cpp
│ ├── startapp_windows.cpp
│ └── vcpkg.json
├── external/
│ ├── README-doc-deps.md
│ ├── THIRD_PARTY_LICENSES/
│ │ ├── libzip-LICENSE
│ │ ├── pdfium-LICENSE
│ │ └── pugixml-LICENSE.md
│ ├── libzip/
│ │ ├── .clang-format
│ │ ├── .github/
│ │ │ ├── ISSUE_TEMPLATE/
│ │ │ │ ├── bug-report.md
│ │ │ │ ├── compile-error.md
│ │ │ │ ├── feature-request.md
│ │ │ │ └── other.md
│ │ │ └── workflows/
│ │ │ ├── CIFuzz.yml
│ │ │ ├── bsd.yml
│ │ │ ├── build.yml
│ │ │ ├── codeql-analysis.yml
│ │ │ └── coverity.yml
│ │ ├── API-CHANGES.md
│ │ ├── AUTHORS
│ │ ├── CMakeLists.txt
│ │ ├── INSTALL.md
│ │ ├── LICENSE
│ │ ├── NEWS.md
│ │ ├── README.md
│ │ ├── SECURITY.md
│ │ ├── THANKS
│ │ ├── TODO.md
│ │ ├── android/
│ │ │ ├── do.sh
│ │ │ ├── docker/
│ │ │ │ └── Dockerfile
│ │ │ └── readme.txt
│ │ ├── appveyor.yml
│ │ ├── cmake/
│ │ │ ├── Dist.cmake
│ │ │ ├── FindMbedTLS.cmake
│ │ │ ├── FindNettle.cmake
│ │ │ ├── Findzstd.cmake
│ │ │ └── GenerateZipErrorStrings.cmake
│ │ ├── cmake-compat/
│ │ │ ├── CMakePushCheckState.cmake
│ │ │ ├── CheckLibraryExists.cmake
│ │ │ ├── CheckSymbolExists.cmake
│ │ │ ├── FindBZip2.cmake
│ │ │ ├── FindGnuTLS.cmake
│ │ │ ├── FindLibLZMA.cmake
│ │ │ ├── FindPackageHandleStandardArgs.cmake
│ │ │ ├── FindPackageMessage.cmake
│ │ │ └── SelectLibraryConfigurations.cmake
│ │ ├── config.h.in
│ │ ├── examples/
│ │ │ ├── CMakeLists.txt
│ │ │ ├── add-compressed-data.c
│ │ │ ├── autoclose-archive.c
│ │ │ ├── cmake-project/
│ │ │ │ ├── CMakeLists.txt
│ │ │ │ └── cmake-example.c
│ │ │ ├── in-memory.c
│ │ │ └── windows-open.c
│ │ ├── lib/
│ │ │ ├── CMakeLists.txt
│ │ │ ├── compat.h
│ │ │ ├── zip.h
│ │ │ ├── zip_add.c
│ │ │ ├── zip_add_dir.c
│ │ │ ├── zip_add_entry.c
│ │ │ ├── zip_algorithm_bzip2.c
│ │ │ ├── zip_algorithm_deflate.c
│ │ │ ├── zip_algorithm_xz.c
│ │ │ ├── zip_algorithm_zstd.c
│ │ │ ├── zip_buffer.c
│ │ │ ├── zip_close.c
│ │ │ ├── zip_crypto.h
│ │ │ ├── zip_crypto_commoncrypto.c
│ │ │ ├── zip_crypto_commoncrypto.h
│ │ │ ├── zip_crypto_gnutls.c
│ │ │ ├── zip_crypto_gnutls.h
│ │ │ ├── zip_crypto_mbedtls.c
│ │ │ ├── zip_crypto_mbedtls.h
│ │ │ ├── zip_crypto_openssl.c
│ │ │ ├── zip_crypto_openssl.h
│ │ │ ├── zip_crypto_win.c
│ │ │ ├── zip_crypto_win.h
│ │ │ ├── zip_delete.c
│ │ │ ├── zip_dir_add.c
│ │ │ ├── zip_dirent.c
│ │ │ ├── zip_discard.c
│ │ │ ├── zip_entry.c
│ │ │ ├── zip_error.c
│ │ │ ├── zip_error_clear.c
│ │ │ ├── zip_error_get.c
│ │ │ ├── zip_error_get_sys_type.c
│ │ │ ├── zip_error_strerror.c
│ │ │ ├── zip_error_to_str.c
│ │ │ ├── zip_extra_field.c
│ │ │ ├── zip_extra_field_api.c
│ │ │ ├── zip_fclose.c
│ │ │ ├── zip_fdopen.c
│ │ │ ├── zip_file_add.c
│ │ │ ├── zip_file_error_clear.c
│ │ │ ├── zip_file_error_get.c
│ │ │ ├── zip_file_get_comment.c
│ │ │ ├── zip_file_get_external_attributes.c
│ │ │ ├── zip_file_get_offset.c
│ │ │ ├── zip_file_rename.c
│ │ │ ├── zip_file_replace.c
│ │ │ ├── zip_file_set_comment.c
│ │ │ ├── zip_file_set_encryption.c
│ │ │ ├── zip_file_set_external_attributes.c
│ │ │ ├── zip_file_set_mtime.c
│ │ │ ├── zip_file_strerror.c
│ │ │ ├── zip_fopen.c
│ │ │ ├── zip_fopen_encrypted.c
│ │ │ ├── zip_fopen_index.c
│ │ │ ├── zip_fopen_index_encrypted.c
│ │ │ ├── zip_fread.c
│ │ │ ├── zip_fseek.c
│ │ │ ├── zip_ftell.c
│ │ │ ├── zip_get_archive_comment.c
│ │ │ ├── zip_get_archive_flag.c
│ │ │ ├── zip_get_encryption_implementation.c
│ │ │ ├── zip_get_file_comment.c
│ │ │ ├── zip_get_name.c
│ │ │ ├── zip_get_num_entries.c
│ │ │ ├── zip_get_num_files.c
│ │ │ ├── zip_hash.c
│ │ │ ├── zip_io_util.c
│ │ │ ├── zip_libzip_version.c
│ │ │ ├── zip_memdup.c
│ │ │ ├── zip_name_locate.c
│ │ │ ├── zip_new.c
│ │ │ ├── zip_open.c
│ │ │ ├── zip_pkware.c
│ │ │ ├── zip_progress.c
│ │ │ ├── zip_random_unix.c
│ │ │ ├── zip_random_uwp.c
│ │ │ ├── zip_random_win32.c
│ │ │ ├── zip_realloc.c
│ │ │ ├── zip_rename.c
│ │ │ ├── zip_replace.c
│ │ │ ├── zip_set_archive_comment.c
│ │ │ ├── zip_set_archive_flag.c
│ │ │ ├── zip_set_default_password.c
│ │ │ ├── zip_set_file_comment.c
│ │ │ ├── zip_set_file_compression.c
│ │ │ ├── zip_set_name.c
│ │ │ ├── zip_source_accept_empty.c
│ │ │ ├── zip_source_begin_write.c
│ │ │ ├── zip_source_begin_write_cloning.c
│ │ │ ├── zip_source_buffer.c
│ │ │ ├── zip_source_call.c
│ │ │ ├── zip_source_close.c
│ │ │ ├── zip_source_commit_write.c
│ │ │ ├── zip_source_compress.c
│ │ │ ├── zip_source_crc.c
│ │ │ ├── zip_source_error.c
│ │ │ ├── zip_source_file.h
│ │ │ ├── zip_source_file_common.c
│ │ │ ├── zip_source_file_stdio.c
│ │ │ ├── zip_source_file_stdio.h
│ │ │ ├── zip_source_file_stdio_named.c
│ │ │ ├── zip_source_file_win32.c
│ │ │ ├── zip_source_file_win32.h
│ │ │ ├── zip_source_file_win32_ansi.c
│ │ │ ├── zip_source_file_win32_named.c
│ │ │ ├── zip_source_file_win32_utf16.c
│ │ │ ├── zip_source_file_win32_utf8.c
│ │ │ ├── zip_source_free.c
│ │ │ ├── zip_source_function.c
│ │ │ ├── zip_source_get_dostime.c
│ │ │ ├── zip_source_get_file_attributes.c
│ │ │ ├── zip_source_is_deleted.c
│ │ │ ├── zip_source_layered.c
│ │ │ ├── zip_source_open.c
│ │ │ ├── zip_source_pass_to_lower_layer.c
│ │ │ ├── zip_source_pkware_decode.c
│ │ │ ├── zip_source_pkware_encode.c
│ │ │ ├── zip_source_read.c
│ │ │ ├── zip_source_remove.c
│ │ │ ├── zip_source_rollback_write.c
│ │ │ ├── zip_source_seek.c
│ │ │ ├── zip_source_seek_write.c
│ │ │ ├── zip_source_stat.c
│ │ │ ├── zip_source_supports.c
│ │ │ ├── zip_source_tell.c
│ │ │ ├── zip_source_tell_write.c
│ │ │ ├── zip_source_window.c
│ │ │ ├── zip_source_winzip_aes_decode.c
│ │ │ ├── zip_source_winzip_aes_encode.c
│ │ │ ├── zip_source_write.c
│ │ │ ├── zip_source_zip.c
│ │ │ ├── zip_source_zip_new.c
│ │ │ ├── zip_stat.c
│ │ │ ├── zip_stat_index.c
│ │ │ ├── zip_stat_init.c
│ │ │ ├── zip_strerror.c
│ │ │ ├── zip_string.c
│ │ │ ├── zip_unchange.c
│ │ │ ├── zip_unchange_all.c
│ │ │ ├── zip_unchange_archive.c
│ │ │ ├── zip_unchange_data.c
│ │ │ ├── zip_utf-8.c
│ │ │ ├── zip_winzip_aes.c
│ │ │ └── zipint.h
│ │ ├── libzip-config.cmake.in
│ │ ├── libzip.pc.in
│ │ ├── man/
│ │ │ ├── CMakeLists.txt
│ │ │ ├── ZIP_SOURCE_GET_ARGS.html
│ │ │ ├── ZIP_SOURCE_GET_ARGS.man
│ │ │ ├── ZIP_SOURCE_GET_ARGS.mdoc
│ │ │ ├── handle_links
│ │ │ ├── libzip.html
│ │ │ ├── libzip.man
│ │ │ ├── libzip.mdoc
│ │ │ ├── links
│ │ │ ├── update-html.cmake
│ │ │ ├── update-man.cmake
│ │ │ ├── zip.html
│ │ │ ├── zip.man
│ │ │ ├── zip.mdoc
│ │ │ ├── zip_add.html
│ │ │ ├── zip_add.man
│ │ │ ├── zip_add.mdoc
│ │ │ ├── zip_add_dir.html
│ │ │ ├── zip_add_dir.man
│ │ │ ├── zip_add_dir.mdoc
│ │ │ ├── zip_close.html
│ │ │ ├── zip_close.man
│ │ │ ├── zip_close.mdoc
│ │ │ ├── zip_compression_method_supported.html
│ │ │ ├── zip_compression_method_supported.man
│ │ │ ├── zip_compression_method_supported.mdoc
│ │ │ ├── zip_delete.html
│ │ │ ├── zip_delete.man
│ │ │ ├── zip_delete.mdoc
│ │ │ ├── zip_dir_add.html
│ │ │ ├── zip_dir_add.man
│ │ │ ├── zip_dir_add.mdoc
│ │ │ ├── zip_discard.html
│ │ │ ├── zip_discard.man
│ │ │ ├── zip_discard.mdoc
│ │ │ ├── zip_encryption_method_supported.html
│ │ │ ├── zip_encryption_method_supported.man
│ │ │ ├── zip_encryption_method_supported.mdoc
│ │ │ ├── zip_error.html
│ │ │ ├── zip_error.man
│ │ │ ├── zip_error.mdoc
│ │ │ ├── zip_error_clear.html
│ │ │ ├── zip_error_clear.man
│ │ │ ├── zip_error_clear.mdoc
│ │ │ ├── zip_error_code_system.html
│ │ │ ├── zip_error_code_system.man
│ │ │ ├── zip_error_code_system.mdoc
│ │ │ ├── zip_error_code_zip.html
│ │ │ ├── zip_error_code_zip.man
│ │ │ ├── zip_error_code_zip.mdoc
│ │ │ ├── zip_error_fini.html
│ │ │ ├── zip_error_fini.man
│ │ │ ├── zip_error_fini.mdoc
│ │ │ ├── zip_error_get.html
│ │ │ ├── zip_error_get.man
│ │ │ ├── zip_error_get.mdoc
│ │ │ ├── zip_error_get_sys_type.html
│ │ │ ├── zip_error_get_sys_type.man
│ │ │ ├── zip_error_get_sys_type.mdoc
│ │ │ ├── zip_error_init.html
│ │ │ ├── zip_error_init.man
│ │ │ ├── zip_error_init.mdoc
│ │ │ ├── zip_error_set.html
│ │ │ ├── zip_error_set.man
│ │ │ ├── zip_error_set.mdoc
│ │ │ ├── zip_error_set_from_source.html
│ │ │ ├── zip_error_set_from_source.man
│ │ │ ├── zip_error_set_from_source.mdoc
│ │ │ ├── zip_error_strerror.html
│ │ │ ├── zip_error_strerror.man
│ │ │ ├── zip_error_strerror.mdoc
│ │ │ ├── zip_error_system_type.html
│ │ │ ├── zip_error_system_type.man
│ │ │ ├── zip_error_system_type.mdoc
│ │ │ ├── zip_error_to_data.html
│ │ │ ├── zip_error_to_data.man
│ │ │ ├── zip_error_to_data.mdoc
│ │ │ ├── zip_error_to_str.html
│ │ │ ├── zip_error_to_str.man
│ │ │ ├── zip_error_to_str.mdoc
│ │ │ ├── zip_errors.html
│ │ │ ├── zip_errors.man
│ │ │ ├── zip_errors.mdoc
│ │ │ ├── zip_fclose.html
│ │ │ ├── zip_fclose.man
│ │ │ ├── zip_fclose.mdoc
│ │ │ ├── zip_fdopen.html
│ │ │ ├── zip_fdopen.man
│ │ │ ├── zip_fdopen.mdoc
│ │ │ ├── zip_file.html
│ │ │ ├── zip_file.man
│ │ │ ├── zip_file.mdoc
│ │ │ ├── zip_file_add.html
│ │ │ ├── zip_file_add.man
│ │ │ ├── zip_file_add.mdoc
│ │ │ ├── zip_file_attributes_init.html
│ │ │ ├── zip_file_attributes_init.man
│ │ │ ├── zip_file_attributes_init.mdoc
│ │ │ ├── zip_file_extra_field_delete.html
│ │ │ ├── zip_file_extra_field_delete.man
│ │ │ ├── zip_file_extra_field_delete.mdoc
│ │ │ ├── zip_file_extra_field_get.html
│ │ │ ├── zip_file_extra_field_get.man
│ │ │ ├── zip_file_extra_field_get.mdoc
│ │ │ ├── zip_file_extra_field_set.html
│ │ │ ├── zip_file_extra_field_set.man
│ │ │ ├── zip_file_extra_field_set.mdoc
│ │ │ ├── zip_file_extra_fields_count.html
│ │ │ ├── zip_file_extra_fields_count.man
│ │ │ ├── zip_file_extra_fields_count.mdoc
│ │ │ ├── zip_file_get_comment.html
│ │ │ ├── zip_file_get_comment.man
│ │ │ ├── zip_file_get_comment.mdoc
│ │ │ ├── zip_file_get_error.html
│ │ │ ├── zip_file_get_error.man
│ │ │ ├── zip_file_get_error.mdoc
│ │ │ ├── zip_file_get_external_attributes.html
│ │ │ ├── zip_file_get_external_attributes.man
│ │ │ ├── zip_file_get_external_attributes.mdoc
│ │ │ ├── zip_file_rename.html
│ │ │ ├── zip_file_rename.man
│ │ │ ├── zip_file_rename.mdoc
│ │ │ ├── zip_file_set_comment.html
│ │ │ ├── zip_file_set_comment.man
│ │ │ ├── zip_file_set_comment.mdoc
│ │ │ ├── zip_file_set_encryption.html
│ │ │ ├── zip_file_set_encryption.man
│ │ │ ├── zip_file_set_encryption.mdoc
│ │ │ ├── zip_file_set_external_attributes.html
│ │ │ ├── zip_file_set_external_attributes.man
│ │ │ ├── zip_file_set_external_attributes.mdoc
│ │ │ ├── zip_file_set_mtime.html
│ │ │ ├── zip_file_set_mtime.man
│ │ │ ├── zip_file_set_mtime.mdoc
│ │ │ ├── zip_file_strerror.html
│ │ │ ├── zip_file_strerror.man
│ │ │ ├── zip_file_strerror.mdoc
│ │ │ ├── zip_fopen.html
│ │ │ ├── zip_fopen.man
│ │ │ ├── zip_fopen.mdoc
│ │ │ ├── zip_fopen_encrypted.html
│ │ │ ├── zip_fopen_encrypted.man
│ │ │ ├── zip_fopen_encrypted.mdoc
│ │ │ ├── zip_fread.html
│ │ │ ├── zip_fread.man
│ │ │ ├── zip_fread.mdoc
│ │ │ ├── zip_fseek.html
│ │ │ ├── zip_fseek.man
│ │ │ ├── zip_fseek.mdoc
│ │ │ ├── zip_ftell.html
│ │ │ ├── zip_ftell.man
│ │ │ ├── zip_ftell.mdoc
│ │ │ ├── zip_get_archive_comment.html
│ │ │ ├── zip_get_archive_comment.man
│ │ │ ├── zip_get_archive_comment.mdoc
│ │ │ ├── zip_get_archive_flag.html
│ │ │ ├── zip_get_archive_flag.man
│ │ │ ├── zip_get_archive_flag.mdoc
│ │ │ ├── zip_get_error.html
│ │ │ ├── zip_get_error.man
│ │ │ ├── zip_get_error.mdoc
│ │ │ ├── zip_get_file_comment.html
│ │ │ ├── zip_get_file_comment.man
│ │ │ ├── zip_get_file_comment.mdoc
│ │ │ ├── zip_get_name.html
│ │ │ ├── zip_get_name.man
│ │ │ ├── zip_get_name.mdoc
│ │ │ ├── zip_get_num_entries.html
│ │ │ ├── zip_get_num_entries.man
│ │ │ ├── zip_get_num_entries.mdoc
│ │ │ ├── zip_get_num_files.html
│ │ │ ├── zip_get_num_files.man
│ │ │ ├── zip_get_num_files.mdoc
│ │ │ ├── zip_libzip_version.html
│ │ │ ├── zip_libzip_version.man
│ │ │ ├── zip_libzip_version.mdoc
│ │ │ ├── zip_name_locate.html
│ │ │ ├── zip_name_locate.man
│ │ │ ├── zip_name_locate.mdoc
│ │ │ ├── zip_open.html
│ │ │ ├── zip_open.man
│ │ │ ├── zip_open.mdoc
│ │ │ ├── zip_register_cancel_callback_with_state.html
│ │ │ ├── zip_register_cancel_callback_with_state.man
│ │ │ ├── zip_register_cancel_callback_with_state.mdoc
│ │ │ ├── zip_register_progress_callback.html
│ │ │ ├── zip_register_progress_callback.man
│ │ │ ├── zip_register_progress_callback.mdoc
│ │ │ ├── zip_register_progress_callback_with_state.html
│ │ │ ├── zip_register_progress_callback_with_state.man
│ │ │ ├── zip_register_progress_callback_with_state.mdoc
│ │ │ ├── zip_rename.html
│ │ │ ├── zip_rename.man
│ │ │ ├── zip_rename.mdoc
│ │ │ ├── zip_set_archive_comment.html
│ │ │ ├── zip_set_archive_comment.man
│ │ │ ├── zip_set_archive_comment.mdoc
│ │ │ ├── zip_set_archive_flag.html
│ │ │ ├── zip_set_archive_flag.man
│ │ │ ├── zip_set_archive_flag.mdoc
│ │ │ ├── zip_set_default_password.html
│ │ │ ├── zip_set_default_password.man
│ │ │ ├── zip_set_default_password.mdoc
│ │ │ ├── zip_set_file_comment.html
│ │ │ ├── zip_set_file_comment.man
│ │ │ ├── zip_set_file_comment.mdoc
│ │ │ ├── zip_set_file_compression.html
│ │ │ ├── zip_set_file_compression.man
│ │ │ ├── zip_set_file_compression.mdoc
│ │ │ ├── zip_source.html
│ │ │ ├── zip_source.man
│ │ │ ├── zip_source.mdoc
│ │ │ ├── zip_source_begin_write.html
│ │ │ ├── zip_source_begin_write.man
│ │ │ ├── zip_source_begin_write.mdoc
│ │ │ ├── zip_source_buffer.html
│ │ │ ├── zip_source_buffer.man
│ │ │ ├── zip_source_buffer.mdoc
│ │ │ ├── zip_source_buffer_fragment.html
│ │ │ ├── zip_source_buffer_fragment.man
│ │ │ ├── zip_source_buffer_fragment.mdoc
│ │ │ ├── zip_source_close.html
│ │ │ ├── zip_source_close.man
│ │ │ ├── zip_source_close.mdoc
│ │ │ ├── zip_source_commit_write.html
│ │ │ ├── zip_source_commit_write.man
│ │ │ ├── zip_source_commit_write.mdoc
│ │ │ ├── zip_source_error.html
│ │ │ ├── zip_source_error.man
│ │ │ ├── zip_source_error.mdoc
│ │ │ ├── zip_source_file.html
│ │ │ ├── zip_source_file.man
│ │ │ ├── zip_source_file.mdoc
│ │ │ ├── zip_source_filep.html
│ │ │ ├── zip_source_filep.man
│ │ │ ├── zip_source_filep.mdoc
│ │ │ ├── zip_source_free.html
│ │ │ ├── zip_source_free.man
│ │ │ ├── zip_source_free.mdoc
│ │ │ ├── zip_source_function.html
│ │ │ ├── zip_source_function.man
│ │ │ ├── zip_source_function.mdoc
│ │ │ ├── zip_source_is_deleted.html
│ │ │ ├── zip_source_is_deleted.man
│ │ │ ├── zip_source_is_deleted.mdoc
│ │ │ ├── zip_source_is_seekable.html
│ │ │ ├── zip_source_is_seekable.man
│ │ │ ├── zip_source_is_seekable.mdoc
│ │ │ ├── zip_source_keep.html
│ │ │ ├── zip_source_keep.man
│ │ │ ├── zip_source_keep.mdoc
│ │ │ ├── zip_source_layered.html
│ │ │ ├── zip_source_layered.man
│ │ │ ├── zip_source_layered.mdoc
│ │ │ ├── zip_source_make_command_bitmap.html
│ │ │ ├── zip_source_make_command_bitmap.man
│ │ │ ├── zip_source_make_command_bitmap.mdoc
│ │ │ ├── zip_source_open.html
│ │ │ ├── zip_source_open.man
│ │ │ ├── zip_source_open.mdoc
│ │ │ ├── zip_source_pass_to_lower_layer.mdoc
│ │ │ ├── zip_source_read.html
│ │ │ ├── zip_source_read.man
│ │ │ ├── zip_source_read.mdoc
│ │ │ ├── zip_source_rollback_write.html
│ │ │ ├── zip_source_rollback_write.man
│ │ │ ├── zip_source_rollback_write.mdoc
│ │ │ ├── zip_source_seek.html
│ │ │ ├── zip_source_seek.man
│ │ │ ├── zip_source_seek.mdoc
│ │ │ ├── zip_source_seek_compute_offset.html
│ │ │ ├── zip_source_seek_compute_offset.man
│ │ │ ├── zip_source_seek_compute_offset.mdoc
│ │ │ ├── zip_source_seek_write.html
│ │ │ ├── zip_source_seek_write.man
│ │ │ ├── zip_source_seek_write.mdoc
│ │ │ ├── zip_source_stat.html
│ │ │ ├── zip_source_stat.man
│ │ │ ├── zip_source_stat.mdoc
│ │ │ ├── zip_source_tell.html
│ │ │ ├── zip_source_tell.man
│ │ │ ├── zip_source_tell.mdoc
│ │ │ ├── zip_source_tell_write.html
│ │ │ ├── zip_source_tell_write.man
│ │ │ ├── zip_source_tell_write.mdoc
│ │ │ ├── zip_source_win32a.html
│ │ │ ├── zip_source_win32a.man
│ │ │ ├── zip_source_win32a.mdoc
│ │ │ ├── zip_source_win32handle.html
│ │ │ ├── zip_source_win32handle.man
│ │ │ ├── zip_source_win32handle.mdoc
│ │ │ ├── zip_source_win32w.html
│ │ │ ├── zip_source_win32w.man
│ │ │ ├── zip_source_win32w.mdoc
│ │ │ ├── zip_source_window_create.html
│ │ │ ├── zip_source_window_create.man
│ │ │ ├── zip_source_window_create.mdoc
│ │ │ ├── zip_source_write.html
│ │ │ ├── zip_source_write.man
│ │ │ ├── zip_source_write.mdoc
│ │ │ ├── zip_source_zip.html
│ │ │ ├── zip_source_zip.man
│ │ │ ├── zip_source_zip.mdoc
│ │ │ ├── zip_source_zip_file.html
│ │ │ ├── zip_source_zip_file.man
│ │ │ ├── zip_source_zip_file.mdoc
│ │ │ ├── zip_stat.html
│ │ │ ├── zip_stat.man
│ │ │ ├── zip_stat.mdoc
│ │ │ ├── zip_stat_init.html
│ │ │ ├── zip_stat_init.man
│ │ │ ├── zip_stat_init.mdoc
│ │ │ ├── zip_unchange.html
│ │ │ ├── zip_unchange.man
│ │ │ ├── zip_unchange.mdoc
│ │ │ ├── zip_unchange_all.html
│ │ │ ├── zip_unchange_all.man
│ │ │ ├── zip_unchange_all.mdoc
│ │ │ ├── zip_unchange_archive.html
│ │ │ ├── zip_unchange_archive.man
│ │ │ ├── zip_unchange_archive.mdoc
│ │ │ ├── zipcmp.html
│ │ │ ├── zipcmp.man
│ │ │ ├── zipcmp.mdoc
│ │ │ ├── zipmerge.html
│ │ │ ├── zipmerge.man
│ │ │ ├── zipmerge.mdoc
│ │ │ ├── ziptool.html
│ │ │ ├── ziptool.man
│ │ │ └── ziptool.mdoc
│ │ ├── ossfuzz/
│ │ │ ├── CMakeLists.txt
│ │ │ ├── fuzz_main.c
│ │ │ ├── ossfuzz.sh
│ │ │ ├── zip_read_file_fuzzer.c
│ │ │ ├── zip_read_fuzzer.c
│ │ │ ├── zip_read_fuzzer.dict
│ │ │ ├── zip_read_fuzzer_common.h
│ │ │ ├── zip_write_encrypt_aes256_file_fuzzer.c
│ │ │ └── zip_write_encrypt_pkware_file_fuzzer.c
│ │ ├── regress/
│ │ │ ├── CMakeLists.txt
│ │ │ ├── add_dir.test
│ │ │ ├── add_from_buffer.test
│ │ │ ├── add_from_file.test
│ │ │ ├── add_from_file_duplicate.test
│ │ │ ├── add_from_file_twice_duplicate.test
│ │ │ ├── add_from_file_unchange.test
│ │ │ ├── add_from_filep.c
│ │ │ ├── add_from_filep.test
│ │ │ ├── add_from_stdin.test
│ │ │ ├── add_from_zip_closed.test
│ │ │ ├── add_from_zip_deflated.test
│ │ │ ├── add_from_zip_deflated2.test
│ │ │ ├── add_from_zip_partial_deflated.test
│ │ │ ├── add_from_zip_partial_stored.test
│ │ │ ├── add_from_zip_stored.test
│ │ │ ├── add_stored.test
│ │ │ ├── add_stored_in_memory.test
│ │ │ ├── bigstored.zh
│ │ │ ├── buffer-fragment-read.test
│ │ │ ├── buffer-fragment-write.test
│ │ │ ├── can_clone_file.c
│ │ │ ├── cancel_45.test
│ │ │ ├── cancel_90.test
│ │ │ ├── changing-size-decreases-fixed.test
│ │ │ ├── changing-size-decreases.test
│ │ │ ├── changing-size-increases-fixed.test
│ │ │ ├── changing-size-increases-unchecked.test
│ │ │ ├── changing-size-increases.test
│ │ │ ├── check_torrentzip_fail.test
│ │ │ ├── check_torrentzip_modified.test
│ │ │ ├── check_torrentzip_success.test
│ │ │ ├── cleanup.cmake
│ │ │ ├── clone-buffer-add.test
│ │ │ ├── clone-buffer-delete.test
│ │ │ ├── clone-buffer-replace.test
│ │ │ ├── clone-fs-add.test
│ │ │ ├── clone-fs-delete.test
│ │ │ ├── clone-fs-replace.test
│ │ │ ├── cm-default.test
│ │ │ ├── convert_to_torrentzip.test
│ │ │ ├── convert_to_torrentzip_ef.test
│ │ │ ├── count_entries.test
│ │ │ ├── create_empty_keep.test
│ │ │ ├── decrypt-correct-password-aes128.test
│ │ │ ├── decrypt-correct-password-aes192.test
│ │ │ ├── decrypt-correct-password-aes256.test
│ │ │ ├── decrypt-correct-password-pkware-2.test
│ │ │ ├── decrypt-correct-password-pkware.test
│ │ │ ├── decrypt-empty-file-pkware.test
│ │ │ ├── decrypt-no-password-aes256.test
│ │ │ ├── decrypt-wrong-password-aes128.test
│ │ │ ├── decrypt-wrong-password-aes192.test
│ │ │ ├── decrypt-wrong-password-aes256.test
│ │ │ ├── decrypt-wrong-password-pkware-2.test
│ │ │ ├── decrypt-wrong-password-pkware.test
│ │ │ ├── delete_add_same.test
│ │ │ ├── delete_invalid.test
│ │ │ ├── delete_last.test
│ │ │ ├── delete_last_keep.test
│ │ │ ├── delete_multiple_last.test
│ │ │ ├── delete_multiple_partial.test
│ │ │ ├── delete_renamed_rename.test
│ │ │ ├── encrypt.test
│ │ │ ├── encryption-nonrandom-aes128.test
│ │ │ ├── encryption-nonrandom-aes192.test
│ │ │ ├── encryption-nonrandom-aes256.test
│ │ │ ├── encryption-nonrandom-pkware-2.test
│ │ │ ├── encryption-nonrandom-pkware.test
│ │ │ ├── encryption-remove.test
│ │ │ ├── encryption-stat.test
│ │ │ ├── extra_add.test
│ │ │ ├── extra_add_multiple.test
│ │ │ ├── extra_count.test
│ │ │ ├── extra_count_by_id.test
│ │ │ ├── extra_count_ignore_zip64.test
│ │ │ ├── extra_delete.test
│ │ │ ├── extra_delete_by_id.test
│ │ │ ├── extra_field_align.test
│ │ │ ├── extra_get.test
│ │ │ ├── extra_get_by_id.test
│ │ │ ├── extra_set.test
│ │ │ ├── extra_set_modify_c.test
│ │ │ ├── extra_set_modify_l.test
│ │ │ ├── fdopen_ok.test
│ │ │ ├── file_comment_encmismatch.test
│ │ │ ├── fopen_multiple.test
│ │ │ ├── fopen_multiple_reopen.test
│ │ │ ├── fopen_unchanged.c
│ │ │ ├── fopen_unchanged.test
│ │ │ ├── fread.c
│ │ │ ├── fread.test
│ │ │ ├── fseek.c
│ │ │ ├── fseek_deflated.test
│ │ │ ├── fseek_fail.test
│ │ │ ├── fseek_ok.test
│ │ │ ├── get_comment.test
│ │ │ ├── get_comment_long.test
│ │ │ ├── hmac-error.test
│ │ │ ├── hole.c
│ │ │ ├── junk_at_end.test
│ │ │ ├── junk_at_start.test
│ │ │ ├── large-uncompressible
│ │ │ ├── liboverride-test.c
│ │ │ ├── liboverride.c
│ │ │ ├── malloc.c
│ │ │ ├── mtime-dstpoint.test
│ │ │ ├── mtime-post-dstpoint.test
│ │ │ ├── mtime-pre-dstpoint.test
│ │ │ ├── name_locate-cp437.test
│ │ │ ├── name_locate-utf8.test
│ │ │ ├── name_locate.test
│ │ │ ├── nihtest.conf.in
│ │ │ ├── nonrandomopen.c
│ │ │ ├── nonrandomopentest.c
│ │ │ ├── open_archive_comment_wrong.test
│ │ │ ├── open_cons_extrabytes.test
│ │ │ ├── open_empty.test
│ │ │ ├── open_empty_2.test
│ │ │ ├── open_extrabytes.test
│ │ │ ├── open_file_count.test
│ │ │ ├── open_filename_duplicate.test
│ │ │ ├── open_filename_duplicate_consistency.test
│ │ │ ├── open_filename_duplicate_empty.test
│ │ │ ├── open_filename_duplicate_empty_consistency.test
│ │ │ ├── open_filename_empty.test
│ │ │ ├── open_incons.test
│ │ │ ├── open_many_fail.test
│ │ │ ├── open_many_ok.test
│ │ │ ├── open_multidisk.test
│ │ │ ├── open_new_but_exists.test
│ │ │ ├── open_new_ok.test
│ │ │ ├── open_nonarchive.test
│ │ │ ├── open_nosuchfile.test
│ │ │ ├── open_ok.test
│ │ │ ├── open_too_short.test
│ │ │ ├── open_truncate.test
│ │ │ ├── open_truncated.test
│ │ │ ├── open_zip64_3mf.test
│ │ │ ├── open_zip64_ok.test
│ │ │ ├── preload.test
│ │ │ ├── progress.test
│ │ │ ├── read_incons.test
│ │ │ ├── read_seek_read.test
│ │ │ ├── rename_ascii.test
│ │ │ ├── rename_cp437.test
│ │ │ ├── rename_deleted.test
│ │ │ ├── rename_fail.test
│ │ │ ├── rename_ok.test
│ │ │ ├── rename_utf8.test
│ │ │ ├── rename_utf8_encmismatch.test
│ │ │ ├── reopen.test
│ │ │ ├── reopen_partial.test
│ │ │ ├── reopen_partial_rest.test
│ │ │ ├── replace_set_stored.test
│ │ │ ├── set_comment_all.test
│ │ │ ├── set_comment_localonly.test
│ │ │ ├── set_comment_removeglobal.test
│ │ │ ├── set_comment_revert.test
│ │ │ ├── set_compression_bzip2_to_deflate.test
│ │ │ ├── set_compression_deflate_to_bzip2.test
│ │ │ ├── set_compression_deflate_to_deflate.test
│ │ │ ├── set_compression_deflate_to_store.test
│ │ │ ├── set_compression_lzma_no_eos_to_store.test
│ │ │ ├── set_compression_lzma_to_store.test
│ │ │ ├── set_compression_store_to_bzip2.test
│ │ │ ├── set_compression_store_to_deflate.test
│ │ │ ├── set_compression_store_to_lzma.test
│ │ │ ├── set_compression_store_to_store.test
│ │ │ ├── set_compression_store_to_xz.test
│ │ │ ├── set_compression_store_to_zstd.test
│ │ │ ├── set_compression_unknown.test
│ │ │ ├── set_compression_xz_to_store.test
│ │ │ ├── set_compression_zstd_to_store.test
│ │ │ ├── set_file_dostime.test
│ │ │ ├── set_file_mtime.test
│ │ │ ├── set_file_mtime_pkware.test
│ │ │ ├── short
│ │ │ ├── source_hole.c
│ │ │ ├── stat_index_cp437_guess.test
│ │ │ ├── stat_index_cp437_raw.test
│ │ │ ├── stat_index_cp437_strict.test
│ │ │ ├── stat_index_fileorder.test
│ │ │ ├── stat_index_streamed.test
│ │ │ ├── stat_index_streamed_zip64.test
│ │ │ ├── stat_index_utf8_guess.test
│ │ │ ├── stat_index_utf8_raw.test
│ │ │ ├── stat_index_utf8_strict.test
│ │ │ ├── stat_index_utf8_unmarked_strict.test
│ │ │ ├── stat_index_zip64.test
│ │ │ ├── testfile.txt
│ │ │ ├── truncate_empty_keep.test
│ │ │ ├── tryopen.c
│ │ │ ├── unchange-delete-namelocate.test
│ │ │ ├── utf-8-standardization.test
│ │ │ ├── want_torrentzip_stat.test
│ │ │ ├── zip-in-archive-comment.test
│ │ │ ├── zip64-in-archive-comment.test
│ │ │ ├── zip64_creation.test
│ │ │ ├── zip64_stored_creation.test
│ │ │ ├── zipcmp_zip_dir.test
│ │ │ ├── zipcmp_zip_dir_slash.test
│ │ │ └── ziptool_regress.c
│ │ ├── src/
│ │ │ ├── CMakeLists.txt
│ │ │ ├── diff_output.c
│ │ │ ├── diff_output.h
│ │ │ ├── getopt.c
│ │ │ ├── getopt.h
│ │ │ ├── zipcmp.c
│ │ │ ├── zipmerge.c
│ │ │ └── ziptool.c
│ │ ├── vcpkg.json
│ │ └── zipconf.h.in
│ ├── pdfium/
│ │ ├── README.md
│ │ ├── linux-x64/
│ │ │ ├── LICENSE
│ │ │ ├── PDFiumConfig.cmake
│ │ │ ├── VERSION
│ │ │ ├── args.gn
│ │ │ ├── include/
│ │ │ │ ├── cpp/
│ │ │ │ │ ├── fpdf_deleters.h
│ │ │ │ │ └── fpdf_scopers.h
│ │ │ │ ├── fpdf_annot.h
│ │ │ │ ├── fpdf_attachment.h
│ │ │ │ ├── fpdf_catalog.h
│ │ │ │ ├── fpdf_dataavail.h
│ │ │ │ ├── fpdf_doc.h
│ │ │ │ ├── fpdf_edit.h
│ │ │ │ ├── fpdf_ext.h
│ │ │ │ ├── fpdf_flatten.h
│ │ │ │ ├── fpdf_formfill.h
│ │ │ │ ├── fpdf_fwlevent.h
│ │ │ │ ├── fpdf_javascript.h
│ │ │ │ ├── fpdf_ppo.h
│ │ │ │ ├── fpdf_progressive.h
│ │ │ │ ├── fpdf_save.h
│ │ │ │ ├── fpdf_searchex.h
│ │ │ │ ├── fpdf_signature.h
│ │ │ │ ├── fpdf_structtree.h
│ │ │ │ ├── fpdf_sysfontinfo.h
│ │ │ │ ├── fpdf_text.h
│ │ │ │ ├── fpdf_thumbnail.h
│ │ │ │ ├── fpdf_transformpage.h
│ │ │ │ ├── fpdfview.h
│ │ │ │ └── fpdfview.h.orig
│ │ │ └── licenses/
│ │ │ ├── abseil.txt
│ │ │ ├── agg23.txt
│ │ │ ├── fast_float.txt
│ │ │ ├── freetype.txt
│ │ │ ├── icu.txt
│ │ │ ├── lcms.txt
│ │ │ ├── libjpeg_turbo.ijg
│ │ │ ├── libjpeg_turbo.md
│ │ │ ├── libopenjpeg.txt
│ │ │ ├── libpng.txt
│ │ │ ├── libtiff.txt
│ │ │ ├── llvm-libc.txt
│ │ │ ├── pdfium.txt
│ │ │ ├── simdutf.txt
│ │ │ └── zlib.txt
│ │ ├── macos-arm64/
│ │ │ ├── LICENSE
│ │ │ ├── PDFiumConfig.cmake
│ │ │ ├── VERSION
│ │ │ ├── args.gn
│ │ │ ├── include/
│ │ │ │ ├── cpp/
│ │ │ │ │ ├── fpdf_deleters.h
│ │ │ │ │ └── fpdf_scopers.h
│ │ │ │ ├── fpdf_annot.h
│ │ │ │ ├── fpdf_attachment.h
│ │ │ │ ├── fpdf_catalog.h
│ │ │ │ ├── fpdf_dataavail.h
│ │ │ │ ├── fpdf_doc.h
│ │ │ │ ├── fpdf_edit.h
│ │ │ │ ├── fpdf_ext.h
│ │ │ │ ├── fpdf_flatten.h
│ │ │ │ ├── fpdf_formfill.h
│ │ │ │ ├── fpdf_fwlevent.h
│ │ │ │ ├── fpdf_javascript.h
│ │ │ │ ├── fpdf_ppo.h
│ │ │ │ ├── fpdf_progressive.h
│ │ │ │ ├── fpdf_save.h
│ │ │ │ ├── fpdf_searchex.h
│ │ │ │ ├── fpdf_signature.h
│ │ │ │ ├── fpdf_structtree.h
│ │ │ │ ├── fpdf_sysfontinfo.h
│ │ │ │ ├── fpdf_text.h
│ │ │ │ ├── fpdf_thumbnail.h
│ │ │ │ ├── fpdf_transformpage.h
│ │ │ │ ├── fpdfview.h
│ │ │ │ └── fpdfview.h.orig
│ │ │ └── licenses/
│ │ │ ├── abseil.txt
│ │ │ ├── agg23.txt
│ │ │ ├── fast_float.txt
│ │ │ ├── freetype.txt
│ │ │ ├── icu.txt
│ │ │ ├── lcms.txt
│ │ │ ├── libjpeg_turbo.ijg
│ │ │ ├── libjpeg_turbo.md
│ │ │ ├── libopenjpeg.txt
│ │ │ ├── libpng.txt
│ │ │ ├── libtiff.txt
│ │ │ ├── llvm-libc.txt
│ │ │ ├── pdfium.txt
│ │ │ ├── simdutf.txt
│ │ │ └── zlib.txt
│ │ ├── macos-x64/
│ │ │ ├── LICENSE
│ │ │ ├── PDFiumConfig.cmake
│ │ │ ├── VERSION
│ │ │ ├── args.gn
│ │ │ ├── include/
│ │ │ │ ├── cpp/
│ │ │ │ │ ├── fpdf_deleters.h
│ │ │ │ │ └── fpdf_scopers.h
│ │ │ │ ├── fpdf_annot.h
│ │ │ │ ├── fpdf_attachment.h
│ │ │ │ ├── fpdf_catalog.h
│ │ │ │ ├── fpdf_dataavail.h
│ │ │ │ ├── fpdf_doc.h
│ │ │ │ ├── fpdf_edit.h
│ │ │ │ ├── fpdf_ext.h
│ │ │ │ ├── fpdf_flatten.h
│ │ │ │ ├── fpdf_formfill.h
│ │ │ │ ├── fpdf_fwlevent.h
│ │ │ │ ├── fpdf_javascript.h
│ │ │ │ ├── fpdf_ppo.h
│ │ │ │ ├── fpdf_progressive.h
│ │ │ │ ├── fpdf_save.h
│ │ │ │ ├── fpdf_searchex.h
│ │ │ │ ├── fpdf_signature.h
│ │ │ │ ├── fpdf_structtree.h
│ │ │ │ ├── fpdf_sysfontinfo.h
│ │ │ │ ├── fpdf_text.h
│ │ │ │ ├── fpdf_thumbnail.h
│ │ │ │ ├── fpdf_transformpage.h
│ │ │ │ ├── fpdfview.h
│ │ │ │ └── fpdfview.h.orig
│ │ │ └── licenses/
│ │ │ ├── abseil.txt
│ │ │ ├── agg23.txt
│ │ │ ├── fast_float.txt
│ │ │ ├── freetype.txt
│ │ │ ├── icu.txt
│ │ │ ├── lcms.txt
│ │ │ ├── libjpeg_turbo.ijg
│ │ │ ├── libjpeg_turbo.md
│ │ │ ├── libopenjpeg.txt
│ │ │ ├── libpng.txt
│ │ │ ├── libtiff.txt
│ │ │ ├── llvm-libc.txt
│ │ │ ├── pdfium.txt
│ │ │ ├── simdutf.txt
│ │ │ └── zlib.txt
│ │ └── windows-x64/
│ │ ├── LICENSE
│ │ ├── PDFiumConfig.cmake
│ │ ├── VERSION
│ │ ├── args.gn
│ │ ├── include/
│ │ │ ├── cpp/
│ │ │ │ ├── fpdf_deleters.h
│ │ │ │ └── fpdf_scopers.h
│ │ │ ├── fpdf_annot.h
│ │ │ ├── fpdf_attachment.h
│ │ │ ├── fpdf_catalog.h
│ │ │ ├── fpdf_dataavail.h
│ │ │ ├── fpdf_doc.h
│ │ │ ├── fpdf_edit.h
│ │ │ ├── fpdf_ext.h
│ │ │ ├── fpdf_flatten.h
│ │ │ ├── fpdf_formfill.h
│ │ │ ├── fpdf_fwlevent.h
│ │ │ ├── fpdf_javascript.h
│ │ │ ├── fpdf_ppo.h
│ │ │ ├── fpdf_progressive.h
│ │ │ ├── fpdf_save.h
│ │ │ ├── fpdf_searchex.h
│ │ │ ├── fpdf_signature.h
│ │ │ ├── fpdf_structtree.h
│ │ │ ├── fpdf_sysfontinfo.h
│ │ │ ├── fpdf_text.h
│ │ │ ├── fpdf_thumbnail.h
│ │ │ ├── fpdf_transformpage.h
│ │ │ ├── fpdfview.h
│ │ │ └── fpdfview.h.orig
│ │ ├── lib/
│ │ │ └── pdfium.dll.lib
│ │ └── licenses/
│ │ ├── abseil.txt
│ │ ├── agg23.txt
│ │ ├── fast_float.txt
│ │ ├── freetype.txt
│ │ ├── icu.txt
│ │ ├── lcms.txt
│ │ ├── libjpeg_turbo.ijg
│ │ ├── libjpeg_turbo.md
│ │ ├── libopenjpeg.txt
│ │ ├── libpng.txt
│ │ ├── libtiff.txt
│ │ ├── llvm-libc.txt
│ │ ├── pdfium.txt
│ │ ├── simdutf.txt
│ │ └── zlib.txt
│ └── pugixml/
│ ├── CMakeLists.txt
│ ├── LICENSE.md
│ ├── docs/
│ │ ├── manual.html
│ │ ├── quickstart.html
│ │ └── samples/
│ │ ├── character.xml
│ │ ├── custom_memory_management.cpp
│ │ ├── include.cpp
│ │ ├── load_error_handling.cpp
│ │ ├── load_file.cpp
│ │ ├── load_memory.cpp
│ │ ├── load_options.cpp
│ │ ├── load_stream.cpp
│ │ ├── modify_add.cpp
│ │ ├── modify_base.cpp
│ │ ├── modify_remove.cpp
│ │ ├── save_custom_writer.cpp
│ │ ├── save_declaration.cpp
│ │ ├── save_file.cpp
│ │ ├── save_options.cpp
│ │ ├── save_stream.cpp
│ │ ├── save_subtree.cpp
│ │ ├── text.cpp
│ │ ├── transitions.xml
│ │ ├── traverse_base.cpp
│ │ ├── traverse_iter.cpp
│ │ ├── traverse_predicate.cpp
│ │ ├── traverse_rangefor.cpp
│ │ ├── traverse_walker.cpp
│ │ ├── tree.xml
│ │ ├── weekly-shift_jis.xml
│ │ ├── weekly-utf-16.xml
│ │ ├── weekly-utf-8.xml
│ │ ├── xgconsole.xml
│ │ ├── xpath_error.cpp
│ │ ├── xpath_query.cpp
│ │ ├── xpath_select.cpp
│ │ └── xpath_variables.cpp
│ ├── readme.txt
│ ├── scripts/
│ │ ├── cocoapods_push.sh
│ │ ├── natvis/
│ │ │ ├── pugixml.natvis
│ │ │ └── pugixml_compact.natvis
│ │ ├── nuget/
│ │ │ └── pugixml.nuspec
│ │ ├── nuget_build.ps1
│ │ ├── premake4.lua
│ │ ├── pugixml-config.cmake.in
│ │ ├── pugixml.pc.in
│ │ ├── pugixml.podspec
│ │ ├── pugixml.xcodeproj/
│ │ │ └── project.pbxproj
│ │ ├── pugixml_airplay.mkf
│ │ ├── pugixml_codeblocks.cbp
│ │ ├── pugixml_codelite.project
│ │ ├── pugixml_dll.rc
│ │ ├── pugixml_vs2005.vcproj
│ │ ├── pugixml_vs2005_static.vcproj
│ │ ├── pugixml_vs2008.vcproj
│ │ ├── pugixml_vs2008_static.vcproj
│ │ ├── pugixml_vs2010.vcxproj
│ │ ├── pugixml_vs2010_static.vcxproj
│ │ ├── pugixml_vs2013.vcxproj
│ │ ├── pugixml_vs2013_static.vcxproj
│ │ ├── pugixml_vs2015.vcxproj
│ │ ├── pugixml_vs2015_static.vcxproj
│ │ ├── pugixml_vs2017.vcxproj
│ │ ├── pugixml_vs2017_static.vcxproj
│ │ ├── pugixml_vs2019.vcxproj
│ │ ├── pugixml_vs2019_static.vcxproj
│ │ ├── pugixml_vs2022.vcxproj
│ │ ├── pugixml_vs2022_static.vcxproj
│ │ └── sbom.cdx.json
│ └── src/
│ ├── pugiconfig.hpp
│ ├── pugixml.cpp
│ └── pugixml.hpp
└── tests/
├── .gitignore
├── run_all_tests.sh
├── run_database_tests.sh
├── run_translation_tests.sh
└── unit/
├── TestHelpers.hpp
├── test_cache_interactions.cpp
├── test_categorization_dialog.cpp
├── test_checkbox_matrix.cpp
├── test_cli_reporter.cpp
├── test_custom_api_endpoint.cpp
├── test_custom_llm.cpp
├── test_database_manager_rename_only.cpp
├── test_file_scanner.cpp
├── test_ggml_runtime_paths.cpp
├── test_image_rename_metadata_service.cpp
├── test_llava_image_analyzer.cpp
├── test_llm_downloader.cpp
├── test_llm_selection_dialog_visual.cpp
├── test_local_llm_backend.cpp
├── test_main_app_image_options.cpp
├── test_main_app_translation.cpp
├── test_main_app_visual_fallback.cpp
├── test_media_rename_metadata_service.cpp
├── test_review_dialog_rename_gate.cpp
├── test_settings_image_options.cpp
├── test_support_prompt.cpp
├── test_ui_translator.cpp
├── test_update_feed.cpp
├── test_updater.cpp
├── test_updater_build_modes.cpp
├── test_utils.cpp
└── test_whitelist_and_prompt.cpp
================================================
FILE CONTENTS
================================================
================================================
FILE: .clang-format
================================================
BasedOnStyle: Google
IndentWidth: 4
TabWidth: 4
UseTab: Never
ColumnLimit: 120
BreakBeforeBraces: Attach
AllowShortFunctionsOnASingleLine: Inline
PointerAlignment: Left
DerivePointerAlignment: false
SpacesInParentheses: false
SpaceAfterCStyleCast: true
SpaceBeforeParens: ControlStatements
SortIncludes: true
================================================
FILE: .github/workflows/build.yml
================================================
name: Build
on:
pull_request:
branches: [ main, dev ]
jobs:
build-linux:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up Qt
uses: jurplel/install-qt-action@v3
with:
version: '6.6.3'
host: linux
target: desktop
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake ninja-build libcurl4-openssl-dev libjsoncpp-dev libsqlite3-dev libssl-dev libfmt-dev libspdlog-dev
- name: Configure
run: |
cmake -S app -B build -G "Ninja" -DCMAKE_BUILD_TYPE=Release
- name: Build
run: cmake --build build
- name: Run ctest
run: cd build && ctest --output-on-failure
build-windows:
runs-on: windows-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Enable long paths
run: git config --system core.longpaths true
- name: Set up vcpkg
shell: pwsh
run: |
git clone https://github.com/microsoft/vcpkg.git "$env:GITHUB_WORKSPACE\vcpkg"
& "$env:GITHUB_WORKSPACE\vcpkg\bootstrap-vcpkg.bat"
- name: Build llama runtime
shell: pwsh
run: |
$vcpkgRoot = "$env:GITHUB_WORKSPACE\vcpkg"
& "$env:GITHUB_WORKSPACE\app\scripts\build_llama_windows.ps1" `
"cuda=off" `
"vulkan=off" `
"blas=off" `
"vcpkgroot=$vcpkgRoot"
- name: Build and test Windows app
shell: pwsh
run: |
$vcpkgRoot = "$env:GITHUB_WORKSPACE\vcpkg"
& "$env:GITHUB_WORKSPACE\app\build_windows.ps1" `
-Configuration Release `
-VcpkgRoot $vcpkgRoot `
-BuildTests `
-RunTests `
-Parallel 4
================================================
FILE: .gitignore
================================================
# Created by https://www.toptal.com/developers/gitignore/api/c++,visualstudiocode
# Edit at https://www.toptal.com/developers/gitignore?templates=c++,visualstudiocode
### C++ ###
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
obj/
# Precompiled Headers
*.gch
*.pch
# Databases
*.db
# Environment variables
# .env
encryption.ini
# Virtual environments
.venv/
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
main
obfuscate_encrypt
bin/
# Keep vendored PDFium runtimes in source control.
!external/pdfium/**/bin/
!external/pdfium/**/bin/pdfium.dll
!external/pdfium/**/lib/
!external/pdfium/**/lib/pdfium.dll.lib
!external/pdfium/**/lib/libpdfium.so
!external/pdfium/**/lib/libpdfium.dylib
# Scripts
reset_key_4_github.sh
create_macos_bundle.sh
create_dmg.sh
# Packaging
*.nsi
*.msix
msix/
package_msix.ps1
appcert-AIFileSorter.xml
msix-layout/
# References
# includePaths.txt
### VisualStudioCode ###
.vscode/
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
### Visual Studio
vcpkg_installed/
# Local History for Visual Studio Code
.history/
# Cache directories
.cache/
.ccache*/
.codacy/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
# Temp files
*.*~
*~
# Temp notes
todos_md/
# Mac files
.DS_Store
# Script docs + backups
app/scripts/SCRIPTS-README.md
app/scripts/local_support_code_signer.py
app/scripts/__pycache__/
*.bak
# Ignore llama headers (generated or from external)
app/include/llama/*.h
# R & D stage
/assets/
/docs/
/scripts/
!app/scripts/
/tools/
/reports/
/logo-workdir-temp/
/Testing/
/changelog/
/models/
/rd/
/prototypes/
/store/ms-store/
/.github/workflows/ms-store-listing.yml
# Build artifacts
build*/
dist/
app/lib/ggml/
# Generated precompiled llama runtime directories
app/lib/precompiled*/
external/libzip/build*/
# No longer relevant
api-key-encryption/
# End of https://www.toptal.com/developers/gitignore/api/c++,visualstudiocode
#Ignore vscode AI rules
.github/instructions/codacy.instructions.md
================================================
FILE: .gitmodules
================================================
[submodule "app/include/external/llama.cpp"]
path = app/include/external/llama.cpp
url = https://github.com/ggerganov/llama.cpp.git
ignore = dirty
[submodule "external/Catch2"]
path = external/Catch2
url = https://github.com/catchorg/Catch2.git
================================================
FILE: CHANGELOG.md
================================================
# Changelog
## [1.7.3] - 2026-03-22
- Non-English categorization is now more reliable: files are categorized canonically in English first, then translated into the selected category language, with localized labels persisted separately from the canonical taxonomy/cache.
- App updates now support separate update streams for Windows, macOS, and Linux, while still accepting the legacy single-stream manifest format for newer clients.
- Windows feeds can now provide a direct installer URL plus SHA-256 checksum so the app can download the installer, show download progress, verify its integrity, and launch it after confirmation.
- The UI translation system was migrated fully to Qt `.ts` / `.qm` catalogs.
- Local categorization with local LLMs is now more robust: prompt budgeting, output sanitization, and category/subcategory parsing were hardened so verbose or oddly formatted replies no longer cause widespread invalid categorization failures.
- Recursive scans now tolerate unreadable subfolders and other filesystem errors instead of aborting the overall run.
- Cached category labels are sanitized more aggressively to avoid malformed UTF-8 data breaking later categorization or display.
- macOS local-LLM packaging/runtime handling was hardened: bundled llama/ggml dylibs are now relocatable, and the app no longer falls back to conflicting system/Homebrew ggml libraries during backend loading.
- Linux/macOS build and packaging flows were improved, including staged PDFium runtime files, better Debian package dependencies, CPU/CUDA/Vulkan Debian package variants, and improved Homebrew MediaInfo detection on macOS source builds.
- Added cross-platform diagnostics collection scripts for Linux, macOS, and Windows.
- Misc improvements.
- Misc bug fixes.
## [1.7.0] - 2026-03-08
- Progress dialog redesigned into a stage-based table view with explicit stages for Image analysis, Document analysis, and Categorization.
- Added an image analysis option to append image creation dates (if available) to category names.
- Added optional audio/video metadata-based filename suggestions for supported media files. When enabled, AI File Sorter can use embedded tags (such as ID3, Vorbis comments, and MP4-style metadata) to propose normalized names like `year_artist_album_title.ext` during review.
- Bug fixes.
## [1.6.1] - 2026-02-06
- Local text LLM now prompts to switch to CPU when GPU initialization or inference fails.
## [1.6.0] - 2026-02-04
- Added document content analysis (text LLM) with rename-only/document-only options and optional creation-date suffixes for categories. Supported document formats include PDF, DOCX, XLSX, PPTX, ODT, ODS, and ODP (plus common text formats).
- Local 3B model download now defaults to Q4 for better GPU compatibility. The legacy Local 3B Q8 is still selectable when an existing download is found.
- Improved the LLM selection dialog latency.
- Added custom API endpoints to the Select LLM dialog. Custom endpoints accept base URLs or full /chat/completions endpoints, with optional API keys for local servers.
- LLM-derived categorizations and rename suggestions are now saved as you go, so progress isn't lost if the app closes unexpectedly.
- Image analysis now falls back (with a user prompt) to CPU if the GPU has insufficient available memory.
- Review dialog now lets you select highlighted rows and bulk edit their categories.
- Review dialog is now scrollable on smaller screens so action buttons stay visible.
- Improved subcategory consistency by merging labels that only differ by generic suffixes (e.g., “files”).
- Added a system compatibility check (benchmarking) to determine the most suitable LLM for your system.
- Added Korean as an interface language.
- macOS builds now include variant `make` targets for Apple Silicon (M1 / M2-M3) and Intel outputs, plus improved arch-aware llama.cpp builds.
- UI, stability, persistence, and usability improvements.
## [1.5.0] - 2026-01-11
- Added content analysis for picture files via LLaVA (visual LLM), with separate model + mmproj downloads in the Select LLM dialog.
- Added image analysis options in the main window (analyze images, offer rename suggestions, rename-only mode).
- Added an image-only processing toggle to focus runs on supported picture files and disable standard categorization controls.
- Added document content analysis (text LLM) with rename-only/document-only modes and optional creation-date suffixes for categories.
- Added support for document formats including PDF, DOCX, XLSX, PPTX, ODT, ODS, and ODP (plus common text formats).
- Document analysis now uses embedded PDFium/libzip/pugixml in bundled builds (no pdftotext/unzip requirement).
- Review dialog now supports rename-only flows, suggested filename edits, and status labels for Renamed / Renamed & Moved.
- Track applied picture renames so already-renamed files are not reprocessed; rename-only review hides them while categorization review keeps them visible for folder moves.
- Added Dutch as a selectable interface language.
- Analysis progress dialog output is now fully localized (status tags, scan/process lines, and file/directory labels) to match the selected UI language.
- Build/test updates: mtmd progress callback auto-detection, mtmd-cli build fix, and new Catch2 tests for rename-only caching.
## [1.4.5] - 2025-12-05
- Added support for Gemini (a remote LLM) - with your own Gemini API key.
- Fixed compile under Arch Linux.
## [1.4.0] - 2025-12-05
- Added dry run / preview-only mode with From/To table, no moves performed until you uncheck.
- Persistent Undo: the latest sort saves a plan file; use Edit -> "Undo last run" even after closing dialogs.
- UI tweaks: Name column auto-resizes, new translations for dry run/undo strings, Undo moved to top of Edit menu.
- A few more guard rails added.
- Remote LLM flow now uses your own OpenAI API key (any ChatGPT model supported); the bundled remote key and obfuscation step were removed.
## [1.3.0] - 2025-11-21
- You can now switch between two categorization modes: More Refined and More Consistent. Choose depending on your folder and use case.
- Added optional Whitelists - limit the number and names of categories when needed.
- Added sorting by file names, categories, subcategories in the Categorization Review dialog.
- You can now add a custom Local LLM in the Select LLM dialog.
- Multilingual categorization: the file categorization labels can now be assigned in Dutch, French, German, Italian, Polish, Portuguese, Spanish, and Turkish.
- New interface languages: Dutch, German, Italian, Polish, Portugese, Spanish, and Turkish.
## [1.1.0] - 2025-11-08
- New feature: Support for Vulkan. This means that many non-Nvidia graphics cards (GPUs) are now supported for compute acceleration during local LLM inference.
- New feature: Toggle subcategories in the categorization review dialog.
- New feature: Undo the recent file sort (move) action.
- Fixes: Bug fixes and stability improvements.
- Added a CTest-integrated test suite. Expanded test coverage.
- Code optimization refactors.
## [1.0.0] - 2025-10-30
- Migrated the entire desktop UI from GTK/Glade to a native Qt6 interface.
- Added selection boxes for files in the categorization review dialog.
- Added internatioinalization framework and the French translation for the user interface.
- Added refreshed menu icons, mnemonic behaviour, and persistent File Explorer settings.
- Simplified cross-platform builds (Linux/macOS) around Qt6; retired the MSYS2/GTK toolchain.
- Optimized and cleaned up the code. Fixed error-prone areas.
- Modernized the build pipeline. Introduced CMake for compilation on Windows.
## [0.9.7] - 2025-10-19
- Added paths to files in LLM requests for more context.
- Added taxonomy for more consistent assignment of categories across categorizations.
(Narrowing down the number of categories and subcategories).
- Improved the readability of the categorization progress dialog box.
- Improved the stability of CUDA detection and interaction.
- Added more logging coverage throughout the code base.
## [0.9.3] - 2025-09-22
- Added compatibility with CUDA 13.
## [0.9.2] - 2025-08-06
- Bug fixes.
- Increased code coverage with logging.
## [0.9.1] - 2025-08-01
- Bug fixes.
- Minor improvements for stability.
- Removed the deprecated GPU backend from the runtime build.
## [0.9.0] - 2025-07-18
- Local LLM support with `llama.cpp`.
- LLM selection and download dialog.
- Improved `Makefile` for a more hassle-free build and installation.
- Minor bug fixes and improvements.
================================================
FILE: LICENSE
================================================
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 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 Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are 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.
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.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
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 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 work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
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 AGPL, see
.
================================================
FILE: README.md
================================================
# AI File Sorter
[](#)
[](#)

[](https://sourceforge.net/projects/ai-file-sorter/files/latest/download)
[](https://app.codacy.com/gh/hyperfield/ai-file-sorter/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
[](https://filesorter.app/donate/)
AI File Sorter is a cross-platform desktop application that uses AI to organize files and suggest cleaner, more consistent names for images, documents, and supported audio/video files. It is designed to reduce clutter, improve consistency, and make files easier to find later, whether for review, archiving, or long-term storage.
The app can analyze picture files locally and suggest meaningful, human-readable names. For example, a generic file like IMG_2048.jpg can be renamed to something descriptive such as clouds_over_lake.jpg. It can also analyze supported document files and propose clearer names based on their text content. AI File Sorter can also clean up messy audio and video filenames by using the metadata already stored inside supported media files. If tags such as year, artist, album, or title are available, the app can turn them into a clear suggestion like `2024_artist_album_title.mp3`, which you can review, edit, or ignore before any change is applied.
AI File Sorter helps tidy up cluttered folders such as Downloads, external drives, or NAS storage by automatically grouping files based on their names, extensions, folder context, and learned organization patterns.
Instead of relying on fixed rules, the app gradually builds an internal understanding of how your files are typically organized and named. This allows it to make more consistent categorization and naming suggestions over time, while still letting you review and adjust everything before anything is applied.
Categories (and optional subcategories) are suggested for each file, and for supported file types, rename suggestions are provided as well. Once you confirm, the required folders are created automatically and files are sorted accordingly.
Privacy-first by design:
AI File Sorter can run entirely on your device, using local AI models such as Llama 3B (Q4) and Mistral 7B. No files, filenames, images, or metadata are uploaded anywhere, and no telemetry is sent. An internet connection is only needed if you explicitly choose to enable a remote model.
---
#### How It Works
1. Point the app at a folder or drive
2. Files (and image content, when applicable) are analyzed using the selected local or remote model
3. Category and rename suggestions are generated
4. You review and adjust if needed - done
---
[](https://sourceforge.net/projects/ai-file-sorter/files/latest/download)
[](https://apps.microsoft.com/detail/9npk4dzd6r6s)
  
---
- [AI File Sorter](#ai-file-sorter)
- [Changelog](#changelog)
- [Features](#features)
- [Categorization](#categorization)
- [Categorization modes](#categorization-modes)
- [Category whitelists](#category-whitelists)
- [Image analysis (Visual LLM)](#image-analysis-visual-llm)
- [Required visual LLM files](#required-visual-llm-files)
- [Main window options](#main-window-options)
- [Document analysis (Text LLM)](#document-analysis-text-llm)
- [Supported document formats](#supported-document-formats)
- [Main window options (documents)](#main-window-options-documents)
- [Audio/video metadata filename suggestions](#audiovideo-metadata-filename-suggestions)
- [Supported audio/video formats](#supported-audiovideo-formats)
- [System compatibility check](#system-compatibility-check)
- [Requirements](#requirements)
- [Installation](#installation)
- [Linux](#linux)
- [macOS](#macos)
- [Windows](#windows)
- [Categorization cache database](#categorization-cache-database)
- [Uninstallation](#uninstallation)
- [Using your OpenAI API key](#using-your-openai-api-key)
- [Using your Gemini API key](#using-your-gemini-api-key)
- [Testing](#testing)
- [Diagnostics](#diagnostics)
- [How to Use](#how-to-use)
- [Sorting a Remote Directory (e.g., NAS)](#sorting-a-remote-directory-eg-nas)
- [Contributing](#contributing)
- [Credits](#credits)
- [License](#license)
- [Donation](#donation)
---
## Changelog
## [1.7.3] - 2026-03-22
- Non-English categorization is now more reliable: files are categorized canonically in English first, then translated into the selected category language. This change is due to LLM language limitations.
- App updates now support separate update streams for Windows, macOS, and Linux, while still accepting the legacy single-stream manifest format for newer clients.
- Windows feeds can now provide a direct installer URL plus SHA-256 checksum so the app can download the installer, show download progress, verify its integrity, and launch it after confirmation.
- The UI translation system was migrated fully to Qt `.ts` / `.qm` catalogs.
- Local categorization with local LLMs is now more robust.
- Cached category labels are sanitized more aggressively to avoid malformed UTF-8 data breaking later categorization or display.
- Misc improvements.
- Misc bug fixes.
See [CHANGELOG.md](CHANGELOG.md) for the full history.
---
## Features
- **AI-Powered Categorization**: Classify files intelligently using either a **local LLM** (Llama, Mistral) or a remote model (ChatGPT with your own OpenAI API key, or Gemini with your own Gemini API key).
- **Offline-Friendly**: Use a local LLM to categorize files entirely - no internet or API key required.
- **Robust categorization**: Taxonomy and heuristics help keep labels more consistent across runs.
- **Customizable sorting rules**: Automatically assign categories and subcategories for granular organization.
- **Two categorization modes**: Pick **More Refined** for detailed labels or **More Consistent** to bias toward uniform categories within a folder.
- **Category whitelists**: Define named whitelists of allowed categories/subcategories, manage them under **Settings → Manage category whitelists…**, and toggle/select them in the main window when you want to constrain model output for a session.
- **Multilingual categorization**: Have the LLM assign categories in Dutch, French, German, Italian, Polish, Portuguese, Spanish, or Turkish (model dependent).
- **Custom local LLMs**: Register your own local GGUF models directly from the **Select LLM** dialog.
- **Image content analysis (Visual LLM)**: Analyze supported picture files with LLaVA to produce descriptions and optional filename suggestions (rename-only mode supported).
- **Image date-to-category suffix (optional)**: Append image creation date metadata to image category names when available.
- **Document content analysis (Text LLM)**: Analyze supported document files to summarize content and suggest filenames; uses the same selected LLM (local or remote).
- **Audio/video metadata filename suggestions**: Turn embedded media tags into clean, library-style filenames for supported audio and video files, with full review before anything is renamed.
- **Sortable review**: Sort the Categorization Review table by file name, category, or subcategory to triage faster.
- **Qt6 Interface**: Lightweight and responsive UI with refreshed menus and icons.
- **Interface languages**: English, Dutch, French, German, Italian, Korean, Spanish, and Turkish.
- **Cross-Platform Compatibility**: Works on Windows, macOS, and Linux.
- **Local Database Caching**: Speeds up repeated categorization and minimizes remote LLM usage costs.
- **Sorting Preview**: See how files will be organized before confirming changes.
- **Dry run** / preview-only mode to inspect planned moves without touching files.
- **Persistent Undo** ("Undo last run") even after closing the sort dialog.
- **Bring your own key**: Paste your OpenAI or Gemini API key once; it's stored locally and reused for remote runs.
- **Update Notifications**: Get notified about updates - with optional or required update flows.
---
## Categorization
### Categorization modes
- **More refined**: The flexible, detail-oriented mode. Consistency hints are disabled so the model can pick the most specific category/subcategory it deems appropriate, which is useful for long-tail or mixed folders.
- **More consistent**: The uniform mode. The model receives consistency hints from prior assignments in the current session so files with similar names/extensions trend toward the same categories. This is helpful when you want strict uniformity across a batch.
- Switch between the two via the **Categorization type** radio buttons on the main window; your choice is saved for the next run.
### Category whitelists
- Enable **Use a whitelist** to inject the selected whitelist into the LLM prompt; disable it to let the model choose freely.
- Manage lists (add, edit, remove) under **Settings → Manage category whitelists…**. A default list is auto-created only when no lists exist, and multiple named lists can be kept for different projects.
- Keep each whitelist to roughly **15–20 categories/subcategories** to avoid overlong prompts on smaller local models. Use several narrower lists instead of a single very long one.
- Whitelists apply in either categorization mode; pair them with **More consistent** when you want the strongest adherence to a constrained vocabulary.
---
## Image analysis (Visual LLM)
Image analysis uses a local LLaVA-based visual LLM to describe image contents and (optionally) suggest a better filename. This runs locally and does not require an API key.
### Required visual LLM files
The **Select LLM** dialog now includes an "Image analysis models (LLaVA)" section with two downloads:
- **LLaVA text model (GGUF)**: The main language model that produces the description and the filename suggestion.
- **LLaVA mmproj (vision encoder projection, GGUF)**: The adapter that maps vision embeddings into the LLM token space so the model can accept images.
Both files are required. If either one is missing, image analysis is disabled and the app will prompt to open the **Select LLM** dialog to download them. The download URLs can be overridden with `LLAVA_MODEL_URL` and `LLAVA_MMPROJ_URL` (see [Environment variables](#environment-variables)).
### Main window options
Image analysis adds six related checkboxes to the main window:
- **Analyze picture files by content (can be slow)**: Runs the visual LLM on supported picture files and reports progress in the analysis dialog.
- **Process picture files only (ignore any other files)**: Restricts the run to supported picture files and disables the categorization controls while active.
- **Add image creation date (if available) to category name**: Appends `YYYY-MM-DD` from image metadata to the category label when available. Disabled when rename-only is enabled.
- **Add photo date and place to filename (if available)**: Adds metadata-based date/place prefixes to suggested image filenames when available.
- **Offer to rename picture files**: Shows a **Suggested filename** column in the Review dialog with the visual LLM proposal. You can edit it before confirming.
- **Do not categorize picture files (only rename)**: Skips text categorization for images and keeps them in place while applying (optional) renames.
The separate top-level checkbox **Add audio/video metadata to file name (if available)** controls metadata-based rename suggestions for supported audio/video files. See [Audio/video metadata filename suggestions](#audiovideo-metadata-filename-suggestions).
---
## Document analysis (Text LLM)
Document analysis uses the same selected LLM (local or remote) to extract text from supported document files, summarize content, and optionally suggest a better filename. No extra model downloads are required.
### Supported document formats
- Plain text: `.txt`, `.md`, `.rtf`, `.csv`, `.tsv`, `.json`, `.xml`, `.yml`/`.yaml`, `.ini`/`.cfg`/`.conf`, `.log`, `.html`/`.htm`, `.tex`, `.rst`
- PDF: `.pdf` (embedded PDFium by default; CLI fallback via `pdftotext` is available only if you explicitly configure `-DAI_FILE_SORTER_REQUIRE_EMBEDDED_PDF_BACKEND=OFF`)
- Office/OpenOffice: `.docx`, `.xlsx`, `.pptx`, `.odt`, `.ods`, `.odp` (embedded libzip+pugixml in bundled builds; CLI fallback uses `unzip` if you build without vendored libs)
- Legacy binary formats like `.doc`, `.xls`, `.ppt` are not currently supported.
Source builds: embedded extractors are used by default. If the vendored PDFium artifacts are missing for your target platform, CMake now fails loudly instead of silently disabling PDF content extraction. You can opt back into the old CLI fallback with `-DAI_FILE_SORTER_REQUIRE_EMBEDDED_PDF_BACKEND=OFF`.
### Main window options (documents)
- **Analyze document files by content**: Extracts document text and feeds it into the LLM for summary + rename suggestion.
- **Process document files only (ignore any other files)**: Restricts the run to supported document files and disables the categorization controls while active.
- **Offer to rename document files**: Shows a **Suggested filename** column in the Review dialog with the LLM proposal. You can edit it before confirming.
- **Do not categorize document files (only rename)**: Skips text categorization for documents and keeps them in place while applying (optional) renames.
- **Add document creation date (if available) to category name**: Appends `YYYY-MM` from metadata when available. Disabled when rename-only is enabled.
---
## Audio/video metadata filename suggestions
Let AI File Sorter turn embedded media tags into clean, consistent filenames for your music and video library. When enabled, the app reads supported metadata fields and builds a polished suggested name in the format `year_artist_album_title.ext`. As with all rename suggestions, nothing is changed until you review and confirm it.
### Supported audio/video formats
- Audio extensions: `.aac`, `.aif`, `.aiff`, `.alac`, `.ape`, `.flac`, `.m4a`, `.mp3`, `.ogg`, `.oga`, `.opus`, `.wav`, `.wma`
- Video extensions: `.3gp`, `.avi`, `.flv`, `.m4v`, `.mkv`, `.mov`, `.mp4`, `.mpeg`, `.mpg`, `.mts`, `.m2ts`, `.ts`, `.webm`, `.wmv`
- Built-in tag readers currently cover MP3 (`ID3v1`/`ID3v2`), FLAC (Vorbis comments), OGG/OGA/Opus (Vorbis comments), and MP4-family containers such as `.m4a`, `.mp4`, `.m4v`, `.mov`, and `.3gp` (MP4/MOV metadata atoms).
- When compiled with package-managed `MediaInfoLib`, the same rename flow can also use metadata exposed by MediaInfo for additional supported containers when available.
---
## System compatibility check
The **System compatibility check** runs a quick benchmark that estimates how well your system can handle:
- **Categorization** with the selected local LLMs
- **Document analysis** by content
- **Image analysis** (visual LLM)
You can launch it from the menu (**File → System compatibility check…**). It only runs if at least one local or visual LLM is downloaded, and it won’t auto-rerun if it's already been run.
What it does:
- Detects available CPU threads and GPU backends (e.g., Vulkan/CUDA)
- Times a small categorization and document-analysis workload per default model
- Times a single image-analysis pass if visual LLM files are present
- Reports speed tiers (optimal / acceptable / a bit long) and suggests a recommended local LLM
Tip: quit CPU/GPU‑intensive apps before running the check for more accurate results.
---
## Requirements
- **Operating System**: Linux, macOS, or Windows. Linux/macOS source builds use the Makefile flow below; Windows source builds use the native Qt/MSVC + CMake flow in the Windows section.
- **Compiler**: A C++20-capable compiler (`g++` or `clang++` on Linux/macOS, MSVC 2022 on Windows).
- **Qt 6**: Core, Gui, Widgets modules and the Qt resource compiler (`qt6-base-dev` / `qt6-tools` on Linux, `brew install qt` on macOS, or a Qt 6 MSVC kit / `qtbase` via vcpkg on Windows).
- **Libraries**: `curl`, `sqlite3`, `fmt`, `spdlog`, `libmediainfo` (required for full source builds), and the prebuilt `llama` libraries shipped under `app/lib/precompiled` on Linux/Windows or `app/lib/precompiled-*` for macOS variant builds. On Windows, these non-Qt libraries are supplied through the `app/vcpkg.json` manifest.
- **MediaInfo policy**: MediaInfo must be installed through a package manager (`apt`/`dnf`/`pacman`/`brew`/`vcpkg`). The build rejects vendored MediaInfo submodules and checked-in binaries.
- **Document analysis libraries** (vendored): PDFium, libzip, and pugixml. PDFium is required by default so packaged/source builds keep PDF extraction embedded on Windows, macOS, and Linux; set `-DAI_FILE_SORTER_REQUIRE_EMBEDDED_PDF_BACKEND=OFF` only if you intentionally want the `pdftotext` fallback.
- **Optional GPU backends**: A Vulkan 1.2+ runtime (preferred) or CUDA 12.x for NVIDIA cards. `StartAiFileSorter.exe`/`run_aifilesorter.sh` auto-detect the best available backend and fall back to CPU/OpenBLAS automatically, so CUDA is never required to run the app.
- **Git** (optional): For cloning this repository. Archives can also be downloaded.
- **OpenAI or Gemini API key** (optional): Required only when using the remote ChatGPT or Gemini workflow.
---
## Installation
File categorization with local LLMs is completely free of charge. If you prefer to use a remote workflow (ChatGPT or Gemini) you will need your own API key with a small balance or within the free tier (see [Using your OpenAI API key](#using-your-openai-api-key) or [Using your Gemini API key](#using-your-gemini-api-key)).
### Linux
#### Prebuilt Debian/Ubuntu package
1. **Install runtime prerequisites** (Qt6, networking, database, math libraries):
- Ubuntu 24.04 / Debian 12:
```bash
sudo apt update && sudo apt install -y \
libqt6widgets6 libcurl4 libjsoncpp25 libfmt9 libopenblas0-pthread \
libvulkan1 mesa-vulkan-drivers patchelf
```
- Debian 13 (trixie):
```bash
sudo apt update && sudo apt install -y \
libqt6widgets6 libcurl4t64 libjsoncpp26 libfmt10 libopenblas0-pthread \
libvulkan1 mesa-vulkan-drivers patchelf
```
If you build the Vulkan backend from source, install `glslc` (Debian/Ubuntu package: `glslc`; on some distros: `shaderc` or `shaderc-tools`).
On Debian 13, use `libjsoncpp26`, `libfmt10`, and `libcurl4t64` (APT may auto-select `libcurl4t64` if `libcurl4` is not available).
Ensure that the Qt platform plugins are installed (on Ubuntu 22.04 this is provided by `qt6-wayland`).
GPU acceleration additionally requires either a working Vulkan 1.2+ stack (Mesa, AMD/Intel/NVIDIA drivers) or, for NVIDIA users, the matching CUDA runtime (`nvidia-cuda-toolkit` or vendor packages). The launcher automatically prefers Vulkan when both are present and falls back to CPU if neither is available.
2. **Install the package**
```bash
sudo apt install ./aifilesorter_*.deb
```
Using `apt install` (rather than `dpkg -i`) ensures any missing dependencies listed above are installed automatically.
#### Build from source
1. **Install dependencies**
- Debian / Ubuntu:
```bash
sudo apt update && sudo apt install -y \
build-essential cmake git qt6-base-dev qt6-base-dev-tools qt6-l10n-tools qt6-tools-dev-tools \
libcurl4-openssl-dev libjsoncpp-dev libsqlite3-dev libssl-dev libfmt-dev libspdlog-dev libmediainfo-dev \
zlib1g-dev
```
- Fedora / RHEL:
```bash
export PATH="/usr/lib64/qt6/libexec:$PATH"
sudo dnf install -y gcc-c++ cmake git qt6-qtbase-devel qt6-qttools-devel \
libcurl-devel jsoncpp-devel sqlite-devel openssl-devel fmt-devel spdlog-devel mediainfo-devel
```
- Arch / Manjaro:
```bash
sudo pacman -S --needed base-devel git cmake qt6-base qt6-tools curl jsoncpp sqlite openssl fmt spdlog mediainfo
```
Optional GPU acceleration also requires either the distro Vulkan 1.2+ driver/runtime (Mesa, AMD, Intel, NVIDIA) or CUDA packages for NVIDIA cards. Install whichever stack you plan to use; the app will fall back to CPU automatically if none are detected.
MediaInfo is enforced as package-managed only; vendored `MediaInfoLib` folders or repo-local binaries are rejected by the build.
2. **Clone the repository**
```bash
git clone https://github.com/hyperfield/ai-file-sorter.git
cd ai-file-sorter
git submodule update --init --recursive
```
> **Submodule tip:** If you previously downloaded `llama.cpp` or Catch2 manually, remove or rename `app/include/external/llama.cpp` and `external/Catch2` before running the `git submodule` command. Git needs those directories to be empty so it can populate them with the tracked submodules.
3. **Build vendored libzip** (generates `zipconf.h` and `libzip.a`)
```bash
cmake -S external/libzip -B external/libzip/build \
-DBUILD_SHARED_LIBS=OFF \
-DBUILD_DOC=OFF \
-DENABLE_BZIP2=OFF \
-DENABLE_LZMA=OFF \
-DENABLE_ZSTD=OFF \
-DENABLE_OPENSSL=OFF \
-DENABLE_GNUTLS=OFF \
-DENABLE_MBEDTLS=OFF \
-DENABLE_COMMONCRYPTO=OFF \
-DENABLE_WINDOWS_CRYPTO=OFF
cmake --build external/libzip/build
```
On Ubuntu/Debian you will also need the Zlib development headers (`zlib1g-dev`) or
the libzip configure step will fail.
If you prefer system headers instead, install `libzip-dev` and ensure `zipconf.h` is on your include path.
4. **Build the llama runtime variants** (run once per backend you plan to ship/test)
```bash
# CPU / OpenBLAS
./app/scripts/build_llama_linux.sh cuda=off vulkan=off
# CUDA (optional; requires NVIDIA driver + CUDA toolkit)
./app/scripts/build_llama_linux.sh cuda=on vulkan=off
# Vulkan (optional; requires a working Vulkan 1.2+ stack and glslc, e.g. mesa-vulkan-drivers + vulkan-tools + glslc)
./app/scripts/build_llama_linux.sh cuda=off vulkan=on
```
Each invocation stages the corresponding `llama`/`ggml` libraries under `app/lib/precompiled/` and the runtime DLL/SO copies under `app/lib/ggml/w`. The script refuses to enable CUDA and Vulkan simultaneously, so run it separately for each backend. Shipping both directories lets the launcher pick Vulkan when available, then CUDA, and otherwise stay on CPU—no CUDA-only dependency remains.
5. **Compile the application**
```bash
cd app
make -j4
```
The binary is produced at `app/bin/aifilesorter`.
The Makefile requires `pkg-config` + package-managed `libmediainfo`; it intentionally rejects vendored MediaInfo copies.
6. **Install system-wide (optional)**
```bash
sudo make install
```
7. **Build a Debian package (optional)**
```bash
./app/scripts/package_deb.sh
```
The packaging script always bundles the CPU runtime and auto-includes any staged GPU
variants already present under `app/lib/precompiled` (for example `vulkan` after
`./app/scripts/build_llama_linux.sh cuda=off vulkan=on`). Use
`./app/scripts/package_deb.sh --cpu-only` for a smaller CPU-only package, or
`--include-vulkan` / `--include-cuda` if you want the script to fail when a specific
staged variant is missing.
### macOS
1. **Install Xcode command-line tools** (`xcode-select --install`).
2. **Install Homebrew** (if required).
3. **Install dependencies**
```bash
brew install qt curl jsoncpp sqlite openssl fmt spdlog mediainfo cmake git pkgconfig libffi
```
Add Qt to your environment if it is not already present:
```bash
export PATH="$(brew --prefix)/opt/qt/bin:$PATH"
export PKG_CONFIG_PATH="$(brew --prefix)/lib/pkgconfig:$(brew --prefix)/share/pkgconfig:$PKG_CONFIG_PATH"
```
4. **Clone the repository and submodules** (same commands as Linux).
> The macOS build pins `MACOSX_DEPLOYMENT_TARGET=11.0` so the Mach-O `LC_BUILD_VERSION` covers Apple Silicon and newer releases (including Sequoia). Raise or lower it (e.g., `export MACOSX_DEPLOYMENT_TARGET=15.0`) if you need a different floor.
5. **Build vendored libzip** (generates `zipconf.h` and `libzip.a`)
```bash
cmake -S external/libzip -B external/libzip/build \
-DBUILD_SHARED_LIBS=OFF \
-DBUILD_DOC=OFF \
-DENABLE_BZIP2=OFF \
-DENABLE_LZMA=OFF \
-DENABLE_ZSTD=OFF \
-DENABLE_OPENSSL=OFF \
-DENABLE_GNUTLS=OFF \
-DENABLE_MBEDTLS=OFF \
-DENABLE_COMMONCRYPTO=OFF \
-DENABLE_WINDOWS_CRYPTO=OFF
cmake --build external/libzip/build
```
6. **Build the llama runtime (Metal-enabled on Apple Silicon)**
```bash
./app/scripts/build_llama_macos.sh
```
The macOS app and `.app` bundles use the runtime staged under `app/lib/precompiled*`; they do not need Homebrew `ggml` or `llama.cpp` libraries.
If you have older `ggml` / `llama.cpp` copies installed in generic library locations, prefer unlinking or removing them instead of relying on them implicitly.
7. **Compile the application**
```bash
cd app
make -j8 # use -jN to control parallelism
sudo make install # optional
```
The default build places the binary at `app/bin/aifilesorter`.
**Variant targets:**
```bash
make -j8 MACOS_LLAMA_M1 # outputs app/bin/m1/aifilesorter
make -j8 MACOS_LLAMA_M2 # outputs app/bin/m2/aifilesorter
make -j8 MACOS_LLAMA_INTEL # outputs app/bin/intel/aifilesorter
```
These targets rebuild the llama.cpp runtime before compiling the app.
When cross-compiling Intel on Apple Silicon, use x86_64 Homebrew (under `/usr/local`) or set `BREW_PREFIX=/usr/local` so Qt/pkg-config resolve correctly.
`sudo make install` places the macOS runtime libraries under `/usr/local/lib/aifilesorter` to avoid collisions with unrelated system or Homebrew ggml libraries.
Each variant uses distinct build directories to avoid cross-arch collisions:
- llama.cpp libs: `app/lib/precompiled-m1`, `app/lib/precompiled-m2`, `app/lib/precompiled-intel`
- object files: `app/obj/arm64` or `app/obj/x86_64`
### Windows
Build now targets native MSVC + Qt6 without MSYS2. Two options are supported; the vcpkg route is simplest.
Option A - CMake + vcpkg (recommended)
1. Install prerequisites:
- Visual Studio 2022 with Desktop C++ workload
- CMake 3.21+ (Visual Studio ships a recent version)
- vcpkg: (clone and bootstrap)
- package-managed `libmediainfo` via vcpkg manifest (no vendored MediaInfo submodule/binaries)
- **MSYS2 MinGW64 + OpenBLAS**: install MSYS2 from , open an *MSYS2 MINGW64* shell, and run `pacman -S --needed mingw-w64-x86_64-openblas`. The `build_llama_windows.ps1` script uses this OpenBLAS copy for CPU-only builds (the vcpkg variant is not suitable), defaulting to `C:\msys64\mingw64` unless you pass `openblasroot=` or set `OPENBLAS_ROOT`.
2. Clone repo and submodules:
```powershell
git clone https://github.com/hyperfield/ai-file-sorter.git
cd ai-file-sorter
git submodule update --init --recursive
```
3. **Build vendored libzip** (generates `zipconf.h` and `libzip.lib`)
Run from the same x64 Native Tools / VS Developer PowerShell you will use to build the app:
```powershell
cmake -S external\libzip -B external\libzip\build -A x64 `
-DBUILD_SHARED_LIBS=OFF `
-DBUILD_DOC=OFF `
-DENABLE_BZIP2=OFF `
-DENABLE_LZMA=OFF `
-DENABLE_ZSTD=OFF `
-DENABLE_OPENSSL=OFF `
-DENABLE_GNUTLS=OFF `
-DENABLE_MBEDTLS=OFF `
-DENABLE_COMMONCRYPTO=OFF `
-DENABLE_WINDOWS_CRYPTO=OFF
cmake --build external\libzip\build --config Release
```
4. Determine your vcpkg root. It is the folder that contains `vcpkg.exe` (for example `C:\dev\vcpkg`).
- If `vcpkg` is on your `PATH`, run this command to print the location:
```powershell
Split-Path -Parent (Get-Command vcpkg).Source
```
- Otherwise use the directory where you cloned vcpkg.
MediaInfo note: you do **not** manually add `MediaInfoLib` include/lib paths on Windows. The project already declares `libmediainfo` in `app/vcpkg.json`, and `app\build_windows.ps1` configures CMake with the vcpkg toolchain + manifest so `find_package(MediaInfoLib ...)` resolves it automatically. If you want to preinstall or verify it explicitly, run `vcpkg install libmediainfo:x64-windows`.
5. Build the bundled `llama.cpp` runtime variants (run from the same **x64 Native Tools** / **VS 2022 Developer PowerShell** shell). Invoke the script once per backend you need. Make sure the MSYS2 OpenBLAS install from step 1 is present before running the CPU-only variant (or pass `openblasroot=` explicitly):
```powershell
# CPU / OpenBLAS only
app\scripts\build_llama_windows.ps1 cuda=off vulkan=off vcpkgroot=C:\dev\vcpkg
# CUDA (requires matching NVIDIA toolkit/driver)
app\scripts\build_llama_windows.ps1 cuda=on vulkan=off vcpkgroot=C:\dev\vcpkg
# Vulkan (requires LunarG Vulkan SDK or vendor Vulkan 1.2+ runtime)
app\scripts\build_llama_windows.ps1 cuda=off vulkan=on vcpkgroot=C:\dev\vcpkg
```
Each run emits the appropriate `llama.dll` / `ggml*.dll` pair under `app\lib\precompiled\` and copies the runtime DLLs into `app\lib\ggml\w`. For Vulkan builds, install the latest LunarG Vulkan SDK (or the vendor's runtime), ensure `vulkaninfo` succeeds in the same shell, and then run the script. Supplying both Vulkan and (optionally) CUDA artifacts lets `StartAiFileSorter.exe` detect the best backend at launch—Vulkan is preferred, CUDA is used when Vulkan is missing, and CPU remains the fallback, so CUDA is not required.
6. Build the Qt6 application using the helper script (still in the VS shell). The helper stages runtime DLLs via `windeployqt`, shares one dependency install tree across variants, and by default produces three Windows builds in one run:
```powershell
# One-time per shell if script execution is blocked:
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
app\build_windows.ps1 -Configuration Release -VcpkgRoot C:\dev\vcpkg
```
- Replace `C:\dev\vcpkg` with the path where you cloned vcpkg; it must contain `scripts\buildsystems\vcpkg.cmake`.
- The helper produces these output directories by default:
- Standard installer build with Windows auto-update enabled: `app\build-windows\Release`
- Microsoft Store build with update checks disabled: `app\build-windows-store\Release`
- Standalone Windows build with notification-only/manual updates: `app\build-windows-standalone\Release`
- Use `-Variants Standard`, `-Variants MsStore`, or `-Variants Standalone` to build only a subset.
- `aifilesorter.exe` is the primary Windows GUI entry point. `StartAiFileSorter.exe` is still built beside it as the legacy bootstrapper and carries the same updater mode.
- `-VcpkgRoot` is optional if `VCPKG_ROOT`/`VPKG_ROOT` is set or `vcpkg`/`vpkg` is on `PATH`.
- Each variant directory receives its own executable and staged Qt/third-party DLLs. Pass `-SkipDeploy` if you only want the binaries without bundling runtime DLLs.
- Pass `-Parallel ` to override the default “all cores” parallel build behaviour (for example, `-Parallel 8`). By default the script invokes `cmake --build ... --parallel ` and `ctest -j ` to keep both MSBuild and Ninja fully utilized.
Option B - CMake + Qt online installer
1. Install prerequisites:
- Visual Studio 2022 with Desktop C++ workload
- Qt 6.x MSVC kit via Qt Online Installer (e.g., Qt 6.6+ with MSVC 2019/2022)
- CMake 3.21+
- vcpkg (for non-Qt libs): curl, jsoncpp, sqlite3, openssl, fmt, spdlog, gettext, libmediainfo
2. **Build vendored libzip** (generates `zipconf.h` and `libzip.lib`)
Run from the same x64 Native Tools / VS Developer PowerShell you will use to build the app:
```powershell
cmake -S external\libzip -B external\libzip\build -A x64 `
-DBUILD_SHARED_LIBS=OFF `
-DBUILD_DOC=OFF `
-DENABLE_BZIP2=OFF `
-DENABLE_LZMA=OFF `
-DENABLE_ZSTD=OFF `
-DENABLE_OPENSSL=OFF `
-DENABLE_GNUTLS=OFF `
-DENABLE_MBEDTLS=OFF `
-DENABLE_COMMONCRYPTO=OFF `
-DENABLE_WINDOWS_CRYPTO=OFF
cmake --build external\libzip\build --config Release
```
3. Build the bundled `llama.cpp` runtime (same VS shell). Any missing OpenBLAS/cURL packages are installed automatically via vcpkg:
```powershell
pwsh .\app\scripts\build_llama_windows.ps1 [cuda=on|off] [vulkan=on|off] [vcpkgroot=C:\dev\vcpkg]
```
This is required before configuring the GUI because the build links against the produced `llama` static libraries/DLLs.
4. Configure CMake from the repo root so CMake sees both the Qt install and the app's vcpkg manifest (adapt `CMAKE_PREFIX_PATH` to your Qt install):
```powershell
$env:VCPKG_ROOT = "C:\path\to\vcpkg" # e.g. C:\dev\vcpkg
$qt = "C:\Qt\6.6.3\msvc2019_64" # example
cmake -S app -B build -G "Ninja" `
-DCMAKE_PREFIX_PATH=$qt `
-DCMAKE_TOOLCHAIN_FILE=$env:VCPKG_ROOT\scripts\buildsystems\vcpkg.cmake `
-DVCPKG_MANIFEST_DIR=app `
-DAI_FILE_SORTER_REQUIRE_MEDIAINFOLIB=ON `
-DVCPKG_TARGET_TRIPLET=x64-windows
cmake --build build --config Release
```
This configure step enables vcpkg manifest mode, so `libmediainfo` is installed/resolved from `app\vcpkg.json` automatically. No manual linker or include-path edits are needed for MediaInfo on Windows.
Notes
- To rebuild from scratch, run `.\app\build_windows.ps1 -Clean`. The script removes the selected variant build directories and the shared `app\build-windows-vcpkg_installed` dependency tree before configuring.
- Runtime DLLs are copied automatically via `windeployqt` after each successful build; skip this step with `-SkipDeploy` if you manage deployment yourself.
- If Visual Studio sets `VCPKG_ROOT` to its bundled copy under `Program Files`, clone vcpkg to a writable directory (for example `C:\dev\vcpkg`) and pass `vcpkgroot=` when running `build_llama_windows.ps1`.
- If you plan to ship CUDA or Vulkan acceleration, run the `build_llama_*` helper for each backend you intend to include before configuring CMake so the libraries exist. The runtime can carry both and auto-select at launch, so CUDA remains optional.
- `-BuildTests` and `-RunTests` currently build and execute tests only in the `Standard` variant, which is the primary Windows development/CI configuration.
### Running tests
Catch2-based unit tests are optional. Enable them via CMake:
```bash
cmake -S app -B build-tests -DAI_FILE_SORTER_BUILD_TESTS=ON -DAI_FILE_SORTER_REQUIRE_MEDIAINFOLIB=ON
cmake --build build-tests --target ai_file_sorter_tests --parallel $(nproc)
ctest --test-dir build-tests --output-on-failure -j $(nproc)
```
On macOS, replace `$(nproc)` with `$(sysctl -n hw.ncpu)`.
On Windows (PowerShell), use:
```powershell
cmake -S app -B build-tests -DAI_FILE_SORTER_BUILD_TESTS=ON -DAI_FILE_SORTER_REQUIRE_MEDIAINFOLIB=ON
cmake --build build-tests --target ai_file_sorter_tests --parallel $env:NUMBER_OF_PROCESSORS
ctest --test-dir build-tests --output-on-failure -j $env:NUMBER_OF_PROCESSORS
```
Notes
- List individual Catch2 cases: `./build-tests/ai_file_sorter_tests --list-tests`
- Print each case name (including successes): `./build-tests/ai_file_sorter_tests --verbosity high --success`
On Windows you can pass `-BuildTests` (and `-RunTests` to execute `ctest`) to `app\build_windows.ps1`:
```powershell
app\build_windows.ps1 -Configuration Release -Variants Standard -BuildTests -RunTests
```
The current suite (under `tests/unit`) focuses on core utilities; expand it as new functionality gains coverage.
### Selecting a backend at runtime
Both the Linux launcher (`app/bin/run_aifilesorter.sh` / `aifilesorter-bin`) and the Windows starter accept the following optional flags:
- `--cuda={on|off}` – force-enable or disable the CUDA backend.
- `--vulkan={on|off}` – force-enable or disable the Vulkan backend.
When no flags are provided the app auto-detects available runtimes in priority order (Vulkan → CUDA → CPU). Use the flags to skip a backend (`--cuda=off` forces Vulkan/CPU even if CUDA is installed, `--vulkan=off` tests CUDA explicitly) or to validate a newly installed stack (`--vulkan=on`). Passing `on` to both flags is rejected, and if neither GPU backend is detected the app automatically stays on CPU.
#### Vulkan and VRAM notes
- Vulkan is preferred when available; CUDA is used only if Vulkan is missing or explicitly requested.
- The app auto-estimates `n_gpu_layers` based on available VRAM. Integrated GPUs are capped to 4 GiB for safety, which can limit offloading.
- If VRAM is tight, the app may fall back to CPU or reduce offload. As a rule of thumb, 8 GB+ VRAM provides a smoother experience for Vulkan offload and image analysis; 4 GB often results in partial offload or CPU fallback.
- Override auto-estimation with `AI_FILE_SORTER_N_GPU_LAYERS` (`-1` auto, `0` force CPU) or `AI_FILE_SORTER_GPU_BACKEND=cpu`.
- For image analysis, `AI_FILE_SORTER_VISUAL_USE_GPU=0` forces the visual encoder to run on CPU to avoid VRAM allocation errors.
### Environment variables
Runtime and GPU:
- `AI_FILE_SORTER_GPU_BACKEND` - select GPU backend: `auto` (default), `vulkan`, `cuda`, or `cpu`.
- `AI_FILE_SORTER_N_GPU_LAYERS` - override `n_gpu_layers` for llama.cpp; `-1` = auto, `0` = force CPU.
- `AI_FILE_SORTER_CTX_TOKENS` - override local LLM context length (default 2048; clamped 512-8192).
- `AI_FILE_SORTER_GGML_DIR` - directory to load ggml backend shared libraries from. On macOS this is only auto-discovered from bundled or sibling app runtime directories; use this variable explicitly if you want a custom ggml runtime.
Visual LLM:
- `LLAVA_MODEL_URL` - download URL for the visual LLM GGUF model (required to enable image analysis).
- `LLAVA_MMPROJ_URL` - download URL for the visual LLM mmproj GGUF file (required to enable image analysis).
- `AI_FILE_SORTER_VISUAL_USE_GPU` - force visual encoder GPU usage (`1`) or CPU (`0`). Defaults to auto; Vulkan may fall back to CPU if VRAM is low.
Timeouts and logging:
- `AI_FILE_SORTER_LOCAL_LLM_TIMEOUT` - seconds to wait for local LLM responses (default 60).
- `AI_FILE_SORTER_REMOTE_LLM_TIMEOUT` - seconds to wait for OpenAI/Gemini responses (default 10).
- `AI_FILE_SORTER_CUSTOM_LLM_TIMEOUT` - seconds to wait for custom OpenAI-compatible API responses (default 60).
- `AI_FILE_SORTER_LLAMA_LOGS` - enable verbose llama.cpp logs (`1`/`true`); also honors `LLAMA_CPP_DEBUG_LOGS`.
Storage and updates:
- `AI_FILE_SORTER_CONFIG_DIR` - override the base config directory (where `config.ini` lives).
- `CATEGORIZATION_CACHE_FILE` - override the SQLite cache filename inside the config dir.
- `UPDATE_SPEC_FILE_URL` - override the update feed spec URL (dev/testing). The updater now reads per-platform streams from `update.windows`, `update.macos`, and `update.linux`, with legacy single-stream feeds still accepted.
- `AI_FILE_SORTER_UPDATER_TEST_MODE` - enable Windows updater live-test mode (`1`/`true`). When enabled, the app skips the update feed fetch and synthesizes a newer version from the values below.
- `AI_FILE_SORTER_UPDATER_TEST_URL` - direct URL for the Windows updater live-test package. This can point to an `.exe`, `.msi`, or a `.zip` containing exactly one `.exe` or `.msi`.
- `AI_FILE_SORTER_UPDATER_TEST_SHA256` - SHA-256 checksum for the downloaded live-test package. If the URL points to a ZIP, this checksum must be for the ZIP archive itself.
- `AI_FILE_SORTER_UPDATER_TEST_VERSION` - optional synthetic version shown by live-test mode. Defaults to the current app version with an extra trailing segment, for example `1.7.2.1`.
- `AI_FILE_SORTER_UPDATER_TEST_MIN_VERSION` - optional synthetic minimum version for live-test mode. Defaults to `0.0.0` so the test behaves like an optional update.
Example update feed:
```json
{
"update": {
"current_version": "1.7.1",
"min_version": "1.6.0",
"download_url": "https://filesorter.app/download",
"windows": {
"current_version": "1.7.1",
"min_version": "1.6.0",
"download_url": "https://filesorter.app/download",
"installer_url": "https://filesorter.app/downloads/AIFileSorterSetup-1.7.1.exe",
"installer_sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
},
"macos": {
"current_version": "1.7.1",
"min_version": "1.6.0",
"download_url": "https://filesorter.app/download"
},
"linux": {
"current_version": "1.7.1",
"min_version": "1.6.0",
"download_url": "https://filesorter.app/download"
}
}
}
```
Compatibility note:
- Older app versions only read the flat top-level fields under `update`, so keep `current_version`, `min_version`, and `download_url` there as a legacy compatibility stream if you still need to support them.
- Newer app versions prefer the platform-specific streams and will use `update.windows`, `update.macos`, or `update.linux` when present.
- The legacy compatibility stream can only represent one generic stream, not separate per-platform versions or installers.
Windows-only direct installer updates:
- `installer_url` - direct URL to the Windows installer package.
- `installer_sha256` - SHA-256 checksum used to verify the downloaded installer before launch.
- `installer_url` can now also point to a ZIP archive, as long as the archive contains exactly one installer payload (`.exe` or `.msi`).
- When both fields are present on Windows, the app can download the installer, verify it, and then prompt: `Quit the app and launch the installer to update`.
Windows updater live-test mode:
- `aifilesorter.exe` accepts the following flags directly on Windows:
`--updater-live-test`
`--updater-live-test-url=`
`--updater-live-test-sha256=`
`--updater-live-test-version=`
`--updater-live-test-min-version=`
- `StartAiFileSorter.exe` accepts and forwards the same flag family if you still use the bootstrapper path.
- Live-test mode is Windows-only and intentionally bypasses the normal update JSON feed.
- If the ZIP contains more than one `.exe` or `.msi`, the updater stops instead of guessing which installer to launch.
- If `--updater-live-test` is present and the URL / SHA flags are omitted, `aifilesorter.exe` also looks for a `live-test.ini` file next to the executable and fills in the missing values from there.
- Command-line flags still win over `live-test.ini`, so you can keep a default file and override just one field when needed.
Example `live-test.ini`:
```ini
[LiveTest]
download_url = https://files.example.com/AIFileSorterSetup-1.7.3.zip
sha256 = 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
current_version = 1.7.3
min_version = 0.0.0
```
Example PowerShell launch:
```powershell
.\aifilesorter.exe `
--development `
--updater-live-test
```
---
## Categorization cache database
AI File Sorter stores categorization results in a local SQLite database next to `config.ini` (the base directory can be overridden via `AI_FILE_SORTER_CONFIG_DIR`). This cache allows the app to skip already-processed files and preserve rename suggestions between runs.
What is stored:
- Directory path, file name, and file type (used as a unique key).
- Category/subcategory, taxonomy id, categorization style, and timestamp.
- Suggested filename (for picture and document rename suggestions).
- Rename-only flag (used when picture/document rename-only modes are enabled).
- Rename-applied flag (marks when a rename was executed so it is not offered again).
If you rename or move a file from the Review dialog, the cache entry is updated to the new name. Already-renamed picture files are skipped for visual analysis and rename suggestions on later runs. In the Review dialog, those already-renamed rows are hidden when rename-only is enabled, but they stay visible when categorization is enabled so you can still move them into category folders. To reset a folder's cache, accept the recategorization prompt or delete the cache file (or point `CATEGORIZATION_CACHE_FILE` to a new filename).
---
## Uninstallation
- **Debian/Ubuntu package installs**: `sudo apt remove aifilesorter`
- **Linux source installs**: `cd app && sudo make uninstall`
- **macOS source installs**: `cd app && sudo make uninstall`
For source installs, `make uninstall` removes the executable and the staged precompiled libraries. You can also delete cached local LLM models in `~/.local/share/aifilesorter/llms` (Linux) or `~/Library/Application Support/aifilesorter/llms` (macOS) if you no longer need them.
---
## Using your OpenAI API key
Want to use ChatGPT instead of the bundled local models? Bring your own OpenAI API key:
1. Open **Settings -> Select LLM** in the app.
2. Choose **ChatGPT (OpenAI API key)**, paste your key, and enter the ChatGPT model you want to use (for example `gpt-4o-mini`, `gpt-4.1`, or `o3-mini`).
3. Click **OK**. The key is stored locally in your AI File Sorter config (`config.ini` in the app data folder) and reused for future runs. Clear the field to remove it.
4. An internet connection is only required while this option is selected.
> The app no longer embeds a bundled key; you always provide your own OpenAI key.
---
## Using your Gemini API key
Prefer Google's models? Use your own Gemini API key:
1. Visit **https://aistudio.google.com** and sign in with your Google account.
2. In the left navigation, open **API keys** (or **Get API key**) and click **Create API key**. Choose *Create API key in new project* (or select an existing project) and copy the generated key.
3. In the app, open **Settings -> Select LLM**, choose **Gemini (Google AI Studio API key)**, paste your key, and enter the Gemini model you want (for example `gemini-2.5-flash-lite`, `gemini-2.5-flash`, or `gemini-2.5-pro`).
4. Click **OK**. The key is stored locally in your AI File Sorter config and reused for future runs. Clear the field to remove it.
> AI Studio keys can be used on the free tier until you hit Google’s limits; higher quotas or enterprise use require billing via Google Cloud.
> The app calls the Gemini `v1` `generateContent` endpoint; use model IDs from `https://generativelanguage.googleapis.com/v1/models?key=YOUR_KEY`. You can enter them with or without the leading `models/` prefix.
---
## Testing
- From the repo root, clean any old cache and run the CTest wrapper:
```bash
cd app
rm -rf ../build-tests # clear a cache from another checkout
./scripts/rebuild_and_test.sh
```
- The script configures to `../build-tests`, builds, then runs `ctest`.
- If you have multiple copies of the repo (e.g., `ai-file-sorter` and `ai-file-sorter-mac-dist`), each needs its own `build-tests` folder; reusing one from a different path will make CMake complain about mismatched source/build directories.
---
## Diagnostics
If you need to report a bug or collect troubleshooting data, use the bundled diagnostics scripts:
- **macOS:** `./app/scripts/collect_macos_diagnostics.sh`
- **Linux:** `./app/scripts/collect_linux_diagnostics.sh`
- **Windows (PowerShell):** `.\app\scripts\collect_windows_diagnostics.ps1`
Each script collects relevant logs, redacts common sensitive paths, and packages the result into a zip archive for sharing. See [app/scripts/README.md](app/scripts/README.md) for options such as time filtering and opening the output folder automatically.
---
## How to Use
1. Launch the application (see the last step in [Installation](#installation) according your OS).
2. Select a directory to analyze.
### Using dry run and undo
- In the results dialog, you can enable **"Dry run (preview only, do not move files)"** to preview planned moves. A preview dialog shows From/To without moving any files.
- After a real sort, the app saves a persistent undo plan. You can revert later via **Edit → "Undo last run"** (best-effort; skips conflicts/changes).
3. Tick off the checkboxes on the main window according to your preferences.
4. Click the **"Analyze"** button. The app will scan each file and/or directory based on your selected options.
5. A review dialog will appear. Verify the assigned categories (and subcategories, if enabled in step 3).
6. Click **"Confirm & Sort!"** to move the files, or **"Continue Later"** to postpone. You can always resume where you left off since categorization results are saved.
---
## Sorting a Remote Directory (e.g., NAS)
Follow the steps in [How to Use](#how-to-use), but modify **step 2** as follows:
- **Windows:** Assign a drive letter (e.g., `Z:` or `X:`) to your network share ([instructions here](https://support.microsoft.com/en-us/windows/map-a-network-drive-in-windows-29ce55d1-34e3-a7e2-4801-131475f9557d)).
- **Linux & macOS:** Mount the network share to a local folder using a command like:
```sh
sudo mount -t cifs //192.168.1.100/shared_folder /mnt/nas -o username=myuser,password=mypass,uid=$(id -u),gid=$(id -g)
```
(Replace 192.168.1.100/shared_folder with your actual network location path and adjust options as needed.)
---
## Contributing
- Fork the repository and submit pull requests.
- Report issues or suggest features on the GitHub issue tracker.
- Follow the existing code style and documentation format.
---
## Credits
- Curl:
- Dotenv:
- git-scm:
- Hugging Face:
- JSONCPP:
- Llama:
- libzip:
- Local File Organizer
- llama.cpp
- MediaInfoLib:
- Mistral AI:
- OpenAI:
- OpenSSL:
- PDFium:
- Poppler (pdftotext):
- pugixml:
- Qt:
- spdlog:
- unzip (Info-ZIP):
## License
This project is licensed under the GNU AFFERO GENERAL PUBLIC LICENSE (GNU AGPL). See the [LICENSE](LICENSE) file for details, or https://www.gnu.org/licenses/agpl-3.0.html.
---
## Donation
Support the development of **AI File Sorter** and its future features. Every contribution counts!
- **[Donate](https://filesorter.app/donate/)**
---
================================================
FILE: TESTS.md
================================================
# Test Suite Guide
This document provides a detailed description of every test case in the project. It is organized by test file and mirrors the intent, setup, procedure, and expected outcomes for each case. All unit tests live under `tests/unit`. Some UI-centric tests are compiled only on non-Windows platforms and use the Qt offscreen platform plugin so they can run without a visible display.
## How to run tests
- Configure tests (once): `cmake -S app -B build-tests -DAI_FILE_SORTER_BUILD_TESTS=ON -DAI_FILE_SORTER_REQUIRE_MEDIAINFOLIB=ON`
- Build and run all tests: `cmake --build build-tests` then `ctest --test-dir build-tests --output-on-failure -j $(nproc)`
- Run a single test case by name: `./build-tests/ai_file_sorter_tests ""`
- MediaInfo is expected from a package manager (`apt`/`dnf`/`pacman`/`brew`/`vcpkg`); vendored MediaInfo directories/binaries are intentionally rejected by the build.
## Unit test catalog
### `tests/unit/test_local_llm_backend.cpp` (skipped when `GGML_USE_METAL` is defined)
#### Test case: detect_preferred_backend reads environment
Purpose: Verify that the backend preference resolver honors the explicit environment override.
Setup: Set `AI_FILE_SORTER_GPU_BACKEND` to `cuda` via an environment guard.
Procedure: Call `detect_preferred_backend()` through the test access layer.
Expected outcome: The detected preference is `Cuda`.
Run: `./build-tests/ai_file_sorter_tests "detect_preferred_backend reads environment"`
#### Test case: CPU backend is honored when forced
Purpose: Ensure the GPU layer count is forced to CPU when the backend is set to CPU.
Setup: Create a temporary GGUF model file and set `AI_FILE_SORTER_GPU_BACKEND=cpu`. Ensure no CUDA disable flag or layer override is set.
Procedure: Call `prepare_model_params_for_testing()` for the temporary model.
Expected outcome: `n_gpu_layers` is `0`.
Run: `./build-tests/ai_file_sorter_tests "CPU backend is honored when forced"`
#### Test case: CUDA backend can be forced off via GGML_DISABLE_CUDA
Purpose: Confirm that the global CUDA disable flag overrides a CUDA backend preference.
Setup: Set `AI_FILE_SORTER_GPU_BACKEND=cuda` and `GGML_DISABLE_CUDA=1`. Inject a probe that reports CUDA available.
Procedure: Call `prepare_model_params_for_testing()`.
Expected outcome: `n_gpu_layers` is `0`, indicating CPU fallback.
Run: `./build-tests/ai_file_sorter_tests "CUDA backend can be forced off via GGML_DISABLE_CUDA"`
#### Test case: CUDA override is applied when backend is available
Purpose: Validate that an explicit layer override is used when CUDA is available.
Setup: Set `AI_FILE_SORTER_GPU_BACKEND=cuda`, set `AI_FILE_SORTER_N_GPU_LAYERS=7`, and inject a CUDA-available probe.
Procedure: Call `prepare_model_params_for_testing()`.
Expected outcome: `n_gpu_layers` equals `7`.
Run: `./build-tests/ai_file_sorter_tests "CUDA override is applied when backend is available"`
#### Test case: CUDA fallback when no GPU is available
Purpose: Ensure CUDA preference falls back when no GPU is detected.
Setup: Set `AI_FILE_SORTER_GPU_BACKEND=cuda`, leave layer override unset, and inject a CUDA-unavailable probe.
Procedure: Call `prepare_model_params_for_testing()`.
Expected outcome: `n_gpu_layers` is `0` or `-1` (CPU or auto fallback).
Run: `./build-tests/ai_file_sorter_tests "CUDA fallback when no GPU is available"`
#### Test case: Vulkan backend honors explicit override
Purpose: Check that Vulkan backend respects a specific GPU layer override.
Setup: Set `AI_FILE_SORTER_GPU_BACKEND=vulkan`, set `AI_FILE_SORTER_N_GPU_LAYERS=12`, and provide a memory probe that returns no data.
Procedure: Call `prepare_model_params_for_testing()`.
Expected outcome: `n_gpu_layers` equals `12`.
Run: `./build-tests/ai_file_sorter_tests "Vulkan backend honors explicit override"`
#### Test case: Vulkan backend derives layer count from memory probe
Purpose: Verify that Vulkan backend derives a sensible layer count from reported GPU memory.
Setup: Use a model with 48 blocks, set `AI_FILE_SORTER_GPU_BACKEND=vulkan`, and inject a probe reporting a 3 GB discrete GPU.
Procedure: Call `prepare_model_params_for_testing()`.
Expected outcome: `n_gpu_layers` is greater than `0` and less than or equal to `48`.
Run: `./build-tests/ai_file_sorter_tests "Vulkan backend derives layer count from memory probe"`
### `tests/unit/test_main_app_image_options.cpp` (non-Windows only)
#### Test case: Image analysis checkboxes enable and enforce rename-only behavior
Purpose: Ensure the image analysis options enable correctly and enforce the rename-only rule.
Setup: Create dummy LLaVA model files, configure settings with image analysis and rename options off, and construct `MainApp` with offscreen Qt.
Procedure: Toggle the "Analyze picture files" checkbox on, then toggle the "Do not categorize picture files" checkbox on and attempt to unset "Offer to rename picture files".
Expected outcome: The option group enables when analysis is checked; enabling rename-only forces offer-rename on; disabling offer-rename clears rename-only.
Run: `./build-tests/ai_file_sorter_tests "Image analysis checkboxes enable and enforce rename-only behavior"`
#### Test case: Image rename-only does not disable categorization unless processing images only
Purpose: Confirm that rename-only for images does not disable file categorization by itself.
Setup: Initialize settings with image analysis off and build `MainApp` with offscreen Qt.
Procedure: Enable image analysis and rename-only, then check whether "Categorize files" remains enabled. Next, enable "Process picture files only".
Expected outcome: Categorization remains enabled with rename-only, but becomes disabled when processing images only.
Run: `./build-tests/ai_file_sorter_tests "Image rename-only does not disable categorization unless processing images only"`
#### Test case: Document rename-only does not disable categorization unless processing documents only
Purpose: Mirror the image-only behavior for documents.
Setup: Initialize settings with document analysis off and build `MainApp` with offscreen Qt.
Procedure: Enable document analysis and rename-only, then check whether "Categorize files" remains enabled. Next, enable "Process document files only".
Expected outcome: Categorization remains enabled with rename-only, but becomes disabled when processing documents only.
Run: `./build-tests/ai_file_sorter_tests "Document rename-only does not disable categorization unless processing documents only"`
#### Test case: Document analysis ignores other files when categorize files is off
Purpose: Verify the entry splitter respects the "categorize files" flag when only document analysis is active.
Setup: Prepare a mixed list of image, document, other file, and a directory entry. Set all flags to analyze documents only and categorize files off.
Procedure: Call `split_entries_for_analysis()` and inspect the output buckets.
Expected outcome: Document entries are analyzed, other non-document files are excluded, and directories are still included in the "other" bucket.
Run: `./build-tests/ai_file_sorter_tests "Document analysis ignores other files when categorize files is off"`
#### Test case: Image analysis toggle disables when dialog closes without downloads
Purpose: Ensure the analysis checkbox reverts if the required visual models are not available.
Setup: Configure settings with image analysis off and inject probes that simulate missing visual models and a prompt acceptance.
Procedure: Toggle the image analysis checkbox on.
Expected outcome: The checkbox reverts to unchecked and settings remain unchanged.
Run: `./build-tests/ai_file_sorter_tests "Image analysis toggle disables when dialog closes without downloads"`
#### Test case: Image analysis toggle cancels when user declines download
Purpose: Verify that declining the download prompt cancels enabling image analysis.
Setup: Configure settings with image analysis off and inject probes that simulate missing visual models and prompt rejection.
Procedure: Toggle the image analysis checkbox on.
Expected outcome: The checkbox remains unchecked, settings remain unchanged, and no download dialog is launched.
Run: `./build-tests/ai_file_sorter_tests "Image analysis toggle cancels when user declines download"`
#### Test case: Already-renamed images skip vision analysis
Purpose: Confirm that images already renamed are handled without re-analysis.
Setup: Provide image entries where one is already renamed and a rename-only flag can be toggled.
Procedure: Run `split_entries_for_analysis()` in two sections: (a) normal categorization and (b) rename-only enabled.
Expected outcome: In normal mode, the already-renamed image is routed to filename-based categorization ("other" bucket). In rename-only mode, the already-renamed image is excluded entirely.
Run: `./build-tests/ai_file_sorter_tests "Already-renamed images skip vision analysis"`
### `tests/unit/test_ui_translator.cpp` (non-Windows only)
#### Test case: UiTranslator updates menus, actions, and controls
Purpose: Validate that the UI translator updates all primary controls, menus, and stateful labels in a consistent pass.
Setup: Build a test harness with a `QMainWindow`, many UI controls, and a translator state set to French in settings. Use a translation function that returns the input string to test label wiring rather than actual translation files.
Procedure: Call `retranslate_all()` and verify the text of buttons, checkboxes, menus, status labels, and the file explorer dock title. Also verify the language action group selection.
Expected outcome: All UI elements show the expected English strings and the French language action is marked checked, demonstrating the retranslate pipeline is correctly wired.
Run: `./build-tests/ai_file_sorter_tests "*UiTranslator updates menus*"`
### `tests/unit/test_utils.cpp`
#### Test case: get_file_name_from_url extracts filename
Purpose: Ensure URL filename extraction returns the last path component.
Setup: Use a URL ending with a file name.
Procedure: Call `Utils::get_file_name_from_url()`.
Expected outcome: The returned string equals the file name (e.g., `mistral-7b.gguf`).
Run: `./build-tests/ai_file_sorter_tests "get_file_name_from_url extracts filename"`
#### Test case: get_file_name_from_url rejects malformed input
Purpose: Confirm invalid URLs are rejected.
Setup: Use a URL with no filename component.
Procedure: Call `Utils::get_file_name_from_url()` and expect an exception.
Expected outcome: A `std::runtime_error` is thrown.
Run: `./build-tests/ai_file_sorter_tests "get_file_name_from_url rejects malformed input"`
#### Test case: is_cuda_available honors probe overrides
Purpose: Verify that CUDA availability probes are honored.
Setup: Install a test hook that returns `true`, then one that returns `false`.
Procedure: Call `Utils::is_cuda_available()` after each probe.
Expected outcome: The function returns `true` and then `false`, matching the probe.
Run: `./build-tests/ai_file_sorter_tests "is_cuda_available honors probe overrides"`
#### Test case: abbreviate_user_path strips home prefix
Purpose: Ensure user paths are shortened relative to `HOME`.
Setup: Create a temporary home directory, set `HOME`, and create a file inside `Documents/`.
Procedure: Call `Utils::abbreviate_user_path()` on the full path.
Expected outcome: The returned string omits the home prefix and begins with `Documents/`.
Run: `./build-tests/ai_file_sorter_tests "abbreviate_user_path strips home prefix"`
#### Test case: sanitize_path_label strips invalid UTF-8 bytes
Purpose: Ensure path labels remain valid UTF-8 even when upstream text contains malformed byte sequences.
Setup: Build a string containing an invalid UTF-8 byte between otherwise valid ASCII text.
Procedure: Call `Utils::sanitize_path_label()`.
Expected outcome: The invalid byte is removed and the returned label remains valid UTF-8 text.
Run: `./build-tests/ai_file_sorter_tests "sanitize_path_label strips invalid UTF-8 bytes"`
### `tests/unit/test_llm_selection_dialog_visual.cpp` (non-Windows only)
#### Test case: Visual LLaVA entry shows missing env var state
Purpose: Confirm UI indicates missing LLaVA download URLs.
Setup: Clear `LLAVA_MODEL_URL` and `LLAVA_MMPROJ_URL` and construct the dialog.
Procedure: Fetch the LLaVA entry via test access.
Expected outcome: The status label reports the missing environment variable and the download button is disabled.
Run: `./build-tests/ai_file_sorter_tests "Visual LLaVA entry shows missing env var state"`
#### Test case: Visual LLaVA entry shows resume state for partial downloads
Purpose: Validate resume state for partial LLaVA downloads.
Setup: Create a fake source file and a smaller destination file, inject metadata headers with an expected size.
Procedure: Update the LLaVA entry state.
Expected outcome: The status label indicates a partial download and the download button changes to "Resume download" and is enabled.
Run: `./build-tests/ai_file_sorter_tests "Visual LLaVA entry shows resume state for partial downloads"`
#### Test case: Visual LLaVA entry reports download errors
Purpose: Ensure download failures are surfaced in the UI.
Setup: Inject a network-available override and a download probe that returns a CURL connection error.
Procedure: Start the LLaVA model download and wait for the label to update.
Expected outcome: The status label begins with "Download error:" indicating the failure is shown to the user.
Run: `./build-tests/ai_file_sorter_tests "Visual LLaVA entry reports download errors"`
### `tests/unit/test_settings_image_options.cpp`
#### Test case: Settings defaults image analysis off even when visual LLM files exist
Purpose: Verify that image analysis defaults remain off when no settings file exists, even if model files are present.
Setup: Create dummy LLaVA model files in the expected location and load settings from an empty config directory.
Procedure: Call `Settings::load()` and read the image analysis flags.
Expected outcome: `load()` returns false, and both analyze and offer-rename flags are false.
Run: `./build-tests/ai_file_sorter_tests "Settings defaults image analysis off even when visual LLM files exist"`
#### Test case: Settings defaults image analysis off when visual LLM files are missing
Purpose: Verify default settings are still off when model files are absent.
Setup: Use a fresh config directory with no LLaVA files.
Procedure: Call `Settings::load()` and read image analysis flags.
Expected outcome: `load()` returns false and analysis/offer-rename remain disabled.
Run: `./build-tests/ai_file_sorter_tests "Settings defaults image analysis off when visual LLM files are missing"`
#### Test case: Settings enforces rename-only implies offer rename
Purpose: Ensure rename-only cannot be enabled without offer-rename.
Setup: Save settings with analyze on, offer-rename off, and rename-only on.
Procedure: Reload settings from disk.
Expected outcome: Offer-rename is forced on while rename-only and process-only settings persist.
Run: `./build-tests/ai_file_sorter_tests "Settings enforces rename-only implies offer rename"`
#### Test case: Settings persists options group expansion state
Purpose: Ensure the image/document option group expansion states persist across load/save.
Setup: Use a temporary config directory and set expanded flags for image and document groups.
Procedure: Save settings, reload into a new `Settings` instance, and read the flags.
Expected outcome: The expansion flags match the saved values.
Run: `./build-tests/ai_file_sorter_tests "Settings persists options group expansion state"`
### `tests/unit/test_checkbox_matrix.cpp`
#### Test case: Checkbox combinations route entries without renamed files
Purpose: Exhaustively validate the file-routing logic for every combination of checkbox flags.
Setup: Define a fixed sample set containing an image, a document, an other file, and a directory. Use an empty set of renamed files.
Procedure: Iterate all 128 combinations of analysis and filtering flags, call `split_entries_for_analysis()`, and compute the expected bucket for each entry.
Expected outcome: Each entry appears only in its expected bucket, image and document buckets contain only supported file types, and a detailed per-combination summary is printed.
Run: `./build-tests/ai_file_sorter_tests "Checkbox combinations route entries without renamed files"`
#### Test case: Checkbox combinations route entries with renamed files
Purpose: Validate routing when image and document entries have already been renamed.
Setup: Use the same sample set but mark the image and document names as already renamed.
Procedure: Repeat the 128-combination sweep and compare actual buckets to expected behavior for rename-only and categorization scenarios.
Expected outcome: Already-renamed items are either skipped or routed to filename-based categorization depending on the rename-only flags, with all entries matching the expected bucket.
Run: `./build-tests/ai_file_sorter_tests "Checkbox combinations route entries with renamed files"`
### `tests/unit/test_llm_downloader.cpp`
#### Test case: LLMDownloader retries full download after a range error
Purpose: Ensure a failed resume attempt triggers a full restart.
Setup: Create a partial destination file and configure the downloader with resume headers. Inject a download probe that returns `CURLE_HTTP_RANGE_ERROR` on the first call and succeeds on the second.
Procedure: Start the download and wait for completion.
Expected outcome: Two attempts are recorded, the second starts from offset 0, the final file size matches the expected size, and no error is reported.
Run: `./build-tests/ai_file_sorter_tests "LLMDownloader retries full download after a range error"`
#### Test case: LLMDownloader uses cached metadata for partial downloads
Purpose: Validate that cached metadata drives download status.
Setup: Create a partial local file and an `.aifs.meta` file with the expected content length.
Procedure: Construct the downloader and query its status.
Expected outcome: Both local and overall download status report `InProgress`, content length is read from metadata, and the downloader is not yet initialized.
Run: `./build-tests/ai_file_sorter_tests "LLMDownloader uses cached metadata for partial downloads"`
#### Test case: LLMDownloader resets to not started when local file is missing
Purpose: Ensure metadata alone does not imply a partial download.
Setup: Create metadata without the local file.
Procedure: Construct the downloader and query its status.
Expected outcome: The status is `NotStarted` for both local and overall views.
Run: `./build-tests/ai_file_sorter_tests "LLMDownloader resets to not started when local file is missing"`
#### Test case: LLMDownloader treats full local file as complete with cached metadata
Purpose: Confirm that a complete local file is recognized as complete.
Setup: Create a local file whose size matches the cached content length.
Procedure: Construct the downloader and query its status.
Expected outcome: Both local and overall download status report `Complete`.
Run: `./build-tests/ai_file_sorter_tests "LLMDownloader treats full local file as complete with cached metadata"`
### `tests/unit/test_update_feed.cpp`
#### Test case: UpdateFeed selects the correct platform stream
Purpose: Ensure the updater resolves the correct platform-specific stream from the shared feed.
Setup: Build a feed JSON payload with distinct `windows`, `macos`, and `linux` entries.
Procedure: Parse the feed for each platform enum.
Expected outcome: Each platform receives its own version and URLs, and the Windows installer checksum is normalized.
Run: `./build-tests/ai_file_sorter_tests "UpdateFeed selects the correct platform stream"`
#### Test case: UpdateFeed falls back to the legacy single-stream schema
Purpose: Preserve compatibility with existing single-stream feeds.
Setup: Build a feed JSON payload with the original flat `update` object.
Procedure: Parse the feed for a platform.
Expected outcome: The legacy fields are still accepted and returned as update info.
Run: `./build-tests/ai_file_sorter_tests "UpdateFeed falls back to the legacy single-stream schema"`
#### Test case: UpdateInstaller downloads, verifies, and reuses a cached installer
Purpose: Validate the Windows-style installer preparation flow without network access.
Setup: Inject a fake installer download callback and a fake launch callback.
Procedure: Prepare the installer twice and then launch it.
Expected outcome: The first prepare downloads and verifies the installer, the second reuses the cached artifact, and the launch callback receives the finalized path.
Run: `./build-tests/ai_file_sorter_tests "UpdateInstaller downloads, verifies, and reuses a cached installer"`
#### Test case: UpdateInstaller rejects installers that fail SHA-256 verification
Purpose: Ensure invalid installer downloads are rejected before launch.
Setup: Inject a fake download callback that writes mismatched bytes.
Procedure: Prepare the installer with an expected SHA-256 that does not match.
Expected outcome: Preparation fails and no finalized installer path is returned.
Run: `./build-tests/ai_file_sorter_tests "UpdateInstaller rejects installers that fail SHA-256 verification"`
#### Test case: UpdateInstaller redownloads cached installers that fail verification
Purpose: Ensure corrupted cached installers are not reused silently.
Setup: Prepare a valid cached installer, then overwrite it with different bytes.
Procedure: Prepare the installer a second time with the same expected SHA-256.
Expected outcome: The cached file is rejected, a new download occurs, and the finalized installer contents match the expected payload.
Run: `./build-tests/ai_file_sorter_tests "UpdateInstaller redownloads cached installers that fail verification"`
#### Test case: UpdateInstaller reports canceled downloads and removes partial files
Purpose: Confirm cancelation produces a canceled result instead of a generic failure and cleans up partial output.
Setup: Inject a fake download callback that writes a partial file and then throws the installer cancelation exception when the cancel probe is true.
Procedure: Call `prepare()` with a cancel probe that always returns true.
Expected outcome: Preparation returns `Canceled`, no finalized installer path is returned, and the partial `.part` file is removed.
Run: `./build-tests/ai_file_sorter_tests "UpdateInstaller reports canceled downloads and removes partial files"`
#### Test case: UpdateInstaller requires installer metadata before preparing
Purpose: Reject malformed update feeds that omit required direct-installer fields.
Setup: Create update info once without `installer_url` and once without `installer_sha256`.
Procedure: Call `prepare()` for both cases.
Expected outcome: Both calls fail with messages indicating the missing field.
Run: `./build-tests/ai_file_sorter_tests "UpdateInstaller requires installer metadata before preparing"`
#### Test case: UpdateInstaller builds launch requests for EXE and MSI installers
Purpose: Verify the installer launch plan uses direct execution for `.exe` files and `msiexec /i` for `.msi` packages.
Setup: Build launch requests for representative `.exe` and `.MSI` paths.
Procedure: Query the test access helper for both inputs.
Expected outcome: The `.exe` request launches the installer directly with no extra arguments, while the `.msi` request targets `msiexec.exe` with `/i `.
Run: `./build-tests/ai_file_sorter_tests "UpdateInstaller builds launch requests for EXE and MSI installers"`
#### Test case: UpdateInstaller auto-install support remains Windows-only
Purpose: Confirm the direct-installer flow is currently gated to Windows builds.
Setup: Create update info with installer metadata.
Procedure: Query the updater installer support state.
Expected outcome: Windows builds report support; other platforms do not.
Run: `./build-tests/ai_file_sorter_tests "UpdateInstaller auto-install support remains Windows-only"`
### `tests/unit/test_updater.cpp`
#### Test case: Updater error dialog offers manual update fallback without quitting when not requested
Purpose: Verify installer-preparation failures still let the user open the normal download page manually for optional updates.
Setup: Construct an updater with test handlers for opening the download URL and quitting the app, and schedule the error dialog to click `Update manually`.
Procedure: Invoke the updater error handler with a `download_url` and `quit_after_open=false`.
Expected outcome: The dialog includes `Update manually`, the download URL handler is called, and the quit handler is not called.
Run: `./build-tests/ai_file_sorter_tests "Updater error dialog offers manual update fallback without quitting when not requested"`
#### Test case: Updater error dialog can request quit after manual fallback
Purpose: Ensure required-update failures can still fall back to the manual download link and then close the app.
Setup: Construct an updater with test handlers and schedule the error dialog to click `Update manually`.
Procedure: Invoke the updater error handler with a `download_url` and `quit_after_open=true`.
Expected outcome: The manual download handler is called and the quit handler is triggered.
Run: `./build-tests/ai_file_sorter_tests "Updater error dialog can request quit after manual fallback"`
#### Test case: Updater error dialog omits manual fallback when no download URL is available
Purpose: Confirm the fallback button is only offered when a manual download link exists.
Setup: Construct an updater with test handlers and invoke the error dialog without a `download_url`.
Procedure: Attempt to click `Update manually`; the helper falls back to `OK` when the button is absent.
Expected outcome: No manual fallback button is present, the error handler returns `false`, and neither the download nor quit handler runs.
Run: `./build-tests/ai_file_sorter_tests "Updater error dialog omits manual fallback when no download URL is available"`
### `tests/unit/test_review_dialog_rename_gate.cpp` (non-Windows only)
#### Test case: Review dialog rename-only toggles disabled when renames are not allowed
Purpose: Verify the review dialog respects the "Offer to rename" gating for images and documents.
Setup: Build a dialog with sample image and document entries and auto-close it using a timer.
Procedure: Show results once with image/document renames disallowed, then again with renames allowed.
Expected outcome: The rename-only checkboxes are disabled in the first case and enabled in the second.
Run: `./build-tests/ai_file_sorter_tests "Review dialog rename-only toggles disabled when renames are not allowed"`
### `tests/unit/test_custom_llm.cpp`
#### Test case: Custom LLM entries persist across Settings load/save
Purpose: Ensure custom LLM definitions persist correctly.
Setup: Insert a custom LLM entry and set it as active, then save settings.
Procedure: Reload settings and retrieve the custom LLM by ID.
Expected outcome: The reloaded entry matches the original fields and the active ID is preserved.
Run: `./build-tests/ai_file_sorter_tests "Custom LLM entries persist across Settings load/save"`
### `tests/unit/test_database_manager_rename_only.cpp`
#### Test case: DatabaseManager keeps rename-only entries with empty labels
Purpose: Ensure rename-only entries are not removed when categories are empty.
Setup: Insert one rename-only entry with a suggested name and one empty entry with no rename suggestion.
Procedure: Call `remove_empty_categorizations()` and then fetch categorized files.
Expected outcome: Only the truly empty entry is removed; the rename-only entry remains with empty category labels and the suggestion intact.
Run: `./build-tests/ai_file_sorter_tests "DatabaseManager keeps rename-only entries with empty labels"`
#### Test case: DatabaseManager sanitizes invalid UTF-8 in cached labels
Purpose: Ensure malformed UTF-8 in cached category labels or suggestions does not propagate into the review dialog pipeline.
Setup: Insert a cached entry whose category, subcategory, and suggested filename contain invalid UTF-8 bytes.
Procedure: Fetch categorized files from the database.
Expected outcome: The loaded category, subcategory, and suggested name are returned with invalid UTF-8 bytes removed.
Run: `./build-tests/ai_file_sorter_tests "DatabaseManager sanitizes invalid UTF-8 in cached labels"`
#### Test case: DatabaseManager normalizes subcategory stopword suffixes for taxonomy matching
Purpose: Verify taxonomy resolution normalizes stopword suffixes like "files".
Setup: Resolve categories with and without the "files" suffix (e.g., "Graphics" vs "Graphics files").
Procedure: Compare the resolved taxonomy IDs and labels.
Expected outcome: Both resolutions share the same taxonomy ID and normalized labels, while unrelated subcategories (e.g., "Photos") remain unchanged.
Run: `./build-tests/ai_file_sorter_tests "DatabaseManager normalizes subcategory stopword suffixes for taxonomy matching"`
#### Test case: DatabaseManager normalizes backup category synonyms for taxonomy matching
Purpose: Ensure backup/archive category variants collapse to a single canonical taxonomy entry.
Setup: Resolve `Archives` and `backup files` with the same subcategory.
Procedure: Compare taxonomy IDs and canonical labels.
Expected outcome: Both labels map to the same taxonomy entry with canonical category `Archives`.
Run: `./build-tests/ai_file_sorter_tests "DatabaseManager normalizes backup category synonyms for taxonomy matching"`
#### Test case: DatabaseManager normalizes image category synonyms and image media aliases
Purpose: Ensure image-related category variants collapse while non-image media remains distinct.
Setup: Resolve `Images`, `Graphics`, `Media + Photos`, and `Media + Audio`.
Procedure: Compare taxonomy IDs and canonical labels.
Expected outcome: `Images/Graphics/Media+Photos` share taxonomy and canonicalize to `Images`; `Media+Audio` remains `Media`.
Run: `./build-tests/ai_file_sorter_tests "DatabaseManager normalizes image category synonyms and image media aliases"`
#### Test case: DatabaseManager normalizes document category synonyms for taxonomy matching
Purpose: Ensure document-like category variants collapse to `Documents`.
Setup: Resolve `Documents`, `Texts`, `Papers`, and `Spreadsheets` with the same subcategory.
Procedure: Compare taxonomy IDs and canonical labels.
Expected outcome: All variants map to the same taxonomy entry with canonical category `Documents`.
Run: `./build-tests/ai_file_sorter_tests "DatabaseManager normalizes document category synonyms for taxonomy matching"`
#### Test case: DatabaseManager normalizes installer and update category synonyms for taxonomy matching
Purpose: Ensure software/install/update category variants collapse to `Software`.
Setup: Resolve `Software`, `Installers`, `Setup files`, `Software Update`, and `Patches`.
Procedure: Compare taxonomy IDs and canonical labels.
Expected outcome: All variants map to the same taxonomy entry with canonical category `Software`.
Run: `./build-tests/ai_file_sorter_tests "DatabaseManager normalizes installer and update category synonyms for taxonomy matching"`
### `tests/unit/test_file_scanner.cpp`
#### Test case: hidden files require explicit flag
Purpose: Ensure hidden files are filtered unless explicitly requested.
Setup: Create a hidden file in a temporary directory.
Procedure: Scan with only `Files`, then with `Files | HiddenFiles`.
Expected outcome: The hidden file is absent in the first scan and present in the second.
Run: `./build-tests/ai_file_sorter_tests "hidden files require explicit flag"`
#### Test case: junk files are skipped regardless of flags
Purpose: Confirm that known junk files are always excluded.
Setup: Create a `.DS_Store` file.
Procedure: Scan with `Files | HiddenFiles`.
Expected outcome: The junk file does not appear in the results.
Run: `./build-tests/ai_file_sorter_tests "junk files are skipped regardless of flags"`
#### Test case: application bundles are treated as files
Purpose: Ensure application bundles are treated as files rather than directories.
Setup: Create a `Sample.app` directory with a `Contents` subdirectory.
Procedure: Scan once for files and once for directories.
Expected outcome: The bundle appears only in the file scan and not in the directory scan.
Run: `./build-tests/ai_file_sorter_tests "application bundles are treated as files"`
#### Test case: recursive scans include nested files
Purpose: Ensure recursive scans still return files from nested subdirectories.
Setup: Create one file in the root and one file in a nested subdirectory.
Procedure: Scan with `Files | Recursive`.
Expected outcome: Both files appear in the results.
Run: `./build-tests/ai_file_sorter_tests "recursive scans include nested files"`
#### Test case: recursive scans skip unreadable directories and continue
Purpose: Ensure one inaccessible subdirectory does not abort an otherwise valid recursive scan.
Setup: Create a readable subtree and a second subtree whose directory permissions are removed (non-Windows only).
Procedure: Scan with `Files | Recursive`.
Expected outcome: The readable file is returned, the scan does not throw, and the unreadable subtree is skipped.
Run: `./build-tests/ai_file_sorter_tests "recursive scans skip unreadable directories and continue"`
### `tests/unit/test_support_prompt.cpp`
#### Test case: Support prompt thresholds advance based on response
Purpose: Verify support prompt scheduling logic under different user responses.
Setup: Create a fresh settings environment and define a callback that returns a simulated response (`NotSure`, `CannotDonate`, or `Support`).
Procedure: Increment the categorized file count to the current threshold, observe the prompt, then advance to the next threshold.
Expected outcome: The prompt fires exactly at thresholds, the total count increments correctly, and the next threshold increases for all response types.
Run: `./build-tests/ai_file_sorter_tests "Support prompt thresholds advance based on response"`
#### Test case: Zero categorized increments do not change totals or trigger prompts
Purpose: Ensure a zero increment is a no-op.
Setup: Fresh settings with a baseline threshold.
Procedure: Call the prompt simulation with an increment of `0`.
Expected outcome: Total counts and thresholds remain unchanged and the callback is not invoked.
Run: `./build-tests/ai_file_sorter_tests "Zero categorized increments do not change totals or trigger prompts"`
### `tests/unit/test_custom_api_endpoint.cpp`
#### Test case: Custom API endpoints persist across Settings load/save
Purpose: Ensure custom OpenAI-compatible endpoint definitions persist correctly.
Setup: Create a custom endpoint with name, description, base URL, API key, and model, then set it active and save.
Procedure: Reload settings and retrieve the endpoint by ID.
Expected outcome: All fields match the original, and the active endpoint ID is preserved.
Run: `./build-tests/ai_file_sorter_tests "Custom API endpoints persist across Settings load/save"`
### `tests/unit/test_categorization_dialog.cpp` (non-Windows only)
#### Test case: CategorizationDialog uses subcategory toggle when moving files
Purpose: Ensure the dialog respects the subcategory visibility toggle during file moves.
Setup: Create a sample categorized file and attach a move probe.
Procedure: Toggle the subcategory column state and confirm the dialog.
Expected outcome: The move probe records the same subcategory setting that was applied.
Run: `./build-tests/ai_file_sorter_tests "CategorizationDialog uses subcategory toggle when moving files"`
#### Test case: CategorizationDialog supports sorting by columns
Purpose: Verify that the table model sorts correctly by different columns.
Setup: Insert two entries with out-of-order file names and categories.
Procedure: Sort by the file name column ascending, then by category descending.
Expected outcome: The first sort yields alphabetical file names; the second yields categories in reverse alphabetical order.
Run: `./build-tests/ai_file_sorter_tests "CategorizationDialog supports sorting by columns"`
#### Test case: CategorizationDialog undo restores moved files
Purpose: Confirm that undo reverses category moves.
Setup: Create a file on disk with a category and subcategory.
Procedure: Confirm the dialog to move the file, then trigger undo.
Expected outcome: The file moves to the category path, then returns to the original location; undo is enabled only when a move exists.
Run: `./build-tests/ai_file_sorter_tests "CategorizationDialog undo restores moved files"`
#### Test case: CategorizationDialog undo allows renaming again
Purpose: Ensure undo resets rename-only operations and allows reapplication.
Setup: Create a rename-only entry with a suggested name.
Procedure: Confirm the rename, undo it, and confirm again.
Expected outcome: Each confirm applies the rename, and undo restores the original filename for a second rename.
Run: `./build-tests/ai_file_sorter_tests "CategorizationDialog undo allows renaming again"`
#### Test case: CategorizationDialog rename-only updates cached filename
Purpose: Verify database updates when a rename-only action occurs.
Setup: Use a dialog with a database manager and a rename-only file with a suggestion.
Procedure: Confirm the dialog and query the database.
Expected outcome: The old name is not cached; the new name is cached with rename-only metadata and the suggested name.
Run: `./build-tests/ai_file_sorter_tests "CategorizationDialog rename-only updates cached filename"`
#### Test case: CategorizationDialog allows editing when rename-only checkbox is off
Purpose: Ensure category fields remain editable when rename-only mode is not enforced.
Setup: Populate the dialog with one rename-only entry and one categorized entry.
Procedure: Inspect the category column editability in the model.
Expected outcome: Both rows remain editable in the category column.
Run: `./build-tests/ai_file_sorter_tests "CategorizationDialog allows editing when rename-only checkbox is off"`
#### Test case: CategorizationDialog deduplicates suggested names when rename-only is toggled
Purpose: Ensure duplicate suggestions are made unique when rename-only is turned on.
Setup: Provide two image entries with identical suggested names.
Procedure: Toggle the rename-only checkbox in the dialog.
Expected outcome: The suggestions are rewritten with numbered suffixes (e.g., `_1`, `_2`).
Run: `./build-tests/ai_file_sorter_tests "CategorizationDialog deduplicates suggested names when rename-only is toggled"`
#### Test case: CategorizationDialog avoids double suffixes for numbered suggestions
Purpose: Prevent double-numbering when suggestions already contain a suffix.
Setup: Use two rename-only entries with a suggestion ending in `_1`.
Procedure: Populate the dialog and read back the suggested names.
Expected outcome: The first remains `_1`, and the second becomes `_2` without duplicating the suffix.
Run: `./build-tests/ai_file_sorter_tests "CategorizationDialog avoids double suffixes for numbered suggestions"`
#### Test case: CategorizationDialog hides suggested names for renamed entries
Purpose: Hide rename suggestions once the rename has already been applied.
Setup: Create an entry with `rename_applied=true` and a suggested name.
Procedure: Populate the dialog and inspect the suggested name cell.
Expected outcome: The suggested name cell is empty and not editable.
Run: `./build-tests/ai_file_sorter_tests "CategorizationDialog hides suggested names for renamed entries"`
#### Test case: CategorizationDialog hides already renamed rows when rename-only is on
Purpose: Ensure completed renames are hidden when only renaming is requested.
Setup: Add one renamed entry and one pending entry, then enable the rename-only checkbox.
Procedure: Toggle rename-only and inspect row visibility.
Expected outcome: The already renamed row becomes hidden while the pending row remains visible.
Run: `./build-tests/ai_file_sorter_tests "CategorizationDialog hides already renamed rows when rename-only is on"`
#### Test case: CategorizationDialog deduplicates suggested picture filenames
Purpose: Ensure image rename suggestions are unique across multiple rows.
Setup: Provide two rename-only image entries with identical suggested names.
Procedure: Populate the dialog and read the suggested names.
Expected outcome: Suggestions become `_1` and `_2` variants to avoid collisions.
Run: `./build-tests/ai_file_sorter_tests "CategorizationDialog deduplicates suggested picture filenames"`
#### Test case: CategorizationDialog avoids existing picture filename collisions
Purpose: Ensure suggested names do not collide with existing files on disk.
Setup: Create a file on disk that matches the suggested name and add a rename-only entry with that suggestion.
Procedure: Populate the dialog and inspect the suggestion.
Expected outcome: The suggestion is incremented (e.g., `_1`) to avoid the existing file.
Run: `./build-tests/ai_file_sorter_tests "CategorizationDialog avoids existing picture filename collisions"`
#### Test case: CategorizationDialog rename-only preserves cached categories without renaming
Purpose: Ensure rename-only mode keeps existing category assignments even when no rename occurs.
Setup: Cache a categorization in the database, then run the dialog with a rename-only entry that has no suggested name.
Procedure: Confirm the dialog and query the cache.
Expected outcome: The cached category and subcategory are preserved and the entry remains marked rename-only.
Run: `./build-tests/ai_file_sorter_tests "CategorizationDialog rename-only preserves cached categories without renaming"`
#### Test case: CategorizationDialog rename-only preserves cached categories when renaming
Purpose: Ensure rename-only mode keeps cached categories after a rename.
Setup: Cache a categorization, then run the dialog with a rename-only entry that includes a suggested name.
Procedure: Confirm the dialog and query the cache for the renamed file.
Expected outcome: The renamed entry retains the cached category and subcategory with rename-only metadata.
Run: `./build-tests/ai_file_sorter_tests "CategorizationDialog rename-only preserves cached categories when renaming"`
### `tests/unit/test_main_app_translation.cpp` (non-Windows only)
#### Test case: MainApp retranslate reflects language changes
Purpose: Validate that main window labels update for all supported UI languages.
Setup: Construct `MainApp` with a settings object and a translation manager.
Procedure: Iterate through supported languages, set the language, trigger a retranslate, and read the analyze button and folder label text.
Expected outcome: Each language produces the exact expected translations for the two labels.
Run: `./build-tests/ai_file_sorter_tests "MainApp retranslate reflects language changes"`
### `tests/unit/test_whitelist_and_prompt.cpp`
#### Test case: WhitelistStore initializes from settings and persists defaults
Purpose: Ensure whitelist entries are loaded into settings and persisted.
Setup: Create a whitelist entry, save it, and initialize the store from settings with a selected whitelist name.
Procedure: Verify the settings fields and reload the whitelist store from disk.
Expected outcome: The whitelist name, categories, and subcategories remain consistent across initialization and reload.
Run: `./build-tests/ai_file_sorter_tests "WhitelistStore initializes from settings and persists defaults"`
#### Test case: CategorizationService builds numbered whitelist context
Purpose: Confirm the whitelist context includes numbered categories and an "any" subcategory fallback.
Setup: Set allowed categories in settings and build a service instance.
Procedure: Call the test access method to build the whitelist context string.
Expected outcome: The context includes numbered category lines and indicates that subcategories are unrestricted.
Run: `./build-tests/ai_file_sorter_tests "CategorizationService builds numbered whitelist context"`
#### Test case: CategorizationService builds category language context when non-English selected
Purpose: Ensure the category language context is generated for non-English settings.
Setup: Set the category language to French.
Procedure: Build the category language context string.
Expected outcome: The context is non-empty and references "French".
Run: `./build-tests/ai_file_sorter_tests "CategorizationService builds category language context when non-English selected"`
#### Test case: CategorizationService builds category language context for Spanish
Purpose: Verify Spanish category language is handled explicitly.
Setup: Set the category language to Spanish.
Procedure: Build the category language context string.
Expected outcome: The context is non-empty and references "Spanish".
Run: `./build-tests/ai_file_sorter_tests "CategorizationService builds category language context for Spanish"`
#### Test case: CategorizationService parses category output without spaced colon delimiters
Purpose: Ensure category parsing accepts compact `Category:Subcategory` output.
Setup: Use a fixed LLM stub response `Documents:Spreadsheets`.
Procedure: Run `categorize_entries` for one file entry.
Expected outcome: Parsed category is `Documents` and parsed subcategory is `Spreadsheets`.
Run: `./build-tests/ai_file_sorter_tests "CategorizationService parses category output without spaced colon delimiters"`
#### Test case: CategorizationService parses labeled category and subcategory lines
Purpose: Ensure category parsing accepts labeled multiline output.
Setup: Use a fixed LLM stub response with `Category: ...` and `Subcategory: ...` lines.
Procedure: Run `categorize_entries` for one file entry.
Expected outcome: Parsed labels match the provided category and subcategory values.
Run: `./build-tests/ai_file_sorter_tests "CategorizationService parses labeled category and subcategory lines"`
### `tests/unit/test_cache_interactions.cpp`
#### Test case: CategorizationService uses cached categorization without calling LLM
Purpose: Ensure cached category/subcategory rows are returned without invoking the LLM.
Setup: Seed the database with a resolved category for a file entry and prepare a counting LLM stub.
Procedure: Call `categorize_entries` for that file.
Expected outcome: The cached category is returned and the LLM call counter stays at zero.
Run: `./build-tests/ai_file_sorter_tests "CategorizationService uses cached categorization without calling LLM"`
#### Test case: CategorizationService falls back to LLM when cache is empty
Purpose: Validate cache fallback to LLM and persistence of returned category values.
Setup: Seed an empty cache record for a file and prepare a counting LLM stub with a valid label response.
Procedure: Call `categorize_entries` for that file and then read the DB row.
Expected outcome: The LLM is called once and the resulting category/subcategory are written back to cache.
Run: `./build-tests/ai_file_sorter_tests "CategorizationService falls back to LLM when cache is empty"`
#### Test case: CategorizationService loads cached entries recursively for analysis
Purpose: Confirm recursive cache loading obeys the `include_subdirectories` setting.
Setup: Seed one cached row at root level and one in a child path.
Procedure: Call `load_cached_entries` with recursion off, then on.
Expected outcome: Non-recursive mode returns only root rows; recursive mode returns both rows.
Run: `./build-tests/ai_file_sorter_tests "CategorizationService loads cached entries recursively for analysis"`
#### Test case: ResultsCoordinator respects full-path cache keys for recursive scans
Purpose: Ensure recursive scans treat same-name files in different folders as distinct when using full-path cache keys.
Setup: Create duplicate filenames at root and nested paths, then seed cache keys by full path.
Procedure: Compute uncached entries via `find_files_to_categorize`.
Expected outcome: Only the truly uncached nested path remains in the result set.
Run: `./build-tests/ai_file_sorter_tests "ResultsCoordinator respects full-path cache keys for recursive scans"`
#### Test case: CategorizationService invokes completion callback per entry
Purpose: Verify per-entry completion notifications fire for categorization progress tracking.
Setup: Prepare multiple file entries and callbacks that count queued/completed events.
Procedure: Run `categorize_entries` and capture callback counters.
Expected outcome: Queue and completion callbacks are each invoked once per processed entry.
Run: `./build-tests/ai_file_sorter_tests "CategorizationService invokes completion callback per entry"`
### Test infrastructure: `tests/unit/test_cli_reporter.cpp`
This file registers a Catch2 event listener that prints a one-line "[TEST]" banner for each test case as it begins. It does not define test cases itself, but it makes CLI output easier to follow during long runs.
================================================
FILE: TRADEMARKS.md
================================================
# Trademarks
This project is open source, but its name and branding are not licensed for general reuse.
The project owner claims trademark rights, to the extent permitted by law, in the following project marks (the "Project Marks"):
- `AI File Sorter`
- `ai-file-sorter`
- The AI File Sorter app name, word marks, logos, icons, badges, and other official branding assets included in this repository, including the app icon in `app/resources/images/icon_256x256.png`
The source code in this repository is licensed separately under the GNU Affero General Public License v3.0 (see `LICENSE`). That license applies to the code and other material covered by copyright. It does not grant permission to use the Project Marks, except for truthful, nominative reference.
You may:
- Use the Project Marks in plain text to refer to the official project, discuss compatibility, or describe unmodified official releases.
- Keep existing trademark notices intact when redistributing unmodified copies.
You may not, without prior written permission from the project owner:
- Use the Project Marks to brand a modified build, fork, derivative distribution, commercial service, or other product in a way that is likely to cause confusion.
- Use the Project Marks, logos, or other branding in a way that implies sponsorship, endorsement, affiliation, or official status.
- Register, adopt, or use confusingly similar names, domains, package names, or social handles.
If you distribute a modified version of this project, you should remove or replace the Project Marks, including the app name, icon, and logos, unless you have separate written permission to keep them.
Third-party product names, logos, and trademarks referenced in this repository remain the property of their respective owners.
All trademark rights are reserved. No trademark license is granted by this repository, by the AGPL, or by any contributor, except by separate express written permission.
This file provides notice of the project's trademark policy. It is not a substitute for trademark registration or jurisdiction-specific legal advice.
================================================
FILE: app/CMakeLists.txt
================================================
cmake_minimum_required(VERSION 3.21)
project(AIFileSorter LANGUAGES CXX)
# C++ standard
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
option(AI_FILE_SORTER_BUILD_TESTS "Build unit tests (requires Catch2 submodule)" OFF)
option(AI_FILE_SORTER_REQUIRE_EMBEDDED_PDF_BACKEND
"Require vendored PDFium for PDF extraction instead of silently falling back to external CLI tools." ON)
set(AI_FILE_SORTER_UPDATE_MODE "PLATFORM_DEFAULT" CACHE STRING
"Updater behavior for this build: PLATFORM_DEFAULT, AUTO_INSTALL, NOTIFY_ONLY, or DISABLED")
set_property(CACHE AI_FILE_SORTER_UPDATE_MODE PROPERTY STRINGS
PLATFORM_DEFAULT AUTO_INSTALL NOTIFY_ONLY DISABLED)
set(_aifs_effective_update_mode "${AI_FILE_SORTER_UPDATE_MODE}")
if(_aifs_effective_update_mode STREQUAL "PLATFORM_DEFAULT")
if(WIN32)
set(_aifs_effective_update_mode "AUTO_INSTALL")
else()
set(_aifs_effective_update_mode "NOTIFY_ONLY")
endif()
endif()
set(_aifs_allowed_update_modes AUTO_INSTALL NOTIFY_ONLY DISABLED)
list(FIND _aifs_allowed_update_modes "${_aifs_effective_update_mode}" _aifs_update_mode_index)
if(_aifs_update_mode_index EQUAL -1)
message(FATAL_ERROR
"Unsupported AI_FILE_SORTER_UPDATE_MODE='${AI_FILE_SORTER_UPDATE_MODE}'. "
"Expected PLATFORM_DEFAULT, AUTO_INSTALL, NOTIFY_ONLY, or DISABLED.")
endif()
if(_aifs_effective_update_mode STREQUAL "AUTO_INSTALL")
set(AI_FILE_SORTER_UPDATE_MODE_DEFINE AI_FILE_SORTER_UPDATE_MODE_AUTO_INSTALL)
elseif(_aifs_effective_update_mode STREQUAL "NOTIFY_ONLY")
set(AI_FILE_SORTER_UPDATE_MODE_DEFINE AI_FILE_SORTER_UPDATE_MODE_NOTIFY_ONLY)
else()
set(AI_FILE_SORTER_UPDATE_MODE_DEFINE AI_FILE_SORTER_UPDATE_MODE_DISABLED)
endif()
if(DEFINED VCPKG_INSTALLED_DIR AND NOT "${VCPKG_INSTALLED_DIR}" STREQUAL "")
set(AIFS_VCPKG_INSTALLED_ROOT "${VCPKG_INSTALLED_DIR}")
else()
set(AIFS_VCPKG_INSTALLED_ROOT "${CMAKE_BINARY_DIR}/vcpkg_installed")
endif()
message(STATUS "AI File Sorter updater mode: ${_aifs_effective_update_mode}")
include(CheckSymbolExists)
include(CheckCXXSourceCompiles)
function(aifs_apply_update_mode target_name)
target_compile_definitions(${target_name} PRIVATE ${AI_FILE_SORTER_UPDATE_MODE_DEFINE})
endfunction()
function(aifs_apply_named_update_mode target_name update_mode)
if(update_mode STREQUAL "AUTO_INSTALL")
target_compile_definitions(${target_name} PRIVATE AI_FILE_SORTER_UPDATE_MODE_AUTO_INSTALL)
elseif(update_mode STREQUAL "NOTIFY_ONLY")
target_compile_definitions(${target_name} PRIVATE AI_FILE_SORTER_UPDATE_MODE_NOTIFY_ONLY)
elseif(update_mode STREQUAL "DISABLED")
target_compile_definitions(${target_name} PRIVATE AI_FILE_SORTER_UPDATE_MODE_DISABLED)
else()
message(FATAL_ERROR
"Unsupported explicit updater mode '${update_mode}' for target '${target_name}'.")
endif()
endfunction()
function(aifs_apply_expected_update_mode target_name update_mode)
if(update_mode STREQUAL "AUTO_INSTALL")
target_compile_definitions(${target_name} PRIVATE AI_FILE_SORTER_EXPECTED_UPDATE_MODE_AUTO_INSTALL)
elseif(update_mode STREQUAL "NOTIFY_ONLY")
target_compile_definitions(${target_name} PRIVATE AI_FILE_SORTER_EXPECTED_UPDATE_MODE_NOTIFY_ONLY)
elseif(update_mode STREQUAL "DISABLED")
target_compile_definitions(${target_name} PRIVATE AI_FILE_SORTER_EXPECTED_UPDATE_MODE_DISABLED)
else()
message(FATAL_ERROR
"Unsupported expected updater mode '${update_mode}' for target '${target_name}'.")
endif()
endfunction()
# Prefer MSVC on Windows if available
if(WIN32 AND CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
add_definitions(-D_CRT_SECURE_NO_WARNINGS)
# Keep MSVC worker processes busy within each project in addition to
# the top-level cmake --build --parallel job scheduling.
add_compile_options(
"$<$:/MP>"
"$<$:/MP>"
)
endif()
# Qt6
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets)
find_package(Qt6LinguistTools CONFIG QUIET)
if(NOT Qt6LinguistTools_FOUND)
set(_aifs_qttools_hints "")
if(DEFINED ENV{HOMEBREW_PREFIX} AND NOT "$ENV{HOMEBREW_PREFIX}" STREQUAL "")
list(APPEND _aifs_qttools_hints "$ENV{HOMEBREW_PREFIX}/opt/qttools")
endif()
list(APPEND _aifs_qttools_hints /opt/homebrew/opt/qttools /usr/local/opt/qttools)
if(DEFINED Qt6_DIR AND NOT "${Qt6_DIR}" STREQUAL "")
get_filename_component(_aifs_qt6_prefix "${Qt6_DIR}/../../.." ABSOLUTE)
list(APPEND _aifs_qttools_hints "${_aifs_qt6_prefix}")
endif()
list(REMOVE_DUPLICATES _aifs_qttools_hints)
foreach(_aifs_qttools_hint IN LISTS _aifs_qttools_hints)
if(EXISTS "${_aifs_qttools_hint}/lib/cmake/Qt6LinguistTools/Qt6LinguistToolsConfig.cmake")
list(APPEND CMAKE_PREFIX_PATH "${_aifs_qttools_hint}")
endif()
endforeach()
list(REMOVE_DUPLICATES CMAKE_PREFIX_PATH)
find_package(Qt6LinguistTools CONFIG REQUIRED)
endif()
# Third-party deps (resolved best via vcpkg on Windows)
find_package(CURL REQUIRED)
find_package(OpenSSL REQUIRED)
find_package(SQLite3 REQUIRED)
# JsonCpp may not ship a CMake config on some distros (e.g. Ubuntu). Try
# config mode first, then fall back to pkg-config.
find_package(JsonCpp CONFIG QUIET)
if(NOT JsonCpp_FOUND)
find_package(PkgConfig REQUIRED)
pkg_check_modules(JSONCPP REQUIRED jsoncpp)
add_library(JsonCpp::JsonCpp INTERFACE IMPORTED)
target_include_directories(JsonCpp::JsonCpp INTERFACE ${JSONCPP_INCLUDE_DIRS})
target_link_directories(JsonCpp::JsonCpp INTERFACE ${JSONCPP_LIBRARY_DIRS})
target_link_libraries(JsonCpp::JsonCpp INTERFACE ${JSONCPP_LINK_LIBRARIES})
else()
if(NOT TARGET JsonCpp::JsonCpp)
add_library(JsonCpp::JsonCpp INTERFACE IMPORTED)
endif()
endif()
find_package(fmt REQUIRED CONFIG)
find_package(spdlog REQUIRED CONFIG)
find_package(Intl REQUIRED) # libintl/gettext
# MediaInfoLib from vcpkg depends on the separate ZenLib package and refers to
# its imported target as plain "zen".
find_package(ZenLib CONFIG QUIET)
# MediaInfoLib policy:
# - Must come from package managers (apt/dnf/pacman/brew/vcpkg)
# - Vendored submodules / checked-in binaries are rejected
option(AI_FILE_SORTER_REQUIRE_MEDIAINFOLIB
"Require MediaInfoLib at configure time for full audio/video metadata extraction" ON)
option(AI_FILE_SORTER_ALLOW_VENDORED_MEDIAINFOLIB
"Allow vendored MediaInfo in the repository (not recommended)" OFF)
file(REAL_PATH "${CMAKE_CURRENT_SOURCE_DIR}/.." AI_FILE_SORTER_REPO_ROOT)
set(_aifs_blocked_mediainfo_paths
"${AI_FILE_SORTER_REPO_ROOT}/external/MediaInfoLib"
"${AI_FILE_SORTER_REPO_ROOT}/external/libmediainfo"
"${CMAKE_CURRENT_SOURCE_DIR}/lib/mediainfo"
"${CMAKE_CURRENT_SOURCE_DIR}/include/MediaInfo"
)
if(NOT AI_FILE_SORTER_ALLOW_VENDORED_MEDIAINFOLIB)
foreach(_blocked_path IN LISTS _aifs_blocked_mediainfo_paths)
if(EXISTS "${_blocked_path}")
message(FATAL_ERROR
"Found vendored MediaInfo content at '${_blocked_path}'. "
"Use a package-managed libmediainfo (apt/dnf/pacman/brew/vcpkg) instead.")
endif()
endforeach()
endif()
function(aifs_assert_path_not_in_repo path_value label)
if(NOT path_value)
return()
endif()
file(TO_CMAKE_PATH "${AI_FILE_SORTER_REPO_ROOT}" _repo_root_norm)
string(REGEX REPLACE "([][+.*^$(){}|?\\\\])" "\\\\\\1" _repo_root_regex "${_repo_root_norm}")
foreach(_entry IN LISTS path_value)
if(NOT _entry OR _entry MATCHES "^\\$<")
continue()
endif()
file(TO_CMAKE_PATH "${_entry}" _entry_norm)
# vcpkg manifest mode can stage package-managed headers/libs inside the
# build tree under a local vcpkg_installed directory. That is still a
# package-manager origin, not vendored MediaInfo content.
if(_entry_norm MATCHES "(^|/)vcpkg_installed(/|$)")
continue()
endif()
if(_entry_norm MATCHES "^${_repo_root_regex}(/|$)")
message(FATAL_ERROR
"MediaInfo must come from a package manager. "
"Repository-local path detected for ${label}: '${_entry}'.")
endif()
endforeach()
endfunction()
function(aifs_assert_mediainfo_target_external target_name)
if(NOT TARGET ${target_name})
return()
endif()
foreach(_prop IN ITEMS IMPORTED_LOCATION IMPORTED_IMPLIB INTERFACE_INCLUDE_DIRECTORIES INTERFACE_LINK_DIRECTORIES)
get_target_property(_prop_value ${target_name} ${_prop})
if(NOT _prop_value OR _prop_value STREQUAL "_prop_value-NOTFOUND")
continue()
endif()
aifs_assert_path_not_in_repo("${_prop_value}" "${target_name}:${_prop}")
endforeach()
endfunction()
set(MEDIAINFO_DEPS_LIBS "")
find_package(MediaInfoLib CONFIG QUIET)
if(TARGET MediaInfoLib::MediaInfoLib)
list(APPEND MEDIAINFO_DEPS_LIBS MediaInfoLib::MediaInfoLib)
elseif(TARGET MediaInfoLib::MediaInfo)
list(APPEND MEDIAINFO_DEPS_LIBS MediaInfoLib::MediaInfo)
elseif(TARGET MediaInfo::MediaInfo)
list(APPEND MEDIAINFO_DEPS_LIBS MediaInfo::MediaInfo)
elseif(DEFINED MediaInfoLib_LIBRARY AND TARGET ${MediaInfoLib_LIBRARY})
# The vcpkg MediaInfoLib port exports a plain imported target named
# "mediainfo" and exposes that name through MediaInfoLib_LIBRARY.
list(APPEND MEDIAINFO_DEPS_LIBS ${MediaInfoLib_LIBRARY})
elseif(TARGET mediainfo)
list(APPEND MEDIAINFO_DEPS_LIBS mediainfo)
else()
find_package(PkgConfig QUIET)
if(PkgConfig_FOUND)
pkg_check_modules(MEDIAINFO QUIET IMPORTED_TARGET libmediainfo)
if(TARGET PkgConfig::MEDIAINFO)
list(APPEND MEDIAINFO_DEPS_LIBS PkgConfig::MEDIAINFO)
endif()
endif()
if(NOT MEDIAINFO_DEPS_LIBS AND APPLE)
set(_aifs_mediainfo_prefixes "")
if(DEFINED ENV{HOMEBREW_PREFIX} AND NOT "$ENV{HOMEBREW_PREFIX}" STREQUAL "")
list(APPEND _aifs_mediainfo_prefixes "$ENV{HOMEBREW_PREFIX}")
endif()
list(APPEND _aifs_mediainfo_prefixes /opt/homebrew /usr/local)
list(REMOVE_DUPLICATES _aifs_mediainfo_prefixes)
set(_aifs_mediainfo_include_hints "")
set(_aifs_mediainfo_library_hints "")
foreach(_aifs_prefix IN LISTS _aifs_mediainfo_prefixes)
list(APPEND _aifs_mediainfo_include_hints
"${_aifs_prefix}/opt/libmediainfo/include"
"${_aifs_prefix}/include"
)
list(APPEND _aifs_mediainfo_library_hints
"${_aifs_prefix}/opt/libmediainfo/lib"
"${_aifs_prefix}/lib"
)
endforeach()
find_path(AIFS_MEDIAINFO_INCLUDE_DIR
NAMES MediaInfo/MediaInfo.h
HINTS ${_aifs_mediainfo_include_hints}
)
find_library(AIFS_MEDIAINFO_LIBRARY
NAMES mediainfo libmediainfo
HINTS ${_aifs_mediainfo_library_hints}
)
if(AIFS_MEDIAINFO_INCLUDE_DIR AND AIFS_MEDIAINFO_LIBRARY)
if(NOT TARGET AIFS::MediaInfoLib)
add_library(AIFS::MediaInfoLib UNKNOWN IMPORTED)
set_target_properties(AIFS::MediaInfoLib PROPERTIES
IMPORTED_LOCATION "${AIFS_MEDIAINFO_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${AIFS_MEDIAINFO_INCLUDE_DIR}"
)
endif()
list(APPEND MEDIAINFO_DEPS_LIBS AIFS::MediaInfoLib)
endif()
unset(_aifs_mediainfo_include_hints)
unset(_aifs_mediainfo_library_hints)
unset(_aifs_mediainfo_prefixes)
endif()
endif()
if(MEDIAINFO_DEPS_LIBS)
foreach(_mediainfo_target IN LISTS MEDIAINFO_DEPS_LIBS)
aifs_assert_mediainfo_target_external(${_mediainfo_target})
endforeach()
add_compile_definitions(AI_FILE_SORTER_USE_MEDIAINFOLIB)
message(STATUS "MediaInfoLib found via package-managed dependency: enabling full audio/video metadata extraction.")
elseif(AI_FILE_SORTER_REQUIRE_MEDIAINFOLIB)
message(FATAL_ERROR
"MediaInfoLib is required but was not found. Install libmediainfo development files "
"(Linux: libmediainfo-dev, macOS: brew install mediainfo, Windows/vcpkg: libmediainfo). "
"Vendored MediaInfo submodules/binaries are not supported.")
else()
message(STATUS
"MediaInfoLib not found: using built-in metadata fallback parsers "
"(audio + MP4/MOV/M4V/3GP tags only).")
endif()
# Vendored document analysis deps
set(EXTERNAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../external")
set(PUGIXML_DIR "${EXTERNAL_DIR}/pugixml")
set(LIBZIP_DIR "${EXTERNAL_DIR}/libzip")
set(PDFIUM_DIR "${EXTERNAL_DIR}/pdfium")
set(DOC_DEPS_INCLUDE_DIRS "")
set(DOC_DEPS_LIBS "")
# Pugixml (compiled via PugixmlBundle.cpp)
if(EXISTS "${PUGIXML_DIR}/src/pugixml.hpp")
list(APPEND DOC_DEPS_INCLUDE_DIRS "${PUGIXML_DIR}/src")
add_compile_definitions(PUGIXML_NO_EXCEPTIONS AI_FILE_SORTER_USE_PUGIXML)
endif()
# libzip (build from vendored source)
if(EXISTS "${LIBZIP_DIR}/CMakeLists.txt")
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(ENABLE_BZIP2 OFF CACHE BOOL "" FORCE)
set(ENABLE_LZMA OFF CACHE BOOL "" FORCE)
set(ENABLE_ZSTD OFF CACHE BOOL "" FORCE)
set(ENABLE_OPENSSL OFF CACHE BOOL "" FORCE)
set(ENABLE_GNUTLS OFF CACHE BOOL "" FORCE)
set(ENABLE_MBEDTLS OFF CACHE BOOL "" FORCE)
set(ENABLE_COMMONCRYPTO OFF CACHE BOOL "" FORCE)
set(ENABLE_WINDOWS_CRYPTO OFF CACHE BOOL "" FORCE)
add_subdirectory("${LIBZIP_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/libzip")
if(TARGET libzip::zip)
add_compile_definitions(AI_FILE_SORTER_USE_LIBZIP)
list(APPEND DOC_DEPS_INCLUDE_DIRS "${LIBZIP_DIR}/lib" "${CMAKE_CURRENT_BINARY_DIR}/libzip")
list(APPEND DOC_DEPS_LIBS libzip::zip)
endif()
endif()
set(PDFIUM_PLATFORM_DIR "")
set(PDFIUM_LIBRARY "")
set(PDFIUM_RUNTIME "")
set(_aifs_pdfium_missing_reason "")
if(WIN32)
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(PDFIUM_PLATFORM_DIR "${PDFIUM_DIR}/windows-x64")
set(PDFIUM_LIBRARY "${PDFIUM_PLATFORM_DIR}/lib/pdfium.dll.lib")
set(PDFIUM_RUNTIME "${PDFIUM_PLATFORM_DIR}/bin/pdfium.dll")
else()
set(_aifs_pdfium_missing_reason
"Embedded PDFium is only vendored for 64-bit Windows builds.")
endif()
elseif(APPLE)
set(_aifs_pdfium_apple_arch "")
if(CMAKE_OSX_ARCHITECTURES)
list(LENGTH CMAKE_OSX_ARCHITECTURES _aifs_pdfium_arch_count)
if(_aifs_pdfium_arch_count GREATER 1)
set(_aifs_pdfium_missing_reason
"Embedded PDFium is vendored per-architecture. Configure a single macOS architecture instead of a universal build.")
else()
list(GET CMAKE_OSX_ARCHITECTURES 0 _aifs_pdfium_apple_arch)
endif()
endif()
if(NOT _aifs_pdfium_apple_arch)
set(_aifs_pdfium_apple_arch "${CMAKE_SYSTEM_PROCESSOR}")
endif()
string(TOLOWER "${_aifs_pdfium_apple_arch}" _aifs_pdfium_apple_arch)
if(_aifs_pdfium_apple_arch MATCHES "^(arm64|aarch64)$")
set(PDFIUM_PLATFORM_DIR "${PDFIUM_DIR}/macos-arm64")
set(PDFIUM_LIBRARY "${PDFIUM_PLATFORM_DIR}/lib/libpdfium.dylib")
set(PDFIUM_RUNTIME "${PDFIUM_LIBRARY}")
elseif(_aifs_pdfium_apple_arch MATCHES "^(x86_64|amd64)$")
set(PDFIUM_PLATFORM_DIR "${PDFIUM_DIR}/macos-x64")
set(PDFIUM_LIBRARY "${PDFIUM_PLATFORM_DIR}/lib/libpdfium.dylib")
set(PDFIUM_RUNTIME "${PDFIUM_LIBRARY}")
else()
set(_aifs_pdfium_missing_reason
"Embedded PDFium is not vendored for macOS architecture '${_aifs_pdfium_apple_arch}'.")
endif()
elseif(UNIX)
string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _aifs_pdfium_linux_arch)
if(_aifs_pdfium_linux_arch MATCHES "^(x86_64|amd64)$")
set(PDFIUM_PLATFORM_DIR "${PDFIUM_DIR}/linux-x64")
set(PDFIUM_LIBRARY "${PDFIUM_PLATFORM_DIR}/lib/libpdfium.so")
set(PDFIUM_RUNTIME "${PDFIUM_LIBRARY}")
else()
set(_aifs_pdfium_missing_reason
"Embedded PDFium is not vendored for Linux architecture '${CMAKE_SYSTEM_PROCESSOR}'.")
endif()
endif()
set(_aifs_pdfium_ready FALSE)
if(PDFIUM_PLATFORM_DIR
AND EXISTS "${PDFIUM_PLATFORM_DIR}/include/fpdfview.h"
AND EXISTS "${PDFIUM_LIBRARY}"
AND PDFIUM_RUNTIME
AND EXISTS "${PDFIUM_RUNTIME}")
set(_aifs_pdfium_ready TRUE)
endif()
if(_aifs_pdfium_ready)
add_compile_definitions(AI_FILE_SORTER_USE_PDFIUM)
list(APPEND DOC_DEPS_INCLUDE_DIRS "${PDFIUM_PLATFORM_DIR}/include")
add_library(pdfium SHARED IMPORTED)
if(WIN32)
set_target_properties(pdfium PROPERTIES
IMPORTED_IMPLIB "${PDFIUM_LIBRARY}"
IMPORTED_LOCATION "${PDFIUM_RUNTIME}"
)
else()
set_target_properties(pdfium PROPERTIES
IMPORTED_LOCATION "${PDFIUM_LIBRARY}"
)
endif()
list(APPEND DOC_DEPS_LIBS pdfium)
elseif(PDFIUM_PLATFORM_DIR OR _aifs_pdfium_missing_reason)
if(NOT _aifs_pdfium_missing_reason)
set(_aifs_pdfium_missing_reason
"Embedded PDFium is incomplete under '${PDFIUM_PLATFORM_DIR}'. Run app/scripts/vendor_doc_deps.sh or app/scripts/vendor_doc_deps.ps1 to populate the vendored runtime files.")
endif()
if(AI_FILE_SORTER_REQUIRE_EMBEDDED_PDF_BACKEND)
message(FATAL_ERROR "${_aifs_pdfium_missing_reason}")
else()
message(WARNING "${_aifs_pdfium_missing_reason} Falling back to 'pdftotext' when available.")
endif()
endif()
# Sources
set(APP_MAIN_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp")
file(GLOB APP_LIB_SOURCES CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/lib/*.cpp"
)
file(GLOB APP_HEADER_SOURCES CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/include/*.hpp"
)
set(APP_SOURCES ${APP_MAIN_SOURCE} ${APP_LIB_SOURCES})
set(AIFS_TRANSLATION_TS_FILES
"${CMAKE_CURRENT_SOURCE_DIR}/resources/i18n/aifilesorter_fr.ts"
"${CMAKE_CURRENT_SOURCE_DIR}/resources/i18n/aifilesorter_de.ts"
"${CMAKE_CURRENT_SOURCE_DIR}/resources/i18n/aifilesorter_it.ts"
"${CMAKE_CURRENT_SOURCE_DIR}/resources/i18n/aifilesorter_es.ts"
"${CMAKE_CURRENT_SOURCE_DIR}/resources/i18n/aifilesorter_nl.ts"
"${CMAKE_CURRENT_SOURCE_DIR}/resources/i18n/aifilesorter_tr.ts"
"${CMAKE_CURRENT_SOURCE_DIR}/resources/i18n/aifilesorter_ko.ts"
)
set(AIFS_TRANSLATION_QM_DIR "${CMAKE_CURRENT_BINARY_DIR}/i18n")
# Executable (GUI subsystem on Windows)
if(WIN32)
add_executable(aifilesorter WIN32 ${APP_SOURCES})
else()
add_executable(aifilesorter ${APP_SOURCES})
endif()
aifs_apply_update_mode(aifilesorter)
target_include_directories(aifilesorter PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/include"
"${CMAKE_CURRENT_SOURCE_DIR}/include/llama"
"${CMAKE_CURRENT_SOURCE_DIR}/include/external/llama.cpp/tools/mtmd"
${DOC_DEPS_INCLUDE_DIRS}
)
qt_add_lupdate(
NO_GLOBAL_TARGET
LUPDATE_TARGET update_translations
SOURCE_TARGETS aifilesorter
TS_FILES ${AIFS_TRANSLATION_TS_FILES}
SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/startapp_windows.cpp" ${APP_HEADER_SOURCES}
OPTIONS -no-obsolete
)
qt_add_lrelease(
LRELEASE_TARGET release_app_translations
TS_FILES ${AIFS_TRANSLATION_TS_FILES}
QM_OUTPUT_DIRECTORY "${AIFS_TRANSLATION_QM_DIR}"
QM_FILES_OUTPUT_VARIABLE AIFS_TRANSLATION_QM_FILES
)
set_source_files_properties(${AIFS_TRANSLATION_QM_FILES} PROPERTIES GENERATED TRUE)
function(aifs_add_translation_resources target_name)
qt_add_resources(${target_name} "${target_name}_translations"
PREFIX "/i18n"
BASE "${AIFS_TRANSLATION_QM_DIR}"
FILES ${AIFS_TRANSLATION_QM_FILES}
)
endfunction()
aifs_add_translation_resources(aifilesorter)
# Resources (equivalent to rcc generation in Makefile)
set(APP_RESOURCE_FILES
"${CMAKE_CURRENT_SOURCE_DIR}/resources/.env"
"${CMAKE_CURRENT_SOURCE_DIR}/resources/images/logo.png"
"${CMAKE_CURRENT_SOURCE_DIR}/resources/images/qn_logo.png"
"${CMAKE_CURRENT_SOURCE_DIR}/resources/images/app_icon_128.png"
"${CMAKE_CURRENT_SOURCE_DIR}/resources/images/icon_512x512.png"
"${CMAKE_CURRENT_SOURCE_DIR}/resources/certs/cacert.pem"
)
set_source_files_properties(
"${CMAKE_CURRENT_SOURCE_DIR}/resources/.env"
PROPERTIES QT_RESOURCE_ALIAS ".env"
)
set_source_files_properties(
"${CMAKE_CURRENT_SOURCE_DIR}/resources/images/logo.png"
PROPERTIES QT_RESOURCE_ALIAS "images/logo.png"
)
set_source_files_properties(
"${CMAKE_CURRENT_SOURCE_DIR}/resources/images/qn_logo.png"
PROPERTIES QT_RESOURCE_ALIAS "images/qn_logo.png"
)
set_source_files_properties(
"${CMAKE_CURRENT_SOURCE_DIR}/resources/images/app_icon_128.png"
PROPERTIES QT_RESOURCE_ALIAS "images/app_icon_128.png"
)
set_source_files_properties(
"${CMAKE_CURRENT_SOURCE_DIR}/resources/images/icon_512x512.png"
PROPERTIES QT_RESOURCE_ALIAS "images/icon_512x512.png"
)
set_source_files_properties(
"${CMAKE_CURRENT_SOURCE_DIR}/resources/certs/cacert.pem"
PROPERTIES QT_RESOURCE_ALIAS "certs/cacert.pem"
)
qt_add_resources(aifilesorter "app_resources"
PREFIX "/net/quicknode/AIFileSorter"
FILES ${APP_RESOURCE_FILES}
)
if(WIN32)
if(DEFINED ENV{WindowsSdkDir} AND DEFINED ENV{WindowsSDKLibVersion})
set(WIN_SDK_UM_DIR "$ENV{WindowsSdkDir}Lib/$ENV{WindowsSDKLibVersion}/um/x64")
set(WIN_SDK_UCRT_DIR "$ENV{WindowsSdkDir}Lib/$ENV{WindowsSDKLibVersion}/ucrt/x64")
if(EXISTS "${WIN_SDK_UM_DIR}")
link_directories("${WIN_SDK_UM_DIR}")
endif()
if(EXISTS "${WIN_SDK_UCRT_DIR}")
link_directories("${WIN_SDK_UCRT_DIR}")
endif()
endif()
set(PRECOMPILED_CPU_LIB_DIR "${CMAKE_CURRENT_SOURCE_DIR}/lib/precompiled/cpu/lib")
set(PRECOMPILED_CPU_BIN_DIR "${CMAKE_CURRENT_SOURCE_DIR}/lib/precompiled/cpu/bin")
set(LLAMA_CPU_IMPORT "${PRECOMPILED_CPU_LIB_DIR}/llama.lib")
set(LLAMA_CPU_DLL "${PRECOMPILED_CPU_BIN_DIR}/llama.dll")
if(NOT EXISTS "${LLAMA_CPU_IMPORT}")
message(FATAL_ERROR "Missing ${LLAMA_CPU_IMPORT}. Run app/scripts/build_llama_windows.ps1 cuda=off vcpkgroot= first.")
endif()
if(NOT EXISTS "${LLAMA_CPU_DLL}")
message(FATAL_ERROR "Missing ${LLAMA_CPU_DLL}. Run app/scripts/build_llama_windows.ps1 cuda=off vcpkgroot= to stage runtime DLLs.")
endif()
add_library(llama SHARED IMPORTED)
set_target_properties(llama PROPERTIES
IMPORTED_IMPLIB "${LLAMA_CPU_IMPORT}"
IMPORTED_LOCATION "${LLAMA_CPU_DLL}"
)
foreach(libName IN ITEMS ggml ggml-base ggml-cpu)
set(import_path "${PRECOMPILED_CPU_LIB_DIR}/${libName}.lib")
set(dll_path "${PRECOMPILED_CPU_BIN_DIR}/${libName}.dll")
if(NOT EXISTS "${import_path}")
message(FATAL_ERROR "Missing ${import_path}. Run app/scripts/build_llama_windows.ps1 cuda=off vcpkgroot= first.")
endif()
if(NOT EXISTS "${dll_path}")
message(FATAL_ERROR "Missing ${dll_path}. Run app/scripts/build_llama_windows.ps1 cuda=off vcpkgroot= to stage runtime DLLs.")
endif()
string(REPLACE "-" "_" imported_target "${libName}")
add_library(${imported_target} SHARED IMPORTED)
set_target_properties(${imported_target} PROPERTIES
IMPORTED_IMPLIB "${import_path}"
IMPORTED_LOCATION "${dll_path}"
)
endforeach()
set(MTMD_IMPORT "${PRECOMPILED_CPU_LIB_DIR}/mtmd.lib")
set(MTMD_DLL "${PRECOMPILED_CPU_BIN_DIR}/mtmd.dll")
if(EXISTS "${MTMD_IMPORT}" AND EXISTS "${MTMD_DLL}")
add_library(mtmd SHARED IMPORTED)
set_target_properties(mtmd PROPERTIES
IMPORTED_IMPLIB "${MTMD_IMPORT}"
IMPORTED_LOCATION "${MTMD_DLL}"
)
target_compile_definitions(aifilesorter PRIVATE AI_FILE_SORTER_HAS_MTMD)
set(_aifs_saved_required_libs "${CMAKE_REQUIRED_LIBRARIES}")
set(CMAKE_REQUIRED_LIBRARIES "${MTMD_IMPORT}")
check_cxx_source_compiles([[
extern "C" void mtmd_helper_set_progress_callback(void*, void*);
int main() {
mtmd_helper_set_progress_callback(0, 0);
return 0;
}
]] HAVE_MTMD_HELPER_SET_PROGRESS_CALLBACK)
check_cxx_source_compiles([[
extern "C" void mtmd_helper_log_set(void*, void*);
int main() {
mtmd_helper_log_set(0, 0);
return 0;
}
]] HAVE_MTMD_HELPER_LOG_SET)
set(CMAKE_REQUIRED_LIBRARIES "${_aifs_saved_required_libs}")
unset(_aifs_saved_required_libs)
if(HAVE_MTMD_HELPER_SET_PROGRESS_CALLBACK)
target_compile_definitions(aifilesorter PRIVATE AI_FILE_SORTER_MTMD_PROGRESS_CALLBACK)
endif()
if(HAVE_MTMD_HELPER_LOG_SET)
target_compile_definitions(aifilesorter PRIVATE AI_FILE_SORTER_MTMD_LOG_CALLBACK)
endif()
else()
message(WARNING "Visual LLM support disabled: missing mtmd.lib/mtmd.dll under app/lib/precompiled/cpu. Run app/scripts/build_llama_windows.ps1 to stage mtmd.")
endif()
else()
# Build llama.cpp from the included submodule for non-Windows platforms
set(LLAMA_BUILD_COMMON ON CACHE BOOL "llama: build common utils library" FORCE)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build shared libs" FORCE)
add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/include/external/llama.cpp" "${CMAKE_CURRENT_BINARY_DIR}/llama-build")
get_directory_property(_llama_install_version DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/llama-build" DEFINITION LLAMA_INSTALL_VERSION)
if (NOT _llama_install_version OR _llama_install_version STREQUAL "LLAMA_INSTALL_VERSION-NOTFOUND")
set(_llama_install_version "0.0.0")
endif()
set(LLAMA_INSTALL_VERSION "${_llama_install_version}")
add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/include/external/llama.cpp/tools/mtmd" "${CMAKE_CURRENT_BINARY_DIR}/mtmd-build")
target_link_libraries(aifilesorter PRIVATE llama)
target_link_libraries(aifilesorter PRIVATE mtmd)
target_compile_definitions(aifilesorter PRIVATE AI_FILE_SORTER_HAS_MTMD)
set(CMAKE_REQUIRED_INCLUDES
"${CMAKE_CURRENT_SOURCE_DIR}/include/external/llama.cpp/tools/mtmd"
"${CMAKE_CURRENT_SOURCE_DIR}/include/external/llama.cpp/include"
"${CMAKE_CURRENT_SOURCE_DIR}/include/external/llama.cpp/ggml/include"
)
check_symbol_exists(mtmd_helper_set_progress_callback "mtmd-helper.h" HAVE_MTMD_HELPER_SET_PROGRESS_CALLBACK)
check_symbol_exists(mtmd_helper_log_set "mtmd-helper.h" HAVE_MTMD_HELPER_LOG_SET)
set(CMAKE_REQUIRED_INCLUDES)
if(HAVE_MTMD_HELPER_SET_PROGRESS_CALLBACK)
target_compile_definitions(aifilesorter PRIVATE AI_FILE_SORTER_MTMD_PROGRESS_CALLBACK)
endif()
if(HAVE_MTMD_HELPER_LOG_SET)
target_compile_definitions(aifilesorter PRIVATE AI_FILE_SORTER_MTMD_LOG_CALLBACK)
endif()
if(MSVC)
target_compile_options(llama PRIVATE /Zc:char8_t-)
endif()
endif()
# Link libraries
target_link_libraries(aifilesorter PRIVATE
Qt6::Widgets Qt6::Gui Qt6::Core
CURL::libcurl
OpenSSL::SSL OpenSSL::Crypto
SQLite::SQLite3
JsonCpp::JsonCpp
fmt::fmt
spdlog::spdlog
Intl::Intl
llama # imported on Windows or built from submodule on other platforms
${MEDIAINFO_DEPS_LIBS}
${DOC_DEPS_LIBS}
)
if(WIN32)
target_link_libraries(aifilesorter PRIVATE ggml ggml_base ggml_cpu)
if(TARGET mtmd)
target_link_libraries(aifilesorter PRIVATE mtmd)
endif()
endif()
if(_aifs_pdfium_ready)
if(PDFIUM_RUNTIME AND EXISTS "${PDFIUM_RUNTIME}")
add_custom_command(TARGET aifilesorter POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${PDFIUM_RUNTIME}"
"$"
)
endif()
if(NOT WIN32)
if(APPLE)
set(_pdfium_rpath "@loader_path")
else()
set(_pdfium_rpath "$ORIGIN")
endif()
set_target_properties(aifilesorter PROPERTIES
BUILD_RPATH "${_pdfium_rpath}"
INSTALL_RPATH "${_pdfium_rpath}"
)
unset(_pdfium_rpath)
endif()
endif()
if(WIN32)
set(STARTER_TARGET StartAiFileSorter)
add_executable(${STARTER_TARGET} WIN32
"${CMAKE_CURRENT_SOURCE_DIR}/startapp_windows.cpp"
)
aifs_apply_update_mode(${STARTER_TARGET})
target_include_directories(${STARTER_TARGET} PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/include"
)
target_link_libraries(${STARTER_TARGET} PRIVATE
Qt6::Widgets Qt6::Gui Qt6::Core
)
target_compile_definitions(${STARTER_TARGET} PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX)
endif()
# On Windows, ensure Unicode and lean Windows headers in deps that need it
if(WIN32)
target_compile_definitions(aifilesorter PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX)
set(WIN_SYSTEM_LIBS wininet d3d11 dxgi dxguid d3d12 mpr userenv)
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(_win_sdk_arch "x64")
else()
set(_win_sdk_arch "x86")
endif()
set(_win_sdk_lib_paths "")
if(DEFINED ENV{WindowsSdkDir} AND DEFINED ENV{WindowsSDKLibVersion})
file(TO_CMAKE_PATH "$ENV{WindowsSdkDir}" _sdk_dir)
file(TO_CMAKE_PATH "$ENV{WindowsSDKLibVersion}" _sdk_version_raw)
string(REGEX REPLACE "/$" "" _sdk_version "${_sdk_version_raw}")
set(_sdk_root "${_sdk_dir}Lib/${_sdk_version}")
foreach(_subdir IN ITEMS um ucrt)
set(_candidate "${_sdk_root}/${_subdir}/${_win_sdk_arch}")
if(EXISTS "${_candidate}")
list(APPEND _win_sdk_lib_paths "${_candidate}")
endif()
endforeach()
endif()
if(NOT _win_sdk_lib_paths AND DEFINED CMAKE_RC_COMPILER AND CMAKE_RC_COMPILER)
get_filename_component(_rc_dir "${CMAKE_RC_COMPILER}" DIRECTORY)
get_filename_component(_bin_version_dir "${_rc_dir}" DIRECTORY)
get_filename_component(_bin_dir "${_bin_version_dir}" DIRECTORY)
get_filename_component(_kits_root "${_bin_dir}" DIRECTORY)
get_filename_component(_sdk_version "${_bin_version_dir}" NAME)
if(_kits_root AND EXISTS "${_kits_root}/Lib/${_sdk_version}")
set(_sdk_root "${_kits_root}/Lib/${_sdk_version}")
foreach(_subdir IN ITEMS um ucrt)
set(_candidate "${_sdk_root}/${_subdir}/${_win_sdk_arch}")
if(EXISTS "${_candidate}")
list(APPEND _win_sdk_lib_paths "${_candidate}")
endif()
endforeach()
endif()
endif()
foreach(libName IN LISTS WIN_SYSTEM_LIBS)
string(TOUPPER "${libName}" upperLib)
set(varName "${upperLib}_LIBRARY")
unset(${varName})
if(_win_sdk_lib_paths)
find_library(${varName} NAMES ${libName}
PATHS ${_win_sdk_lib_paths}
NO_DEFAULT_PATH
)
endif()
if(NOT ${varName})
find_library(${varName} NAMES ${libName})
endif()
if(NOT ${varName})
message(FATAL_ERROR "Required system library '${libName}' not found. Ensure the Windows SDK is installed.")
endif()
target_link_libraries(aifilesorter PRIVATE "${${varName}}")
endforeach()
endif()
if(WIN32)
find_program(POWERSHELL_EXECUTABLE NAMES pwsh pwsh.exe powershell powershell.exe)
if(NOT POWERSHELL_EXECUTABLE)
message(FATAL_ERROR "PowerShell is required to generate Windows resources")
endif()
set(APP_ICON_BASE "${CMAKE_CURRENT_SOURCE_DIR}/resources/images/icon_256x256.png")
set(APP_ICON_ICO "${CMAKE_CURRENT_BINARY_DIR}/generated/app_icon.ico")
add_custom_command(OUTPUT "${APP_ICON_ICO}"
COMMAND "${POWERSHELL_EXECUTABLE}" -NoProfile -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_icon.ps1" -BasePng "${APP_ICON_BASE}" -Ico "${APP_ICON_ICO}"
DEPENDS "${APP_ICON_BASE}" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_icon.ps1"
COMMENT "Generating Windows icon"
VERBATIM
)
set(APP_ICON_RC "${CMAKE_CURRENT_BINARY_DIR}/generated/app_icon.rc")
file(TO_NATIVE_PATH "${APP_ICON_ICO}" APP_ICON_ICO_NATIVE_TEMP)
string(REPLACE "\\" "\\\\" APP_ICON_ICO_NATIVE "${APP_ICON_ICO_NATIVE_TEMP}")
set(APP_ICON_PATH "${APP_ICON_ICO_NATIVE}")
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/resources/windows/app_icon.rc.in"
"${APP_ICON_RC}"
@ONLY
)
set_source_files_properties("${APP_ICON_ICO}" PROPERTIES GENERATED TRUE)
add_custom_target(app_icon_resource ALL DEPENDS "${APP_ICON_ICO}")
add_dependencies(aifilesorter app_icon_resource)
add_dependencies(aifilesorter app_icon_resource)
if(WIN32)
add_dependencies(${STARTER_TARGET} app_icon_resource)
endif()
target_sources(aifilesorter PRIVATE "${APP_ICON_RC}")
target_sources(aifilesorter PRIVATE "${APP_ICON_RC}")
if(WIN32)
target_sources(${STARTER_TARGET} PRIVATE "${APP_ICON_RC}")
endif()
# Version info resource generated from app_version.hpp
set(APP_VERSION_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/include/app_version.hpp")
file(READ "${APP_VERSION_HEADER}" APP_VERSION_CONTENTS)
string(REGEX MATCH "Version\\{([0-9]+),[ \t]*([0-9]+),[ \t]*([0-9]+)" _ "${APP_VERSION_CONTENTS}")
if(CMAKE_MATCH_1)
set(APP_VER_MAJOR "${CMAKE_MATCH_1}")
set(APP_VER_MINOR "${CMAKE_MATCH_2}")
set(APP_VER_PATCH "${CMAKE_MATCH_3}")
else()
set(APP_VER_MAJOR "0")
set(APP_VER_MINOR "0")
set(APP_VER_PATCH "0")
endif()
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/resources/windows/version.rc.in"
"${CMAKE_CURRENT_BINARY_DIR}/generated/version.rc"
@ONLY
)
target_sources(aifilesorter PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated/version.rc")
target_sources(${STARTER_TARGET} PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated/version.rc")
set(PRECOMPILED_CPU_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/lib/precompiled/cpu")
set(PRECOMPILED_CUDA_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/lib/precompiled/cuda")
set(PRECOMPILED_VULKAN_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/lib/precompiled/vulkan")
add_custom_command(TARGET aifilesorter POST_BUILD
# COMMAND ${CMAKE_COMMAND} -E remove -f "$/openblas.dll"
COMMAND ${CMAKE_COMMAND} -E make_directory "$/lib/ggml/wocuda"
COMMAND ${CMAKE_COMMAND} -E make_directory "$/lib/ggml/wcuda"
COMMAND ${CMAKE_COMMAND} -E make_directory "$/lib/ggml/wvulkan"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${PRECOMPILED_CPU_ROOT}"
"$/lib/ggml/wocuda"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${PRECOMPILED_CUDA_ROOT}"
"$/lib/ggml/wcuda"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${PRECOMPILED_VULKAN_ROOT}"
"$/lib/ggml/wvulkan"
)
# set(_precompiled_libopenblas "${PRECOMPILED_CPU_ROOT}/bin/libopenblas.dll")
# if(EXISTS "${_precompiled_libopenblas}")
# add_custom_command(TARGET aifilesorter POST_BUILD
# COMMAND ${CMAKE_COMMAND} -E copy_if_different
# "${_precompiled_libopenblas}"
# "$/libopenblas.dll"
# )
# else()
# message(WARNING "Expected ${_precompiled_libopenblas} to exist. Run app/scripts/build_llama_windows.ps1 to stage GGML artifacts.")
# endif()
endif()
if(AI_FILE_SORTER_BUILD_TESTS)
include(CTest)
set(_catch2_source_dir "${CMAKE_SOURCE_DIR}/../external/Catch2")
if(EXISTS "${_catch2_source_dir}/CMakeLists.txt")
add_subdirectory(${_catch2_source_dir} ${CMAKE_BINARY_DIR}/catch2-build)
else()
message(STATUS "Catch2 submodule not found, fetching via FetchContent")
include(FetchContent)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.5.2
)
FetchContent_MakeAvailable(Catch2)
endif()
function(aifs_stage_test_runtime target_name)
if(WIN32)
set(_aifs_test_runtime_dlls "")
add_custom_command(TARGET ${target_name} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$
$
COMMAND_EXPAND_LISTS
)
if(VCPKG_TARGET_TRIPLET)
set(_aifs_vcpkg_runtime_dir "${AIFS_VCPKG_INSTALLED_ROOT}/${VCPKG_TARGET_TRIPLET}/bin")
if(EXISTS "${_aifs_vcpkg_runtime_dir}")
file(GLOB _aifs_vcpkg_runtime_dlls "${_aifs_vcpkg_runtime_dir}/*.dll")
list(APPEND _aifs_test_runtime_dlls ${_aifs_vcpkg_runtime_dlls})
endif()
endif()
if(EXISTS "${PRECOMPILED_CPU_ROOT}/bin")
file(GLOB _aifs_precompiled_cpu_runtime_dlls
"${PRECOMPILED_CPU_ROOT}/bin/*.dll")
list(APPEND _aifs_test_runtime_dlls ${_aifs_precompiled_cpu_runtime_dlls})
endif()
set(_aifs_mingw_runtime_search_paths "")
if(DEFINED ENV{OPENBLAS_ROOT} AND NOT "$ENV{OPENBLAS_ROOT}" STREQUAL "")
list(APPEND _aifs_mingw_runtime_search_paths "$ENV{OPENBLAS_ROOT}/bin")
endif()
list(APPEND _aifs_mingw_runtime_search_paths "C:/msys64/mingw64/bin")
foreach(_aifs_mingw_runtime_name IN ITEMS
libgomp-1.dll
libgcc_s_seh-1.dll
libgfortran-5.dll
libwinpthread-1.dll
libquadmath-0.dll)
foreach(_aifs_runtime_path IN LISTS _aifs_mingw_runtime_search_paths)
if(EXISTS "${_aifs_runtime_path}/${_aifs_mingw_runtime_name}")
list(APPEND _aifs_test_runtime_dlls
"${_aifs_runtime_path}/${_aifs_mingw_runtime_name}")
break()
endif()
endforeach()
endforeach()
list(REMOVE_DUPLICATES _aifs_test_runtime_dlls)
if(_aifs_test_runtime_dlls)
add_custom_command(TARGET ${target_name} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${_aifs_test_runtime_dlls}
$
COMMAND_EXPAND_LISTS
)
endif()
if(VCPKG_TARGET_TRIPLET)
set(_aifs_qt_plugin_root "${AIFS_VCPKG_INSTALLED_ROOT}/${VCPKG_TARGET_TRIPLET}/Qt6/plugins")
foreach(_aifs_qt_plugin_subdir IN ITEMS platforms styles imageformats)
set(_aifs_qt_plugin_src_dir "${_aifs_qt_plugin_root}/${_aifs_qt_plugin_subdir}")
if(EXISTS "${_aifs_qt_plugin_src_dir}")
file(GLOB _aifs_qt_plugin_dlls "${_aifs_qt_plugin_src_dir}/*.dll")
if(_aifs_qt_plugin_dlls)
add_custom_command(TARGET ${target_name} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory
"$/${_aifs_qt_plugin_subdir}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${_aifs_qt_plugin_dlls}
"$/${_aifs_qt_plugin_subdir}"
COMMAND_EXPAND_LISTS
)
endif()
endif()
endforeach()
endif()
endif()
if(_aifs_pdfium_ready AND PDFIUM_RUNTIME AND EXISTS "${PDFIUM_RUNTIME}")
add_custom_command(TARGET ${target_name} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${PDFIUM_RUNTIME}"
"$"
)
endif()
if(_aifs_pdfium_ready AND NOT WIN32)
if(APPLE)
set(_pdfium_rpath "@loader_path")
else()
set(_pdfium_rpath "$ORIGIN")
endif()
set_target_properties(${target_name} PROPERTIES
BUILD_RPATH "${_pdfium_rpath}"
INSTALL_RPATH "${_pdfium_rpath}"
)
unset(_pdfium_rpath)
endif()
endfunction()
add_executable(ai_file_sorter_tests
${APP_LIB_SOURCES}
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_utils.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_file_scanner.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_local_llm_backend.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_ggml_runtime_paths.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_llm_downloader.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_update_feed.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_updater.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_updater_build_modes.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_llm_selection_dialog_visual.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_main_app_translation.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_main_app_image_options.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_main_app_visual_fallback.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_settings_image_options.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_ui_translator.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_categorization_dialog.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_checkbox_matrix.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_review_dialog_rename_gate.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_cli_reporter.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_support_prompt.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_whitelist_and_prompt.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_database_manager_rename_only.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_cache_interactions.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_image_rename_metadata_service.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_llava_image_analyzer.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_media_rename_metadata_service.cpp"
)
target_include_directories(ai_file_sorter_tests PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/include"
"${CMAKE_CURRENT_SOURCE_DIR}/include/llama"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit"
${DOC_DEPS_INCLUDE_DIRS}
)
target_link_libraries(ai_file_sorter_tests PRIVATE
Catch2::Catch2WithMain
Qt6::Core
Qt6::Widgets
CURL::libcurl
OpenSSL::SSL OpenSSL::Crypto
SQLite::SQLite3
JsonCpp::JsonCpp
spdlog::spdlog
fmt::fmt
Intl::Intl
llama
${MEDIAINFO_DEPS_LIBS}
${DOC_DEPS_LIBS}
)
aifs_add_translation_resources(ai_file_sorter_tests)
target_compile_definitions(ai_file_sorter_tests PRIVATE AI_FILE_SORTER_TEST_BUILD=1 WIN32_LEAN_AND_MEAN NOMINMAX)
aifs_apply_update_mode(ai_file_sorter_tests)
aifs_apply_expected_update_mode(ai_file_sorter_tests "${_aifs_effective_update_mode}")
if(WIN32)
target_link_libraries(ai_file_sorter_tests PRIVATE ggml ggml_base ggml_cpu)
foreach(libName IN LISTS WIN_SYSTEM_LIBS)
string(TOUPPER "${libName}" upperLib)
set(varName "${upperLib}_LIBRARY")
if(DEFINED ${varName} AND ${varName})
target_link_libraries(ai_file_sorter_tests PRIVATE "${${varName}}")
else()
target_link_libraries(ai_file_sorter_tests PRIVATE ${libName})
endif()
endforeach()
endif()
aifs_stage_test_runtime(ai_file_sorter_tests)
if(WIN32)
set(AIFS_UPDATER_MODE_TEST_APP_SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/lib/IniConfig.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/lib/Logger.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/lib/Settings.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/lib/UpdateArchiveExtractor.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/lib/UpdateFeed.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/lib/UpdateInstaller.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/lib/Updater.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/lib/Utils.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/lib/Version.cpp"
)
function(aifs_add_updater_mode_test target_name update_mode)
add_executable(${target_name}
${AIFS_UPDATER_MODE_TEST_APP_SOURCES}
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit/test_updater_build_modes.cpp"
)
target_include_directories(${target_name} PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/include"
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/unit"
${DOC_DEPS_INCLUDE_DIRS}
)
target_link_libraries(${target_name} PRIVATE
Catch2::Catch2WithMain
Qt6::Core
Qt6::Widgets
CURL::libcurl
OpenSSL::SSL OpenSSL::Crypto
SQLite::SQLite3
JsonCpp::JsonCpp
spdlog::spdlog
fmt::fmt
Intl::Intl
${DOC_DEPS_LIBS}
)
target_compile_definitions(${target_name} PRIVATE AI_FILE_SORTER_TEST_BUILD=1 WIN32_LEAN_AND_MEAN NOMINMAX)
foreach(libName IN LISTS WIN_SYSTEM_LIBS)
string(TOUPPER "${libName}" upperLib)
set(varName "${upperLib}_LIBRARY")
if(DEFINED ${varName} AND ${varName})
target_link_libraries(${target_name} PRIVATE "${${varName}}")
else()
target_link_libraries(${target_name} PRIVATE ${libName})
endif()
endforeach()
aifs_apply_named_update_mode(${target_name} "${update_mode}")
aifs_apply_expected_update_mode(${target_name} "${update_mode}")
aifs_stage_test_runtime(${target_name})
endfunction()
aifs_add_updater_mode_test(ai_file_sorter_updater_notify_only_tests "NOTIFY_ONLY")
aifs_add_updater_mode_test(ai_file_sorter_updater_disabled_tests "DISABLED")
endif()
if(BUILD_TESTING)
add_test(NAME ai_file_sorter_tests COMMAND ai_file_sorter_tests)
if(WIN32)
add_test(NAME ai_file_sorter_updater_notify_only_tests COMMAND ai_file_sorter_updater_notify_only_tests)
add_test(NAME ai_file_sorter_updater_disabled_tests COMMAND ai_file_sorter_updater_disabled_tests)
endif()
endif()
if(NOT WIN32)
add_test(
NAME integration_run_all
COMMAND ${CMAKE_COMMAND} -E env bash ${CMAKE_SOURCE_DIR}/../tests/run_all_tests.sh
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/..
)
set_tests_properties(integration_run_all PROPERTIES
PASS_REGULAR_EXPRESSION "All tests completed successfully."
)
endif()
endif()
================================================
FILE: app/Makefile
================================================
# Detect platform
UNAME := $(shell uname | cut -d'-' -f1)
UNAME_M := $(shell uname -m)
BIN_DIR := ./bin
OBJ_ROOT := ./obj
OBJ_DIR := $(OBJ_ROOT)
SRC_DIR := ./lib
APP_NAME ?= AI File Sorter
# Goals that should not require toolchain/dependency discovery.
NON_BUILD_GOALS := clean
NEEDS_BUILD_DEPS := 1
ifneq ($(strip $(MAKECMDGOALS)),)
ifeq ($(strip $(filter-out $(NON_BUILD_GOALS),$(MAKECMDGOALS))),)
NEEDS_BUILD_DEPS := 0
endif
endif
# Optional: build llama.cpp with multi-variant CPU backends on macOS.
# Usage: make LLAMA_MULTI_VARIANT=1
LLAMA_MULTI_VARIANT ?= 0
GGML_PRECOMPILED_SUBDIR ?= precompiled
ifeq ($(UNAME), Linux)
PLATFORM := Linux
CXXFLAGS += -DLINUX
CXXFLAGS += -DAI_FILE_SORTER_HAS_MTMD
TARGET := $(BIN_DIR)/aifilesorter-bin
INSTALL_DIR := /usr/local/bin
INSTALL_LIB_DIR := /usr/local/lib/aifilesorter
LD_CONF_FILE := /etc/ld.so.conf.d/aifilesorter.conf
# --- Qt6 setup ---
PKG_CONFIG := $(shell command -v pkg-config 2>/dev/null)
QT_PACKAGES := Qt6Widgets Qt6Gui Qt6Core
QT_CXXFLAGS :=
QT_LDFLAGS :=
ifneq ($(strip $(PKG_CONFIG)),)
QT_CXXFLAGS := $(shell $(PKG_CONFIG) --cflags $(QT_PACKAGES) 2>/dev/null)
QT_LDFLAGS := $(shell $(PKG_CONFIG) --libs $(QT_PACKAGES) 2>/dev/null)
endif
ifeq ($(strip $(QT_CXXFLAGS)),)
QT_INCLUDE_BASE ?= /usr/include/x86_64-linux-gnu/qt6
QT_LIB_BASE ?= /usr/lib/x86_64-linux-gnu
CXXFLAGS += -I$(QT_INCLUDE_BASE) -I$(QT_INCLUDE_BASE)/QtCore -I$(QT_INCLUDE_BASE)/QtGui -I$(QT_INCLUDE_BASE)/QtWidgets
LDFLAGS += -L$(QT_LIB_BASE) -lQt6Widgets -lQt6Gui -lQt6Core
else
CXXFLAGS += $(QT_CXXFLAGS)
LDFLAGS += $(QT_LDFLAGS)
endif
# --- Other dependencies ---
LDFLAGS += -lcurl -ljsoncpp -lsqlite3 -lcrypto -lfmt -lspdlog -lssl -lllama -lggml -lggml-base -lmtmd -lX11 -pthread
LDFLAGS += -Wl,-rpath,'$$ORIGIN/../lib/precompiled/cpu/bin' -Wl,-rpath,'$$ORIGIN/../lib/precompiled'
LDFLAGS += -Wl,-rpath-link=./lib/precompiled/cpu/bin -Wl,-rpath-link=./lib/precompiled
else ifeq ($(UNAME), Darwin)
MACOS_ARCH ?= $(UNAME_M)
DEFAULT_BREW_PREFIX := $(shell if [ "$(MACOS_ARCH)" = "arm64" ]; then echo /opt/homebrew; else echo /usr/local; fi)
ifeq ($(origin BREW_PREFIX),undefined)
ifeq ($(MACOS_ARCH),x86_64)
BREW_PREFIX := /usr/local
else
BREW_PREFIX := $(shell if command -v brew >/dev/null 2>&1; then brew --prefix; else echo $(DEFAULT_BREW_PREFIX); fi)
endif
endif
QT_PREFIX := $(BREW_PREFIX)/opt/qt
QTBASE_PREFIX := $(BREW_PREFIX)/opt/qtbase
CURL_PREFIX := $(BREW_PREFIX)/opt/curl
LIBFFI_PREFIX := $(BREW_PREFIX)/opt/libffi
EXPAT_PREFIX := $(BREW_PREFIX)/opt/expat
SDKROOT := $(shell xcrun --sdk macosx --show-sdk-path 2>/dev/null)
MACOSX_DEPLOYMENT_TARGET ?= 11.0
HOMEBREW_OS_PKG_CONFIG_DIRS := $(shell if [ -d "$(BREW_PREFIX)/Library/Homebrew/os/mac/pkgconfig" ]; then find "$(BREW_PREFIX)/Library/Homebrew/os/mac/pkgconfig" -mindepth 1 -maxdepth 1 -type d | tr '\n' ':' | sed 's/:$$//'; fi)
# Homebrew exposes system pkg-config metadata like libcurl.pc here; libmediainfo depends on it.
MACOS_PKG_CONFIG_DIRS := $(BREW_PREFIX)/lib/pkgconfig:$(BREW_PREFIX)/share/pkgconfig:$(LIBFFI_PREFIX)/lib/pkgconfig:$(EXPAT_PREFIX)/lib/pkgconfig:$(QT_PREFIX)/lib/pkgconfig$(if $(strip $(HOMEBREW_OS_PKG_CONFIG_DIRS)),:$(HOMEBREW_OS_PKG_CONFIG_DIRS))
export MACOSX_DEPLOYMENT_TARGET
PATH := $(QTBASE_PREFIX)/share/qt/libexec:$(QT_PREFIX)/bin:$(CURL_PREFIX)/bin:$(PATH)
export PATH
ifeq ($(MACOS_ARCH),x86_64)
export PKG_CONFIG_PATH := $(MACOS_PKG_CONFIG_DIRS)
export PKG_CONFIG_LIBDIR := $(MACOS_PKG_CONFIG_DIRS)
CPPFLAGS := $(filter-out -I/opt/homebrew/%,$(CPPFLAGS))
LDFLAGS := $(filter-out -L/opt/homebrew/% -F/opt/homebrew/%,$(LDFLAGS))
else
export PKG_CONFIG_PATH := $(MACOS_PKG_CONFIG_DIRS):$(PKG_CONFIG_PATH)
export PKG_CONFIG_LIBDIR := $(MACOS_PKG_CONFIG_DIRS)
endif
export LDFLAGS += -L$(LIBFFI_PREFIX)/lib
export CPPFLAGS += -I$(LIBFFI_PREFIX)/include
PLATFORM := MacOS
CXXFLAGS += -DMACOS -DENABLE_METAL -DGGML_USE_METAL -DAI_FILE_SORTER_HAS_MTMD -Wno-deprecated -Iinclude/llama -mmacosx-version-min=$(MACOSX_DEPLOYMENT_TARGET)
ifneq ($(strip $(MACOS_ARCH)),)
CXXFLAGS := $(filter-out -arch arm64 -arch x86_64,$(CXXFLAGS))
LDFLAGS := $(filter-out -arch arm64 -arch x86_64,$(LDFLAGS))
CXXFLAGS += -arch $(MACOS_ARCH)
LDFLAGS += -arch $(MACOS_ARCH)
endif
ifneq ($(strip $(SDKROOT)),)
export SDKROOT
CXXFLAGS += -isysroot $(SDKROOT) -stdlib=libc++ -I$(SDKROOT)/usr/include/c++/v1
LDFLAGS += -isysroot $(SDKROOT)
endif
LDFLAGS += -mmacosx-version-min=$(MACOSX_DEPLOYMENT_TARGET)
TARGET := $(BIN_DIR)/aifilesorter
INSTALL_DIR := /usr/local/bin
INSTALL_LIB_DIR := /usr/local/lib/aifilesorter
SPDLOG_PATH := $(BREW_PREFIX)/include
CXXFLAGS += -I$(SPDLOG_PATH)
ifeq ($(origin PKG_CONFIG),undefined)
ifeq ($(MACOS_ARCH),x86_64)
ifneq ($(wildcard /usr/local/bin/pkg-config),)
PKG_CONFIG := /usr/local/bin/pkg-config
else
PKG_CONFIG := $(shell command -v pkg-config 2>/dev/null)
endif
else
PKG_CONFIG := $(shell command -v pkg-config 2>/dev/null)
endif
endif
ifeq ($(strip $(PKG_CONFIG)),)
ifeq ($(NEEDS_BUILD_DEPS),1)
$(error pkg-config is required to locate Qt6 frameworks on macOS. Please install it (e.g. brew install pkg-config))
endif
endif
QT_PACKAGES := Qt6Widgets Qt6Gui Qt6Core
QT_CXXFLAGS := $(shell PKG_CONFIG_PATH="$(PKG_CONFIG_PATH)" $(PKG_CONFIG) --cflags $(QT_PACKAGES))
QT_LDFLAGS := $(shell PKG_CONFIG_PATH="$(PKG_CONFIG_PATH)" $(PKG_CONFIG) --libs $(QT_PACKAGES))
ifeq ($(strip $(QT_CXXFLAGS)),)
ifeq ($(NEEDS_BUILD_DEPS),1)
$(error Could not retrieve Qt6 flags via pkg-config. Ensure Qt6 is installed (brew install qt) and PKG_CONFIG_PATH includes its pkgconfig directory.)
endif
endif
ifeq ($(MACOS_ARCH),x86_64)
ifneq ($(findstring /opt/homebrew,$(QT_CXXFLAGS) $(QT_LDFLAGS)),)
ifeq ($(NEEDS_BUILD_DEPS),1)
$(error x86_64 build resolved Qt flags from /opt/homebrew. Install x86_64 Qt under /usr/local (Intel Homebrew) or set BREW_PREFIX/PKG_CONFIG accordingly.)
endif
endif
endif
CXXFLAGS += $(QT_CXXFLAGS)
LDFLAGS += $(QT_LDFLAGS)
LDFLAGS += -lcurl -ljsoncpp -lsqlite3 -lcrypto -lfmt -lspdlog -lssl -lllama -lggml -lggml-base -lmtmd -lintl -pthread
LDFLAGS += -framework Metal -framework Foundation
LDFLAGS += -Wl,-rpath,@loader_path/lib -Wl,-rpath,@loader_path/../lib/$(GGML_PRECOMPILED_SUBDIR)
BIN_SUBDIR := $(patsubst ./bin/%,%,$(BIN_DIR))
ifneq ($(BIN_SUBDIR),$(BIN_DIR))
ifneq ($(strip $(BIN_SUBDIR)),)
LDFLAGS += -Wl,-rpath,@loader_path/../../lib -Wl,-rpath,@loader_path/../../lib/$(GGML_PRECOMPILED_SUBDIR)
endif
endif
endif
ifeq ($(UNAME), Darwin)
ifneq ($(strip $(MACOS_ARCH)),)
BIN_SUBDIR := $(patsubst ./bin/%,%,$(BIN_DIR))
OBJ_VARIANT := $(GGML_PRECOMPILED_SUBDIR)
ifneq ($(BIN_SUBDIR),$(BIN_DIR))
ifneq ($(strip $(BIN_SUBDIR)),)
OBJ_VARIANT := $(BIN_SUBDIR)
endif
endif
OBJ_DIR := $(OBJ_ROOT)/$(MACOS_ARCH)/$(OBJ_VARIANT)
endif
endif
# MediaInfo policy:
# - Must come from package managers (apt/dnf/pacman/brew/vcpkg)
# - Vendored submodules / checked-in binaries are rejected
MEDIAINFO_REQUIRE ?= 1
MEDIAINFO_ALLOW_VENDORED ?= 0
MEDIAINFO_PKG ?= libmediainfo
MEDIAINFO_CFLAGS :=
MEDIAINFO_LDFLAGS :=
ifeq ($(NEEDS_BUILD_DEPS),1)
MEDIAINFO_BLOCKED_PATHS := ../external/MediaInfoLib ../external/libmediainfo ./lib/mediainfo ./include/MediaInfo
ifneq ($(MEDIAINFO_ALLOW_VENDORED),1)
ifneq ($(strip $(foreach p,$(MEDIAINFO_BLOCKED_PATHS),$(wildcard $(p)))),)
$(error Vendored MediaInfo content detected ($(MEDIAINFO_BLOCKED_PATHS)). Use package-managed libmediainfo only.)
endif
endif
ifeq ($(strip $(PKG_CONFIG)),)
ifeq ($(MEDIAINFO_REQUIRE),1)
$(error pkg-config is required to resolve package-managed libmediainfo. Install pkg-config and libmediainfo (apt/dnf/pacman/brew/vcpkg).)
endif
else
MEDIAINFO_CFLAGS := $(shell PKG_CONFIG_PATH="$(PKG_CONFIG_PATH)" $(PKG_CONFIG) --cflags $(MEDIAINFO_PKG) 2>/dev/null)
MEDIAINFO_LDFLAGS := $(shell PKG_CONFIG_PATH="$(PKG_CONFIG_PATH)" $(PKG_CONFIG) --libs $(MEDIAINFO_PKG) 2>/dev/null)
endif
ifeq ($(MEDIAINFO_REQUIRE),1)
ifeq ($(strip $(MEDIAINFO_LDFLAGS)),)
$(error libmediainfo not found via pkg-config. Install it through apt/dnf/pacman/brew/vcpkg. Vendored copies are not supported.)
endif
endif
endif
ifneq ($(strip $(MEDIAINFO_LDFLAGS)),)
CXXFLAGS += -DAI_FILE_SORTER_USE_MEDIAINFOLIB $(MEDIAINFO_CFLAGS)
LDFLAGS += $(MEDIAINFO_LDFLAGS)
endif
WRAPPED_BINARY := $(notdir $(TARGET))
# Compiler and flags
ifeq ($(UNAME), Darwin)
CXX := clang++
else
CXX := g++
endif
CXXFLAGS += -std=c++20 -Wall -O2 -fPIC '-DAI_FILE_SORTER_APP_NAME="$(APP_NAME)"' '-DAI_FILE_SORTER_GGML_SUBDIR="$(GGML_PRECOMPILED_SUBDIR)"'
CXX_VERSION_INFO := $(shell $(CXX) --version 2>/dev/null)
# Suppress GCC-only diagnostics; clang (macOS) does not support some of them
ifneq (,$(findstring clang,$(CXX_VERSION_INFO)))
CXXFLAGS += -Wno-array-bounds
else
CXXFLAGS += -Wno-array-bounds -Wno-stringop-overflow -Wno-stringop-overread
endif
INCLUDE_DIRS = -I./include -I./include/external/llama.cpp/include -I./include/external/llama.cpp/ggml/include -I./include/external/llama.cpp/tools/mtmd
LIB_DIRS =
ifeq ($(UNAME), Linux)
LIB_DIRS += -L./lib/precompiled/cpu/bin -L./lib/precompiled
else
LIB_DIRS += -L./lib/$(GGML_PRECOMPILED_SUBDIR)
endif
ifeq ($(UNAME), Darwin)
LIB_DIRS += -L$(BREW_PREFIX)/lib
endif
# --- Vendored document analysis dependencies ---
DOC_DEPS_DIR := ../external
LIBZIP_DIR := $(DOC_DEPS_DIR)/libzip
PUGIXML_DIR := $(DOC_DEPS_DIR)/pugixml
PDFIUM_DIR := $(DOC_DEPS_DIR)/pdfium
LIBZIP_CMAKE := $(LIBZIP_DIR)/CMakeLists.txt
LIBZIP_BUILD_DIR := $(LIBZIP_DIR)/build
LIBZIP_LIB := $(LIBZIP_BUILD_DIR)/lib/libzip.a
LIBZIP_CONF := $(LIBZIP_BUILD_DIR)/zipconf.h
LIBZIP_CMAKE_ARGS :=
ifeq ($(UNAME), Darwin)
ifneq ($(strip $(MACOS_ARCH)),)
LIBZIP_BUILD_DIR := $(LIBZIP_DIR)/build-$(MACOS_ARCH)
LIBZIP_LIB := $(LIBZIP_BUILD_DIR)/lib/libzip.a
LIBZIP_CMAKE_ARGS += -DCMAKE_OSX_ARCHITECTURES=$(MACOS_ARCH)
endif
endif
PUGIXML_HDR := $(PUGIXML_DIR)/src/pugixml.hpp
PDFIUM_PLATFORM_DIR :=
PDFIUM_INC :=
PDFIUM_LIB :=
PDFIUM_STAGED_LIB :=
PDFIUM_STAGED_STAMP :=
ifeq ($(UNAME), Linux)
PDFIUM_PLATFORM_DIR := $(PDFIUM_DIR)/linux-x64
PDFIUM_INC := $(PDFIUM_PLATFORM_DIR)/include
PDFIUM_LIB := $(PDFIUM_PLATFORM_DIR)/lib/libpdfium.so
PDFIUM_STAGED_LIB := ./lib/$(GGML_PRECOMPILED_SUBDIR)/libpdfium.so
PDFIUM_STAGED_STAMP := $(PDFIUM_STAGED_LIB).ready
endif
ifeq ($(UNAME), Darwin)
PDFIUM_PLATFORM_DIR := $(PDFIUM_DIR)/macos-arm64
ifeq ($(MACOS_ARCH),x86_64)
PDFIUM_PLATFORM_DIR := $(PDFIUM_DIR)/macos-x64
endif
PDFIUM_INC := $(PDFIUM_PLATFORM_DIR)/include
PDFIUM_LIB := $(PDFIUM_PLATFORM_DIR)/lib/libpdfium.dylib
PDFIUM_STAGED_LIB := ./lib/$(GGML_PRECOMPILED_SUBDIR)/libpdfium.dylib
PDFIUM_STAGED_STAMP := $(PDFIUM_STAGED_LIB).ready
endif
DOC_LIBS :=
DOC_DEPS_HEADERS :=
DOC_RUNTIME_DEPS :=
ifneq ($(wildcard $(PUGIXML_HDR)),)
CXXFLAGS += -DPUGIXML_NO_EXCEPTIONS
CXXFLAGS += -DAI_FILE_SORTER_USE_PUGIXML
INCLUDE_DIRS += -I$(PUGIXML_DIR)/src
endif
ifneq ($(wildcard $(LIBZIP_CMAKE)),)
CXXFLAGS += -DAI_FILE_SORTER_USE_LIBZIP
INCLUDE_DIRS += -I$(LIBZIP_DIR)/lib -I$(LIBZIP_BUILD_DIR)
LDFLAGS += -lz
DOC_LIBS += $(LIBZIP_LIB)
DOC_DEPS_HEADERS += $(LIBZIP_CONF)
endif
ifneq ($(wildcard $(PDFIUM_LIB)),)
CXXFLAGS += -DAI_FILE_SORTER_USE_PDFIUM
INCLUDE_DIRS += -I$(PDFIUM_INC)
DOC_LIBS += $(PDFIUM_STAGED_LIB)
DOC_RUNTIME_DEPS += $(PDFIUM_STAGED_STAMP)
endif
# Enable mtmd progress callback only when the linked libmtmd provides it.
MTMD_PROGRESS_CALLBACK ?= auto
MTMD_LIB_CANDIDATES :=
ifeq ($(UNAME), Linux)
MTMD_LIB_CANDIDATES := ./lib/precompiled/cpu/bin/libmtmd.so ./lib/precompiled/libmtmd.so
endif
ifeq ($(UNAME), Darwin)
MTMD_LIB_CANDIDATES := ./lib/$(GGML_PRECOMPILED_SUBDIR)/libmtmd.dylib $(BREW_PREFIX)/lib/libmtmd.dylib
endif
ifeq ($(MTMD_PROGRESS_CALLBACK),auto)
MTMD_PROGRESS_CALLBACK := $(shell \
if command -v nm >/dev/null 2>&1; then \
for lib in $(MTMD_LIB_CANDIDATES); do \
if [ -e "$$lib" ] && nm -D --defined-only "$$lib" 2>/dev/null | grep -q "mtmd_helper_set_progress_callback"; then \
echo 1; exit 0; \
fi; \
if [ -e "$$lib" ] && nm -g "$$lib" 2>/dev/null | grep -q "[Tt] _mtmd_helper_set_progress_callback"; then \
echo 1; exit 0; \
fi; \
done; \
elif command -v objdump >/dev/null 2>&1; then \
for lib in $(MTMD_LIB_CANDIDATES); do \
if [ -e "$$lib" ] && objdump -T "$$lib" 2>/dev/null | grep -q "mtmd_helper_set_progress_callback"; then \
echo 1; exit 0; \
fi; \
done; \
fi; \
echo 0)
endif
ifeq ($(MTMD_PROGRESS_CALLBACK),1)
CXXFLAGS += -DAI_FILE_SORTER_MTMD_PROGRESS_CALLBACK
else
CXXFLAGS += -UAI_FILE_SORTER_MTMD_PROGRESS_CALLBACK
endif
# Enable mtmd log callback only when the linked libmtmd provides it.
MTMD_LOG_CALLBACK ?= auto
ifeq ($(MTMD_LOG_CALLBACK),auto)
MTMD_LOG_CALLBACK := $(shell \
if command -v nm >/dev/null 2>&1; then \
for lib in $(MTMD_LIB_CANDIDATES); do \
if [ -e "$$lib" ] && nm -D --defined-only "$$lib" 2>/dev/null | grep -q "mtmd_helper_log_set"; then \
echo 1; exit 0; \
fi; \
if [ -e "$$lib" ] && nm -g "$$lib" 2>/dev/null | grep -q "[Tt] _mtmd_helper_log_set"; then \
echo 1; exit 0; \
fi; \
done; \
elif command -v objdump >/dev/null 2>&1; then \
for lib in $(MTMD_LIB_CANDIDATES); do \
if [ -e "$$lib" ] && objdump -T "$$lib" 2>/dev/null | grep -q "mtmd_helper_log_set"; then \
echo 1; exit 0; \
fi; \
done; \
fi; \
echo 0)
endif
ifeq ($(MTMD_LOG_CALLBACK),1)
CXXFLAGS += -DAI_FILE_SORTER_MTMD_LOG_CALLBACK
else
CXXFLAGS += -UAI_FILE_SORTER_MTMD_LOG_CALLBACK
endif
QRC_FILE := resources/app.qrc
QRC_CPP := $(OBJ_DIR)/qrc_app.cpp
QRC_OBJ := $(OBJ_DIR)/qrc_app.o
QRC_DIR := $(dir $(QRC_FILE))
QRC_RESOURCES := $(shell sed -n -E 's|.*([^<]*).*|\1|p' $(QRC_FILE))
QRC_RESOURCES := $(addprefix $(QRC_DIR),$(QRC_RESOURCES))
MAKEFILE_SELF := $(lastword $(MAKEFILE_LIST))
TS_DIR := resources/i18n
TS_FILES := \
$(TS_DIR)/aifilesorter_fr.ts \
$(TS_DIR)/aifilesorter_de.ts \
$(TS_DIR)/aifilesorter_it.ts \
$(TS_DIR)/aifilesorter_es.ts \
$(TS_DIR)/aifilesorter_nl.ts \
$(TS_DIR)/aifilesorter_tr.ts \
$(TS_DIR)/aifilesorter_ko.ts
QM_DIR := $(OBJ_DIR)/i18n
QM_FILES := $(patsubst $(TS_DIR)/%.ts,$(QM_DIR)/%.qm,$(TS_FILES))
TRANSLATIONS_QRC := $(OBJ_DIR)/translations.qrc
TRANSLATIONS_QRC_CPP := $(OBJ_DIR)/qrc_translations.cpp
TRANSLATIONS_QRC_OBJ := $(OBJ_DIR)/qrc_translations.o
QMAKE6 := $(shell \
if command -v qmake6 >/dev/null 2>&1; then \
command -v qmake6; \
elif [ -x /usr/lib/qt6/bin/qmake6 ]; then \
printf '%s\n' /usr/lib/qt6/bin/qmake6; \
fi)
QTPATHS6 := $(shell \
if command -v qtpaths6 >/dev/null 2>&1; then \
command -v qtpaths6; \
elif [ -x /usr/lib/qt6/bin/qtpaths6 ]; then \
printf '%s\n' /usr/lib/qt6/bin/qtpaths6; \
fi)
ifndef RCC
RCC := $(shell command -v qt6-rcc 2>/dev/null)
ifeq ($(strip $(RCC)),)
RCC := $(wildcard /usr/lib/qt6/libexec/rcc)
endif
ifeq ($(strip $(RCC)),)
RCC := $(shell command -v rcc 2>/dev/null)
endif
ifeq ($(strip $(RCC)),)
RCC := $(wildcard /opt/homebrew/opt/qt/bin/qt6-rcc)
endif
ifeq ($(strip $(RCC)),)
RCC := $(wildcard /usr/lib/qt6/libexec/rcc)
endif
ifeq ($(strip $(RCC)),)
RCC := $(wildcard /opt/homebrew/opt/qtbase/share/qt/libexec/rcc)
endif
ifeq ($(strip $(RCC)),)
RCC := $(wildcard /usr/local/opt/qtbase/share/qt/libexec/rcc)
endif
endif
ifeq ($(strip $(RCC)),)
ifeq ($(NEEDS_BUILD_DEPS),1)
$(error Could not find Qt resource compiler (qt6-rcc or rcc))
endif
endif
ifndef LRELEASE
LRELEASE := $(shell \
for candidate in lrelease6 lrelease-qt6; do \
if command -v "$$candidate" >/dev/null 2>&1; then \
command -v "$$candidate"; \
exit 0; \
fi; \
done; \
if [ -n "$(QTPATHS6)" ]; then \
for qt_host_dir in "$$($(QTPATHS6) --query QT_HOST_LIBEXECS 2>/dev/null)" "$$($(QTPATHS6) --query QT_HOST_BINS 2>/dev/null)"; do \
if [ -n "$$qt_host_dir" ] && [ -x "$$qt_host_dir/lrelease" ]; then \
printf '%s\n' "$$qt_host_dir/lrelease"; \
exit 0; \
fi; \
done; \
fi; \
if [ -n "$(QMAKE6)" ]; then \
for qt_host_dir in "$$($(QMAKE6) -query QT_HOST_LIBEXECS 2>/dev/null)" "$$($(QMAKE6) -query QT_HOST_BINS 2>/dev/null)" "$$($(QMAKE6) -query QT_INSTALL_BINS 2>/dev/null)"; do \
if [ -n "$$qt_host_dir" ] && [ -x "$$qt_host_dir/lrelease" ]; then \
printf '%s\n' "$$qt_host_dir/lrelease"; \
exit 0; \
fi; \
done; \
fi; \
for candidate in \
/usr/lib/qt6/libexec/lrelease \
/usr/lib/qt6/bin/lrelease \
/opt/homebrew/bin/lrelease \
/opt/homebrew/opt/qt/share/qt/libexec/lrelease \
/opt/homebrew/opt/qtbase/share/qt/libexec/lrelease \
/usr/local/opt/qtbase/share/qt/libexec/lrelease; do \
if [ -x "$$candidate" ]; then \
printf '%s\n' "$$candidate"; \
exit 0; \
fi; \
done; \
if command -v lrelease >/dev/null 2>&1; then \
candidate="$$(command -v lrelease)"; \
if "$$candidate" -version 2>&1 | grep -Eq 'Qt[^0-9]*6|version 6'; then \
printf '%s\n' "$$candidate"; \
fi; \
fi)
endif
ifeq ($(strip $(LRELEASE)),)
ifeq ($(NEEDS_BUILD_DEPS),1)
$(error Could not find a Qt 6 translation compiler (install qt6-l10n-tools / qt6-tools-dev-tools, or set LRELEASE=/path/to/qt6/lrelease))
endif
endif
# Source files
SRCS = main.cpp $(wildcard $(SRC_DIR)/*.cpp)
OBJS = $(patsubst %.cpp, $(OBJ_DIR)/%.o, $(notdir $(SRCS)))
DEPS = $(OBJS:.o=.d) $(QRC_OBJ:.o=.d) $(TRANSLATIONS_QRC_OBJ:.o=.d)
BUILD_CONFIG_STAMP := $(OBJ_DIR)/.build-config
PRECOMPILED_LLAMA :=
ifeq ($(UNAME), Darwin)
ifneq ($(LLAMA_MULTI_VARIANT),0)
PRECOMPILED_LLAMA := precompiled_llama
endif
endif
.PHONY: all clean install uninstall doc_runtime_libs precompiled_llama MACOS_LLAMA_M1 MACOS_LLAMA_M2 MACOS_LLAMA_INTEL
# Main rules
all: $(PRECOMPILED_LLAMA) doc_runtime_libs $(TARGET)
@printf "\nFinished building AI File Sorter for %s\n" "$(PLATFORM)"
$(TARGET): $(OBJS) $(QRC_OBJ) $(TRANSLATIONS_QRC_OBJ) $(DOC_LIBS) | doc_runtime_libs $(DOC_RUNTIME_DEPS)
mkdir -p $(BIN_DIR)
$(CXX) $(CXXFLAGS) -o $@ $^ $(LIB_DIRS) $(LDFLAGS)
ifeq ($(PLATFORM), Linux)
@$(MAKE) create_run_wrapper
endif
$(BUILD_CONFIG_STAMP): Makefile
mkdir -p $(OBJ_DIR)
@tmp="$(BUILD_CONFIG_STAMP).tmp.$$"; \
printf '%s\n' \
'CXX=$(CXX)' \
'CXXFLAGS=$(CXXFLAGS)' \
'INCLUDE_DIRS=$(INCLUDE_DIRS)' \
'APP_NAME=$(APP_NAME)' \
'GGML_PRECOMPILED_SUBDIR=$(GGML_PRECOMPILED_SUBDIR)' \
'MACOS_ARCH=$(MACOS_ARCH)' \
'BIN_DIR=$(BIN_DIR)' \
> "$$tmp"; \
if [ ! -f "$@" ] || ! cmp -s "$$tmp" "$@"; then \
mv "$$tmp" "$@"; \
else \
rm -f "$$tmp"; \
fi
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp $(DOC_DEPS_HEADERS) $(BUILD_CONFIG_STAMP)
mkdir -p $(OBJ_DIR)
$(CXX) $(CXXFLAGS) $(INCLUDE_DIRS) -MMD -MP -MF $(@:.o=.d) -c $< -o $@
$(LIBZIP_CONF): $(LIBZIP_LIB)
$(LIBZIP_LIB):
@echo "Building libzip (vendored)..."
mkdir -p $(LIBZIP_BUILD_DIR)
LDFLAGS= cmake -S $(LIBZIP_DIR) -B $(LIBZIP_BUILD_DIR) \
$(LIBZIP_CMAKE_ARGS) \
-DCMAKE_EXE_LINKER_FLAGS= \
-DCMAKE_SHARED_LINKER_FLAGS= \
-DCMAKE_MODULE_LINKER_FLAGS= \
-DBUILD_SHARED_LIBS=OFF \
-DENABLE_BZIP2=OFF \
-DENABLE_LZMA=OFF \
-DENABLE_ZSTD=OFF \
-DENABLE_OPENSSL=OFF \
-DENABLE_GNUTLS=OFF \
-DENABLE_MBEDTLS=OFF \
-DENABLE_COMMONCRYPTO=OFF \
-DENABLE_WINDOWS_CRYPTO=OFF
LDFLAGS= cmake --build $(LIBZIP_BUILD_DIR)
doc_runtime_libs:
ifneq ($(wildcard $(PDFIUM_LIB)),)
$(PDFIUM_STAGED_LIB): $(PDFIUM_LIB)
@echo "Staging PDFium runtime library..."
mkdir -p $(dir $(PDFIUM_STAGED_LIB))
cp $(PDFIUM_LIB) $(PDFIUM_STAGED_LIB)
ifeq ($(UNAME), Darwin)
chmod u+w $(PDFIUM_STAGED_LIB) 2>/dev/null || true
install_name_tool -id @rpath/libpdfium.dylib $(PDFIUM_STAGED_LIB)
endif
$(PDFIUM_STAGED_STAMP): $(PDFIUM_STAGED_LIB)
touch $@
doc_runtime_libs: $(PDFIUM_STAGED_STAMP)
endif
precompiled_llama:
ifeq ($(UNAME), Darwin)
@echo "Checking llama.cpp precompiled libraries (multi-variant=$(LLAMA_MULTI_VARIANT))..."
@if [ -f "./lib/$(GGML_PRECOMPILED_SUBDIR)/libllama.dylib" ]; then \
echo "Using existing precompiled libs in ./lib/$(GGML_PRECOMPILED_SUBDIR)"; \
else \
echo "Building llama.cpp precompiled libraries (multi-variant=$(LLAMA_MULTI_VARIANT))..."; \
LLAMA_MACOS_MULTI_VARIANT=$(LLAMA_MULTI_VARIANT) LLAMA_PRECOMPILED_DIR=./lib/$(GGML_PRECOMPILED_SUBDIR) ./scripts/build_llama_macos.sh; \
fi
else
@echo "precompiled_llama is only supported on macOS."
endif
MACOS_LLAMA_M1:
ifeq ($(UNAME), Darwin)
@echo "Checking llama.cpp for M1-safe multi-variant CPU backends..."
@if [ -f "./lib/precompiled-m1/libllama.dylib" ]; then \
echo "Using existing precompiled libs in ./lib/precompiled-m1"; \
else \
echo "Building llama.cpp for M1-safe multi-variant CPU backends..."; \
LLAMA_MACOS_MULTI_VARIANT=1 LLAMA_PRECOMPILED_DIR=./lib/precompiled-m1 ./scripts/build_llama_macos.sh; \
fi
$(MAKE) LLAMA_MULTI_VARIANT=0 GGML_PRECOMPILED_SUBDIR=precompiled-m1 APP_NAME="AI File Sorter for Mac M1" BIN_DIR=./bin/m1 all
else
@echo "MACOS_LLAMA_M1 is only supported on macOS."
endif
MACOS_LLAMA_M2:
ifeq ($(UNAME), Darwin)
@echo "Checking llama.cpp for native (fast) Apple Silicon backends..."
@if [ -f "./lib/precompiled-m2/libllama.dylib" ]; then \
echo "Using existing precompiled libs in ./lib/precompiled-m2"; \
else \
echo "Building llama.cpp for native (fast) Apple Silicon backends..."; \
LLAMA_MACOS_MULTI_VARIANT=0 LLAMA_PRECOMPILED_DIR=./lib/precompiled-m2 ./scripts/build_llama_macos.sh; \
fi
$(MAKE) LLAMA_MULTI_VARIANT=0 GGML_PRECOMPILED_SUBDIR=precompiled-m2 APP_NAME="AI File Sorter for Mac M2-M5" BIN_DIR=./bin/m2 all
else
@echo "MACOS_LLAMA_M2 is only supported on macOS."
endif
MACOS_LLAMA_INTEL:
ifeq ($(UNAME), Darwin)
@echo "Checking llama.cpp for Intel (x86_64) macOS backends..."
@if [ -f "./lib/precompiled-intel/libllama.dylib" ]; then \
echo "Using existing precompiled libs in ./lib/precompiled-intel"; \
else \
echo "Building llama.cpp for Intel (x86_64) macOS backends..."; \
LLAMA_MACOS_ARCH=x86_64 LLAMA_MACOS_ENABLE_METAL=1 LLAMA_MACOS_MULTI_VARIANT=0 LLAMA_PRECOMPILED_DIR=./lib/precompiled-intel ./scripts/build_llama_macos.sh; \
fi
$(MAKE) LLAMA_MULTI_VARIANT=0 GGML_PRECOMPILED_SUBDIR=precompiled-intel APP_NAME="AI File Sorter for Mac Intel" BIN_DIR=./bin/intel MACOS_ARCH=x86_64 all
else
@echo "MACOS_LLAMA_INTEL is only supported on macOS."
endif
$(OBJ_DIR)/main.o: main.cpp $(DOC_DEPS_HEADERS) $(BUILD_CONFIG_STAMP)
mkdir -p $(OBJ_DIR)
$(CXX) $(CXXFLAGS) $(INCLUDE_DIRS) -MMD -MP -MF $(@:.o=.d) -c $< -o $@
$(QRC_CPP): $(QRC_FILE) $(QRC_RESOURCES) $(MAKEFILE_SELF)
mkdir -p $(OBJ_DIR)
$(RCC) -name app -o $@ $<
$(QRC_OBJ): $(QRC_CPP) $(DOC_DEPS_HEADERS) $(BUILD_CONFIG_STAMP)
$(CXX) $(CXXFLAGS) $(INCLUDE_DIRS) -MMD -MP -MF $(@:.o=.d) -c $< -o $@
$(QM_DIR)/%.qm: $(TS_DIR)/%.ts
mkdir -p $(dir $@)
$(LRELEASE) $< -qm $@
$(TRANSLATIONS_QRC): $(QM_FILES)
mkdir -p $(OBJ_DIR)
@tmp="$@.tmp"; \
printf '%s\n' '' ' ' > "$$tmp"; \
for qm in $(QM_FILES); do \
printf ' i18n/%s\n' "$$(basename "$$qm")" "$$(basename "$$qm")" >> "$$tmp"; \
done; \
printf '%s\n' ' ' '' >> "$$tmp"; \
mv "$$tmp" "$@"
$(TRANSLATIONS_QRC_CPP): $(TRANSLATIONS_QRC) $(QM_FILES) $(MAKEFILE_SELF)
mkdir -p $(OBJ_DIR)
$(RCC) -name translations -o $@ $<
$(TRANSLATIONS_QRC_OBJ): $(TRANSLATIONS_QRC_CPP) $(DOC_DEPS_HEADERS) $(BUILD_CONFIG_STAMP)
$(CXX) $(CXXFLAGS) $(INCLUDE_DIRS) -MMD -MP -MF $(@:.o=.d) -c $< -o $@
clean:
rm -rf $(OBJ_ROOT) $(BIN_DIR)
create_run_wrapper:
@mkdir -p $(BIN_DIR)
@python3 scripts/gen_run_wrapper.py \
--mode dev \
--binary '$(WRAPPED_BINARY)' \
--output '$(BIN_DIR)/run_aifilesorter.sh'
@chmod 755 $(BIN_DIR)/run_aifilesorter.sh
install: $(TARGET)
ifeq ($(PLATFORM), Linux)
@echo "Installing runtime payload into $(INSTALL_LIB_DIR)..."
mkdir -p $(INSTALL_LIB_DIR)/bin
mkdir -p $(INSTALL_LIB_DIR)/lib
cp $(TARGET) $(INSTALL_LIB_DIR)/bin/$(WRAPPED_BINARY)
rm -rf $(INSTALL_LIB_DIR)/lib/$(GGML_PRECOMPILED_SUBDIR)
cp -a lib/$(GGML_PRECOMPILED_SUBDIR) $(INSTALL_LIB_DIR)/lib/
if [ -f ../external/pdfium/linux-x64/lib/libpdfium.so ]; then \
cp ../external/pdfium/linux-x64/lib/libpdfium.so $(INSTALL_LIB_DIR)/lib/$(GGML_PRECOMPILED_SUBDIR)/; \
fi
if [ -f resources/certs/cacert.pem ]; then \
mkdir -p $(INSTALL_LIB_DIR)/certs; \
cp resources/certs/cacert.pem $(INSTALL_LIB_DIR)/certs/cacert.pem; \
fi
@echo "Installing launcher script to $(INSTALL_DIR)..."
mkdir -p $(INSTALL_DIR)
@python3 scripts/gen_run_wrapper.py \
--mode install \
--install-app-dir '$(INSTALL_LIB_DIR)' \
--binary '$(WRAPPED_BINARY)' \
--output '$(INSTALL_DIR)/run_aifilesorter.sh'
chmod 755 $(INSTALL_DIR)/run_aifilesorter.sh
ln -sf $(INSTALL_DIR)/run_aifilesorter.sh $(INSTALL_DIR)/aifilesorter
@echo "Installation complete."
else ifeq ($(PLATFORM), MacOS)
@echo "Installing binary to $(INSTALL_DIR)..."
mkdir -p $(INSTALL_DIR)
cp $(TARGET) $(INSTALL_DIR)/aifilesorter
@echo "Installing libraries to $(INSTALL_LIB_DIR)..."
mkdir -p $(INSTALL_LIB_DIR)
cp lib/$(GGML_PRECOMPILED_SUBDIR)/libggml-base.dylib $(INSTALL_LIB_DIR)
cp lib/$(GGML_PRECOMPILED_SUBDIR)/libggml-blas.dylib $(INSTALL_LIB_DIR)
cp lib/$(GGML_PRECOMPILED_SUBDIR)/libggml-cpu.dylib $(INSTALL_LIB_DIR)
cp lib/$(GGML_PRECOMPILED_SUBDIR)/libggml-metal.dylib $(INSTALL_LIB_DIR)
cp lib/$(GGML_PRECOMPILED_SUBDIR)/libggml.dylib $(INSTALL_LIB_DIR)
cp lib/$(GGML_PRECOMPILED_SUBDIR)/libmtmd.dylib $(INSTALL_LIB_DIR)
cp lib/$(GGML_PRECOMPILED_SUBDIR)/libllama.dylib $(INSTALL_LIB_DIR)
if [ -n "$(PDFIUM_LIB)" ] && [ -f "$(PDFIUM_LIB)" ]; then \
cp "$(PDFIUM_LIB)" $(INSTALL_LIB_DIR); \
fi
install_name_tool -add_rpath $(INSTALL_LIB_DIR) $(INSTALL_DIR)/aifilesorter
@echo "macOS installation complete."
endif
uninstall:
ifeq ($(PLATFORM), Linux)
@echo "Uninstalling aifilesorter binary and libraries..."
@echo "Removing binary from /usr/local/bin..."
rm -f /usr/local/bin/aifilesorter
@echo "Removing libraries from /usr/local/lib/aifilesorter..."
rm -rf /usr/local/lib/aifilesorter
@echo "Removing ld config file..."
rm -f /etc/ld.so.conf.d/aifilesorter.conf
@echo "Running ldconfig..."
ldconfig
@echo "Core uninstallation complete."
@bash -c 'read -p "Do you also want to delete the downloaded local LLM models in ~/.local/share/aifilesorter/llms/? [y/N] " ans; \
if [ "$$ans" = "y" ] || [ "$$ans" = "Y" ]; then \
echo "Deleting ~/.local/share/aifilesorter/llms/..."; \
rm -rf "$$HOME/.local/share/aifilesorter/llms"; \
else \
echo "Keeping downloaded models."; \
fi'
else ifeq ($(PLATFORM), MacOS)
@echo "Uninstalling aifilesorter binary and libraries on macOS..."
@echo "Removing binary from $(INSTALL_DIR)..."
rm -f $(INSTALL_DIR)/aifilesorter
@echo "Removing installed libraries..."
rm -rf $(INSTALL_LIB_DIR)
@echo "Core uninstallation complete."
@read -p "Do you also want to delete the downloaded local LLM models in ~/Library/Application\ Support/aifilesorter/llms/? [y/N] " ans; \
if [ "$$ans" = "y" ] || [ "$$ans" = "Y" ]; then \
echo "Deleting ~/Library/Application Support/aifilesorter/llms/..."; \
rm -rf "$$HOME/Library/Application Support/aifilesorter/llms"; \
else \
echo "Keeping downloaded models."; \
fi
endif
-include $(DEPS)
================================================
FILE: app/build_windows.ps1
================================================
param(
[string]$VcpkgRoot,
[ValidateSet("Debug", "Release")]
[string]$Configuration = "Release",
[switch]$Clean,
[string]$Generator,
[switch]$SkipDeploy,
[switch]$BuildTests,
[switch]$RunTests,
[ValidateRange(1, 512)]
[int]$Parallel = [System.Environment]::ProcessorCount,
[ValidateSet("Standard", "MsStore", "Standalone")]
[string[]]$Variants = @("Standard", "MsStore", "Standalone")
)
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$appDir = $scriptDir
$llamaDir = Join-Path $appDir "include/external/llama.cpp"
$legacySharedVcpkgInstalledDir = Join-Path $appDir "build-windows\\vcpkg_installed"
$dedicatedSharedVcpkgInstalledDir = Join-Path $appDir "build-windows-vcpkg_installed"
$sharedVcpkgInstalledDir = if (Test-Path $legacySharedVcpkgInstalledDir) {
$legacySharedVcpkgInstalledDir
} else {
$dedicatedSharedVcpkgInstalledDir
}
$variantDefinitions = @{
Standard = [pscustomobject]@{
Name = "Standard"
BuildDir = (Join-Path $appDir "build-windows")
UpdateMode = "AUTO_INSTALL"
Description = "Auto-install updates"
}
MsStore = [pscustomobject]@{
Name = "MsStore"
BuildDir = (Join-Path $appDir "build-windows-store")
UpdateMode = "DISABLED"
Description = "No update checks"
}
Standalone = [pscustomobject]@{
Name = "Standalone"
BuildDir = (Join-Path $appDir "build-windows-standalone")
UpdateMode = "NOTIFY_ONLY"
Description = "Notification-only updates"
}
}
function Resolve-VcpkgRootFromPath {
param([string]$Path)
if (-not $Path) { return $null }
try {
$candidate = (Resolve-Path $Path -ErrorAction Stop).Path
} catch {
return $null
}
if ((Get-Item $candidate).PSIsContainer) {
$dir = $candidate
} else {
$dir = (Get-Item $candidate).Directory.FullName
}
while ($dir -and (Test-Path $dir)) {
$toolchain = Join-Path $dir "scripts/buildsystems/vcpkg.cmake"
if (Test-Path $toolchain) {
return $dir
}
$parent = Split-Path -Parent $dir
if (-not $parent -or $parent -eq $dir) {
break
}
$dir = $parent
}
return $null
}
function Copy-VcpkgRuntimeDlls {
param(
[string[]]$SourceDirectories,
[string]$Destination
)
$copied = @()
foreach ($dir in $SourceDirectories) {
if (-not $dir) { continue }
if (-not (Test-Path $dir)) { continue }
$dlls = Get-ChildItem -Path $dir -Filter "*.dll" -File -ErrorAction SilentlyContinue |
Where-Object { $_.Name -notmatch '^Qt6' }
foreach ($dll in $dlls) {
Copy-Item $dll.FullName -Destination $Destination -Force
$copied += $dll.Name
}
}
return $copied | Sort-Object -Unique
}
function Get-ConfigureArguments {
param(
[pscustomobject]$Variant,
[string]$ToolchainFile,
[switch]$EnableTests,
[int]$CMakeMajor,
[int]$CMakeMinor
)
$configureArgs = @("-S", $appDir, "-B", $Variant.BuildDir)
$configureArgs += @("-G", $Generator)
$configureArgs += "-DCMAKE_TOOLCHAIN_FILE=$ToolchainFile"
$configureArgs += "-DVCPKG_TARGET_TRIPLET=x64-windows"
$configureArgs += "-DVCPKG_MANIFEST_DIR=$appDir"
$configureArgs += "-DVCPKG_INSTALLED_DIR=$sharedVcpkgInstalledDir"
$configureArgs += "-DAI_FILE_SORTER_UPDATE_MODE=$($Variant.UpdateMode)"
if ($EnableTests) {
$configureArgs += "-DAI_FILE_SORTER_BUILD_TESTS=ON"
}
if ($env:AI_FILE_SORTER_STARTER_CONSOLE) {
$configureArgs += "-DAI_FILE_SORTER_STARTER_CONSOLE=$($env:AI_FILE_SORTER_STARTER_CONSOLE)"
}
if ($CMakeMajor -lt 3 -or ($CMakeMajor -eq 3 -and $CMakeMinor -lt 22)) {
$cmakeMajorMinor = "$CMakeMajor.$CMakeMinor"
Write-Warning "Detected CMake $cmakeMajorMinor < 3.22; passing QT_FORCE_MIN_CMAKE_VERSION_FOR_USING_QT=$cmakeMajorMinor to satisfy Qt 6.9 requirements."
$configureArgs += "-DQT_FORCE_MIN_CMAKE_VERSION_FOR_USING_QT=$cmakeMajorMinor"
}
if ($Generator -eq "Ninja" -or $Generator -eq "Ninja Multi-Config") {
$configureArgs += "-DCMAKE_BUILD_TYPE=$Configuration"
} else {
$configureArgs += "-A"
$configureArgs += "x64"
}
return ,$configureArgs
}
function Resolve-OutputExecutable {
param(
[string]$BuildDir,
[string]$ConfigurationName
)
$binDir = Join-Path $appDir "bin"
$buildConfigDir = Join-Path $BuildDir $ConfigurationName
$outputCandidates = @(
(Join-Path $buildConfigDir "aifilesorter.exe"),
(Join-Path $BuildDir "aifilesorter.exe"),
(Join-Path (Join-Path $binDir $ConfigurationName) "aifilesorter.exe"),
(Join-Path $binDir "aifilesorter.exe")
)
foreach ($candidate in $outputCandidates) {
if ($candidate -and (Test-Path $candidate)) {
return $candidate
}
}
Write-Warning "Expected executable was not found in standard locations. Reported path may not exist: $($outputCandidates[0])"
return $outputCandidates[0]
}
function Stage-BuildOutput {
param(
[string]$OutputExe,
[string]$BuildDir
)
$outputDir = Split-Path -Parent $OutputExe
$pdfiumDll = Join-Path $appDir "..\\external\\pdfium\\windows-x64\\bin\\pdfium.dll"
$pdfiumDllPath = Resolve-Path -Path $pdfiumDll -ErrorAction SilentlyContinue
if ($pdfiumDllPath) {
Copy-Item $pdfiumDllPath.Path -Destination $outputDir -Force
} else {
Write-Warning "PDFium DLL not found under external/pdfium/windows-x64/bin. Run app\\scripts\\vendor_doc_deps.ps1 (or app/scripts/vendor_doc_deps.sh) to populate it."
}
$precompiledCpuBin = Join-Path $appDir "lib/precompiled/cpu/bin"
$precompiledCudaBin = Join-Path $appDir "lib/precompiled/cuda/bin"
$precompiledVulkanBin = Join-Path $appDir "lib/precompiled/vulkan/bin"
$destWocuda = Join-Path $outputDir "lib/ggml/wocuda"
$destWcuda = Join-Path $outputDir "lib/ggml/wcuda"
$destWvulkan = Join-Path $outputDir "lib/ggml/wvulkan"
foreach ($destDir in @($destWocuda, $destWcuda, $destWvulkan)) {
if (-not (Test-Path $destDir)) {
New-Item -ItemType Directory -Path $destDir -Force | Out-Null
}
}
if (Test-Path $precompiledCpuBin) {
Get-ChildItem -Path $precompiledCpuBin -Filter "*.dll" -File -ErrorAction SilentlyContinue |
ForEach-Object {
if ($_.Name -ieq "libcurl.dll") { return }
Copy-Item $_.FullName -Destination $destWocuda -Force
}
}
$mingwRuntimeNames = @("libgomp-1.dll", "libgcc_s_seh-1.dll", "libgfortran-5.dll", "libwinpthread-1.dll", "libquadmath-0.dll")
$runtimeSearchPaths = @()
if ($env:OPENBLAS_ROOT) {
$runtimeSearchPaths += (Join-Path $env:OPENBLAS_ROOT "bin")
}
$runtimeSearchPaths += "C:\msys64\mingw64\bin"
foreach ($dllName in $mingwRuntimeNames) {
$found = $false
foreach ($path in $runtimeSearchPaths) {
if (-not (Test-Path $path)) { continue }
$candidate = Join-Path $path $dllName
if (Test-Path $candidate) {
Copy-Item $candidate -Destination $destWocuda -Force
Copy-Item $candidate -Destination $outputDir -Force
$found = $true
break
}
}
if (-not $found) {
Write-Warning "Could not locate $dllName in any runtime path. Add it manually to $destWocuda if needed."
}
}
if (Test-Path $precompiledCudaBin) {
Get-ChildItem -Path $precompiledCudaBin -Filter "*.dll" -File -ErrorAction SilentlyContinue |
ForEach-Object {
if ($_.Name -ieq "libcurl.dll") { return }
Copy-Item $_.FullName -Destination $destWcuda -Force
}
}
if (Test-Path $precompiledVulkanBin) {
Get-ChildItem -Path $precompiledVulkanBin -Filter "*.dll" -File -ErrorAction SilentlyContinue |
ForEach-Object {
if ($_.Name -ieq "libcurl.dll") { return }
Copy-Item $_.FullName -Destination $destWvulkan -Force
}
}
foreach ($destDir in @($destWocuda, $destWcuda, $destWvulkan)) {
if (Test-Path $destDir) {
Get-ChildItem -Path $destDir -Filter "*.lib" -File -Recurse -ErrorAction SilentlyContinue |
Remove-Item -Force
Get-ChildItem -Path $destDir -Directory -Recurse -ErrorAction SilentlyContinue |
Where-Object { $_.Name -in @("bin", "lib") } |
ForEach-Object { Remove-Item $_.FullName -Recurse -Force }
}
}
if (-not $SkipDeploy) {
$isWindowsHost = [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)
if ($isWindowsHost) {
$windeployCandidates = @(
(Join-Path $sharedVcpkgInstalledDir "x64-windows/tools/Qt6/bin/windeployqt.exe"),
(Join-Path $BuildDir "vcpkg_installed/x64-windows/tools/Qt6/bin/windeployqt.exe"),
(Join-Path $VcpkgRoot "installed/x64-windows/tools/Qt6/bin/windeployqt.exe"),
(Join-Path $appDir "vcpkg_installed/x64-windows/tools/Qt6/bin/windeployqt.exe")
)
$windeploy = $null
foreach ($candidate in $windeployCandidates) {
if ($candidate -and (Test-Path $candidate)) {
$windeploy = $candidate
break
}
}
if ($windeploy) {
Write-Output "Running windeployqt to stage Qt/runtime DLLs for $OutputExe..."
& $windeploy --no-translations $OutputExe
if ($LASTEXITCODE -ne 0) {
throw "windeployqt failed with exit code $LASTEXITCODE"
}
} else {
Write-Warning "windeployqt.exe not found under vcpkg install roots. Install qtbase via vcpkg or run windeployqt manually."
}
} else {
Write-Warning "Skipping runtime deployment; windeployqt is only available on Windows."
}
} else {
Write-Output "Skipping windeployqt step (per -SkipDeploy)."
}
$vcpkgRuntimeSources = @(
(Join-Path $sharedVcpkgInstalledDir "x64-windows/bin"),
(Join-Path $BuildDir "vcpkg_installed/x64-windows/bin"),
(Join-Path $VcpkgRoot "installed/x64-windows/bin")
)
$copiedVcpkgDlls = Copy-VcpkgRuntimeDlls -SourceDirectories $vcpkgRuntimeSources -Destination $outputDir
if ($copiedVcpkgDlls.Count -gt 0) {
Write-Output ("Staged vcpkg runtime DLLs to {0}:" -f $outputDir)
Write-Output " $($copiedVcpkgDlls -join ', ')"
} else {
Write-Warning "No vcpkg runtime DLLs were copied; ensure curl/openssl/sqlite runtimes are present beside the executable before distributing."
}
}
if (-not (Test-Path (Join-Path $llamaDir "CMakeLists.txt"))) {
throw "llama.cpp submodule not found. Run 'git submodule update --init --recursive' before building."
}
if ($RunTests) {
$BuildTests = $true
}
$selectedVariantNames = @($Variants | Select-Object -Unique)
$selectedVariants = foreach ($variantName in $selectedVariantNames) {
$variantDefinitions[$variantName]
}
if ($BuildTests -and ($selectedVariantNames -notcontains "Standard")) {
throw "-BuildTests and -RunTests currently require the Standard variant because tests are only configured in the auto-update build."
}
if (-not $VcpkgRoot) {
$envCandidates = @($env:VCPKG_ROOT, $env:VPKG_ROOT)
foreach ($envCandidate in $envCandidates) {
$resolved = Resolve-VcpkgRootFromPath -Path $envCandidate
if ($resolved) {
$VcpkgRoot = $resolved
break
}
}
}
if (-not $VcpkgRoot) {
$commandCandidates = @("vcpkg", "vpkg")
foreach ($candidate in $commandCandidates) {
$cmd = Get-Command $candidate -ErrorAction SilentlyContinue
if (-not $cmd) { continue }
$possiblePaths = @($cmd.Source, $cmd.Path, $cmd.Definition)
foreach ($cPath in $possiblePaths) {
$resolved = Resolve-VcpkgRootFromPath -Path $cPath
if ($resolved) {
$VcpkgRoot = $resolved
break
}
}
if ($VcpkgRoot) { break }
}
}
if (-not $VcpkgRoot) {
throw "Could not locate vcpkg. Provide -VcpkgRoot or set the VCPKG_ROOT environment variable. If vcpkg is installed via winget, pass -VcpkgRoot explicitly (e.g. C:\dev\vcpkg)."
}
$cmakeCommand = Get-Command cmake -ErrorAction SilentlyContinue
if (-not $cmakeCommand) {
throw "cmake executable not found in PATH. Install CMake (3.22+) or add it to PATH."
}
$cmakeExe = $cmakeCommand.Path
$cmakeVersionOutput = & $cmakeExe --version
$cmakeVersionPattern = [regex]'cmake version (?\d+)\.(?\d+)(\.(?\d+))?'
$cmakeVersionMatch = $cmakeVersionPattern.Match($cmakeVersionOutput)
if (-not $cmakeVersionMatch.Success) {
Write-Warning "Unable to parse CMake version from output:`n$cmakeVersionOutput"
$cmakeMajor = 0
$cmakeMinor = 0
} else {
$cmakeMajor = [int]$cmakeVersionMatch.Groups['major'].Value
$cmakeMinor = [int]$cmakeVersionMatch.Groups['minor'].Value
if ($cmakeMajor -lt 3 -or ($cmakeMajor -eq 3 -and $cmakeMinor -lt 16)) {
throw "CMake 3.16 or newer is required. Detected version $($cmakeVersionMatch.Value)."
}
}
$toolchainFile = Join-Path $VcpkgRoot "scripts/buildsystems/vcpkg.cmake"
if (-not (Test-Path $toolchainFile)) {
throw "The provided vcpkg root '$VcpkgRoot' does not contain scripts/buildsystems/vcpkg.cmake."
}
if (-not $Generator) {
$Generator = "Visual Studio 17 2022"
}
if ($Generator -eq "Ninja" -or $Generator -eq "Ninja Multi-Config") {
$ninjaEnvArch = $env:VSCMD_ARG_TGT_ARCH
if ($ninjaEnvArch -and ($ninjaEnvArch -ne "x64")) {
Write-Warning "Ninja generator selected while MSVC environment targets '$ninjaEnvArch'. Qt packages are built for x64; run from an x64 Native Tools prompt or choose -Generator ""Visual Studio 17 2022""."
} elseif (-not $ninjaEnvArch) {
Write-Warning "Using Ninja generator without an initialized MSVC environment. Ensure you run from an x64 Native Tools command prompt so the 64-bit compiler is available."
}
}
if ($Parallel -lt 1) {
$Parallel = [Math]::Max([System.Environment]::ProcessorCount, 1)
}
if ($Clean) {
foreach ($variant in $selectedVariants) {
if (Test-Path $variant.BuildDir) {
Write-Output "Removing existing build directory '$($variant.BuildDir)'..."
Remove-Item -Recurse -Force $variant.BuildDir
}
}
if (Test-Path $sharedVcpkgInstalledDir) {
Write-Output "Removing shared vcpkg install directory '$sharedVcpkgInstalledDir'..."
Remove-Item -Recurse -Force $sharedVcpkgInstalledDir
}
}
if (-not (Test-Path $sharedVcpkgInstalledDir)) {
New-Item -ItemType Directory -Path $sharedVcpkgInstalledDir | Out-Null
}
Write-Output "Using $Parallel parallel job(s) for builds."
Write-Output "Shared vcpkg installed directory: $sharedVcpkgInstalledDir"
$builtOutputs = New-Object System.Collections.Generic.List[object]
foreach ($variant in $selectedVariants) {
if (-not (Test-Path $variant.BuildDir)) {
New-Item -ItemType Directory -Path $variant.BuildDir | Out-Null
}
$enableTests = $BuildTests -and $variant.Name -eq "Standard"
$configureArgs = Get-ConfigureArguments -Variant $variant `
-ToolchainFile $toolchainFile `
-EnableTests:$enableTests `
-CMakeMajor $cmakeMajor `
-CMakeMinor $cmakeMinor
Write-Output ""
Write-Output "==== Building $($variant.Name) Variant ===="
Write-Output "Description : $($variant.Description)"
Write-Output "Build dir : $($variant.BuildDir)"
Write-Output "Update mode : $($variant.UpdateMode)"
Write-Output "Configure : cmake $($configureArgs -join ' ')"
Write-Output "====================================="
& $cmakeExe @configureArgs
if ($LASTEXITCODE -ne 0) {
throw "cmake configure failed for the $($variant.Name) variant."
}
$buildArgs = @("--build", $variant.BuildDir, "--config", $Configuration, "--parallel", $Parallel)
Write-Output "Building $($variant.Name) variant..."
& $cmakeExe @buildArgs
if ($LASTEXITCODE -ne 0) {
throw "cmake build failed for the $($variant.Name) variant."
}
if ($enableTests) {
Write-Output "Building unit tests in the Standard variant..."
$testBuildArgs = @("--build", $variant.BuildDir, "--config", $Configuration, "--target", "ai_file_sorter_tests", "--parallel", $Parallel)
& $cmakeExe @testBuildArgs
if ($LASTEXITCODE -ne 0) {
throw "Failed to build unit tests in the Standard variant."
}
if ($RunTests) {
$ctestExe = Join-Path (Split-Path $cmakeExe) "ctest.exe"
if (-not (Test-Path $ctestExe)) {
$ctestExe = "ctest"
}
Push-Location $variant.BuildDir
try {
Write-Output "Running ctest in the Standard variant..."
& $ctestExe "-C" $Configuration "--output-on-failure" "-j" $Parallel
if ($LASTEXITCODE -ne 0) {
throw "ctest reported failures."
}
} finally {
Pop-Location
}
}
}
$outputExe = Resolve-OutputExecutable -BuildDir $variant.BuildDir -ConfigurationName $Configuration
Write-Output "Executable located at: $outputExe"
Stage-BuildOutput -OutputExe $outputExe -BuildDir $variant.BuildDir
$builtOutputs.Add([pscustomobject]@{
Variant = $variant.Name
UpdateMode = $variant.UpdateMode
Executable = $outputExe
}) | Out-Null
}
Write-Output ""
Write-Output "Build summary:"
foreach ($output in $builtOutputs) {
Write-Output (" - {0} [{1}]: {2}" -f $output.Variant, $output.UpdateMode, $output.Executable)
}
================================================
FILE: app/include/AppInfo.hpp
================================================
#pragma once
#include
#ifndef AI_FILE_SORTER_APP_NAME
#define AI_FILE_SORTER_APP_NAME "AI File Sorter"
#endif
inline QString app_display_name() {
return QStringLiteral(AI_FILE_SORTER_APP_NAME);
}
================================================
FILE: app/include/CategorizationDialog.hpp
================================================
#ifndef CATEGORIZATIONDIALOG_HPP
#define CATEGORIZATIONDIALOG_HPP
#include "CategoryLanguage.hpp"
#include "Types.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
class DatabaseManager;
class QCloseEvent;
class QEvent;
class QPushButton;
class QTableView;
class QCheckBox;
class QStandardItem;
class CategorizationDialog : public QDialog
{
Q_DECLARE_TR_FUNCTIONS(CategorizationDialog)
public:
CategorizationDialog(DatabaseManager* db_manager,
bool show_subcategory_col,
const std::string& undo_dir,
CategoryLanguage category_language = CategoryLanguage::English,
QWidget* parent = nullptr);
void set_show_subcategory_column(bool enabled);
bool show_subcategory_column_enabled() const { return show_subcategory_column; }
#ifdef AI_FILE_SORTER_TEST_BUILD
void test_set_entries(const std::vector& files);
void test_trigger_confirm();
void test_trigger_undo();
bool test_undo_enabled() const;
#endif
bool is_dialog_valid() const;
void show_results(const std::vector& categorized_files,
const std::string& base_dir_override = std::string(),
bool include_subdirectories = false,
bool allow_image_renames = true,
bool allow_document_renames = true);
protected:
void closeEvent(QCloseEvent* event) override;
void changeEvent(QEvent* event) override;
private:
enum class RowStatus {
None = 0,
Moved,
Renamed,
RenamedAndMoved,
Skipped,
NotSelected,
Preview
};
static constexpr int kStatusRole = Qt::UserRole + 100;
static constexpr int kFilePathRole = Qt::UserRole + 1;
static constexpr int kUsedConsistencyRole = Qt::UserRole + 2;
static constexpr int kRenameOnlyRole = Qt::UserRole + 3;
static constexpr int kFileTypeRole = Qt::UserRole + 4;
static constexpr int kRenameAppliedRole = Qt::UserRole + 5;
static constexpr int kRenameLockedRole = Qt::UserRole + 6;
static constexpr int kHiddenCategoryRole = Qt::UserRole + 7;
static constexpr int kHiddenSubcategoryRole = Qt::UserRole + 8;
static constexpr int kOriginalFileNameRole = Qt::UserRole + 9;
static constexpr int kOriginalCategoryRole = Qt::UserRole + 10;
static constexpr int kOriginalSubcategoryRole = Qt::UserRole + 11;
static constexpr int kCanonicalCategoryRole = Qt::UserRole + 12;
static constexpr int kCanonicalSubcategoryRole = Qt::UserRole + 13;
enum Column {
ColumnSelect = 0,
ColumnFile = 1,
ColumnType = 2,
ColumnSuggestedName = 3,
ColumnCategory = 4,
ColumnSubcategory = 5,
ColumnStatus = 6,
ColumnPreview = 7
};
struct MoveRecord {
int row_index;
std::string source_path;
std::string destination_path;
std::uintmax_t size_bytes{0};
std::time_t mtime{0};
};
struct PreviewRecord {
std::string source;
std::string destination;
std::string source_file_name;
std::string destination_file_name;
std::string category;
std::string subcategory;
bool use_subcategory{false};
bool rename_only{false};
};
void setup_ui();
void populate_model();
void ensure_unique_suggested_names_in_model();
void record_categorization_to_db();
void on_confirm_and_sort_button_clicked();
void on_continue_later_button_clicked();
void on_undo_button_clicked();
void show_close_button();
void restore_action_buttons();
void update_status_column(int row,
bool success,
bool attempted = true,
bool renamed = false,
bool moved = false);
void on_select_all_toggled(bool checked);
/**
* @brief Selects all highlighted rows for processing.
*/
void on_select_highlighted_clicked();
void apply_select_all(bool checked);
/**
* @brief Applies a check state to the given rows in the Process column.
*/
void apply_check_state_to_rows(const std::vector& rows, Qt::CheckState state);
void on_item_changed(QStandardItem* item);
void update_select_all_state();
void update_type_icon(QStandardItem* item);
void retranslate_ui();
void apply_status_text(QStandardItem* item) const;
RowStatus status_from_item(const QStandardItem* item) const;
void on_show_subcategories_toggled(bool checked);
void apply_subcategory_visibility();
void clear_move_history();
void record_move_for_undo(int row,
const std::string& source,
const std::string& destination,
std::uintmax_t size_bytes,
std::time_t mtime);
void handle_selected_row(int row_index,
const std::string& file_name,
const std::string& rename_candidate,
const std::string& category,
const std::string& subcategory,
const std::string& source_dir,
const std::string& base_dir,
std::vector& files_not_moved,
FileType file_type,
bool rename_only,
bool used_consistency_hints,
bool dry_run);
void persist_move_plan();
bool undo_move_history();
void update_status_after_undo();
bool move_file_back(const std::string& source, const std::string& destination);
void remove_empty_parent_directories(const std::string& destination);
void set_preview_status(int row, const std::string& destination);
void update_preview_column(int row);
std::optional compute_preview_path(int row) const;
std::optional build_preview_record_for_row(int row, std::string* debug_reason = nullptr) const;
std::string resolve_destination_name(const std::string& original_name,
const std::string& rename_candidate) const;
bool validate_filename(const std::string& name, std::string& error) const;
bool resolve_row_flags(int row, bool& rename_only, bool& used_consistency_hints, FileType& file_type) const;
void set_show_rename_column(bool enabled);
void apply_rename_visibility();
void apply_category_visibility();
/**
* @brief Hides rows that are rename-only when required by the dialog mode.
*/
void apply_rename_only_row_visibility();
/**
* @brief Syncs the rename-only checkbox state to current UI options.
*/
void update_rename_only_checkbox_state();
/**
* @brief Enables/disables the subcategory checkbox based on rename-only mode.
*/
void update_subcategory_checkbox_state();
/**
* @brief Marks image rows as rename-only when toggled.
* @param checked True when image rename-only is enabled.
*/
void on_rename_images_only_toggled(bool checked);
/**
* @brief Marks document rows as rename-only when toggled.
* @param checked True when document rename-only is enabled.
*/
void on_rename_documents_only_toggled(bool checked);
bool row_is_already_renamed_with_category(int row) const;
/**
* @brief Returns true if the row points to a supported image file.
* @param row Row index in the model.
* @return True when the row is an image file supported by the visual analyzer.
*/
bool row_is_supported_image(int row) const;
/**
* @brief Returns true if the row points to a supported document file.
* @param row Row index in the model.
* @return True when the row is a supported document file.
*/
bool row_is_supported_document(int row) const;
/**
* @brief Returns unique row indices that are highlighted in the table view.
*/
std::vector selected_row_indices() const;
/**
* @brief Opens a dialog to bulk edit categories for highlighted rows.
*/
void on_bulk_edit_clicked();
DatabaseManager* db_manager;
CategoryLanguage category_language_{CategoryLanguage::English};
bool show_subcategory_column;
bool include_subdirectories_{false};
bool allow_image_renames_{true};
bool allow_document_renames_{true};
bool show_rename_column{false};
std::vector categorized_files;
std::shared_ptr core_logger;
std::shared_ptr db_logger;
std::shared_ptr ui_logger;
QTableView* table_view{nullptr};
QStandardItemModel* model{nullptr};
QPushButton* confirm_button{nullptr};
QPushButton* continue_button{nullptr};
QPushButton* close_button{nullptr};
QCheckBox* select_all_checkbox{nullptr};
QPushButton* select_highlighted_button{nullptr};
QPushButton* bulk_edit_button{nullptr};
QCheckBox* show_subcategories_checkbox{nullptr};
QCheckBox* dry_run_checkbox{nullptr};
QCheckBox* rename_images_only_checkbox{nullptr};
QCheckBox* rename_documents_only_checkbox{nullptr};
QPushButton* undo_button{nullptr};
std::vector move_history_;
std::vector dry_run_plan_;
bool updating_select_all{false};
bool suppress_item_changed_{false};
std::string undo_dir_;
std::string base_dir_;
};
#endif // CATEGORIZATIONDIALOG_HPP
================================================
FILE: app/include/CategorizationProgressDialog.hpp
================================================
#ifndef CATEGORIZATIONPROGRESSDIALOG_HPP
#define CATEGORIZATIONPROGRESSDIALOG_HPP
#include "Types.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
class MainApp;
class QLabel;
class QPlainTextEdit;
class QPushButton;
class QTableWidget;
class QTimer;
class QEvent;
class CategorizationProgressDialog : public QDialog
{
Q_DECLARE_TR_FUNCTIONS(CategorizationProgressDialog)
public:
enum class StageId {
ImageAnalysis,
DocumentAnalysis,
Categorization
};
struct StagePlan {
StageId id;
std::vector items;
};
CategorizationProgressDialog(QWidget* parent, MainApp* main_app, bool show_subcategory_col);
void show();
void hide();
void append_text(const std::string& text);
void configure_stages(const std::vector& stages);
void set_stage_items(StageId stage_id, const std::vector& items);
void set_active_stage(StageId stage_id);
void mark_stage_item_pending(StageId stage_id, const FileEntry& entry);
void mark_stage_item_in_progress(StageId stage_id, const FileEntry& entry);
void mark_stage_item_completed(StageId stage_id, const FileEntry& entry);
protected:
void changeEvent(QEvent* event) override;
private:
enum class ItemStatus {
NotApplicable,
Pending,
InProgress,
Completed
};
enum class DisplayType {
Directory,
File,
Image,
Document
};
struct StageState {
bool enabled{false};
std::unordered_set item_keys;
};
struct ItemState {
int row{-1};
DisplayType display_type{DisplayType::File};
std::array stage_statuses{
ItemStatus::NotApplicable,
ItemStatus::NotApplicable,
ItemStatus::NotApplicable
};
};
static constexpr int kStageCount = 3;
void setup_ui(bool show_subcategory_col);
void retranslate_ui();
void request_stop();
static std::string make_item_key(const std::string& full_path, FileType type);
static std::size_t stage_index(StageId stage_id);
QString stage_label(StageId stage_id) const;
static DisplayType classify_display_type(const FileEntry& entry);
QString display_type_label(DisplayType display_type) const;
int column_for_stage(StageId stage_id) const;
void ensure_stage_enabled(StageId stage_id);
void upsert_stage_item(StageId stage_id, const FileEntry& entry);
void upsert_item(const FileEntry& entry);
void set_stage_item_status(StageId stage_id, const FileEntry& entry, ItemStatus status);
void rebuild_headers();
void refresh_stage_overview();
void refresh_row(int row);
void refresh_summary();
void refresh_spinner();
bool has_in_progress_item() const;
ItemStatus stage_status_for_row(const ItemState& state, StageId stage_id) const;
std::optional find_stage_row(StageId stage_id, ItemStatus status) const;
void ensure_row_visible(int row);
MainApp* main_app;
QLabel* stage_list_label{nullptr};
QLabel* summary_label{nullptr};
QLabel* log_label{nullptr};
QTableWidget* status_table{nullptr};
QPlainTextEdit* text_view{nullptr};
QPushButton* stop_button{nullptr};
QTimer* spinner_timer{nullptr};
std::array stage_states_{};
std::vector active_stage_order_;
std::optional active_stage_;
std::unordered_map item_states_;
int spinner_frame_index_{0};
};
#endif // CATEGORIZATIONPROGRESSDIALOG_HPP
================================================
FILE: app/include/CategorizationService.hpp
================================================
#ifndef CATEGORIZATION_SERVICE_HPP
#define CATEGORIZATION_SERVICE_HPP
#include "Types.hpp"
#include "DatabaseManager.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
class Settings;
class ILLMClient;
namespace spdlog { class logger; }
/**
* @brief Provides LLM-backed file categorization with caching and validation.
*/
class CategorizationService {
public:
using ProgressCallback = std::function;
using QueueCallback = std::function;
using CompletionCallback = std::function;
using RecategorizationCallback = std::function;
/**
* @brief Overrides the name/path used in LLM prompts for a file entry.
*/
struct PromptOverride {
std::string name;
std::string path;
};
using PromptOverrideProvider = std::function(const FileEntry&)>;
/** Supplies an optional suggested rename for an entry during categorization. */
using SuggestedNameProvider = std::function;
/**
* @brief Constructs the service with settings, database access, and logging.
* @param settings Application settings reference.
* @param db_manager Database manager used for cache access.
* @param core_logger Logger for core activity.
*/
CategorizationService(Settings& settings,
DatabaseManager& db_manager,
std::shared_ptr core_logger);
/**
* @brief Verifies that required remote credentials are configured.
* @param error_message Optional output for a user-facing error message.
* @return True when credentials are present or not required.
*/
bool ensure_remote_credentials(std::string* error_message = nullptr) const;
/**
* @brief Removes cached entries that have empty categories for a directory.
* @param directory_path Directory to clean.
* @return Entries that were removed.
*/
std::vector prune_empty_cached_entries(const std::string& directory_path);
/**
* @brief Loads cached categorizations for the provided directory.
* @param directory_path Directory to load.
* @return Cached entries for the directory.
*/
std::vector load_cached_entries(const std::string& directory_path) const;
/**
* @brief Categorizes a list of file entries using the configured LLM workflow.
* @param files Entries to categorize.
* @param is_local_llm True when using a local LLM backend.
* @param stop_flag Cancellation flag.
* @param progress_callback Progress updates callback.
* @param queue_callback Called when an entry is queued.
* @param completion_callback Called when an entry has finished processing.
* @param recategorization_callback Called when an entry must be re-categorized.
* @param llm_factory Factory for creating an LLM client.
* @param prompt_override Optional prompt override provider.
* @param suggested_name_provider Optional suggested-name provider.
* @return Categorized entries that were successfully processed.
*/
std::vector categorize_entries(
const std::vector& files,
bool is_local_llm,
std::atomic& stop_flag,
const ProgressCallback& progress_callback,
const QueueCallback& queue_callback,
const CompletionCallback& completion_callback,
const RecategorizationCallback& recategorization_callback,
std::function()> llm_factory,
const PromptOverrideProvider& prompt_override = {},
const SuggestedNameProvider& suggested_name_provider = {}) const;
private:
using CategoryPair = std::pair;
using HintHistory = std::deque;
using SessionHistoryMap = std::unordered_map;
/**
* @brief Returns a cached categorization when available, otherwise calls the LLM.
* @param llm LLM client used for the request.
* @param is_local_llm True when using a local LLM backend.
* @param display_name Display name for logging.
* @param display_path Display path for logging.
* @param dir_path Full directory path for cache lookup.
* @param prompt_name Name used in the prompt.
* @param prompt_path Path used in the prompt.
* @param file_type File or directory.
* @param progress_callback Progress updates callback.
* @param consistency_context Consistency hints block.
* @return Resolved category for the item.
*/
DatabaseManager::ResolvedCategory categorize_with_cache(
ILLMClient& llm,
bool is_local_llm,
const std::string& display_name,
const std::string& display_path,
const std::string& dir_path,
const std::string& prompt_name,
const std::string& prompt_path,
FileType file_type,
const ProgressCallback& progress_callback,
const std::string& consistency_context) const;
/**
* @brief Categorizes a single entry and persists the result.
* @param llm LLM client used for the request.
* @param is_local_llm True when using a local LLM backend.
* @param entry File entry to categorize.
* @param prompt_override Optional prompt override.
* @param suggested_name Optional suggested name for renaming.
* @param stop_flag Cancellation flag.
* @param progress_callback Progress updates callback.
* @param recategorization_callback Callback for re-categorization events.
* @param session_history Mutable session history for consistency hints.
* @return Categorized entry when successful.
*/
std::optional categorize_single_entry(
ILLMClient& llm,
bool is_local_llm,
const FileEntry& entry,
const std::optional& prompt_override,
const std::string& suggested_name,
std::atomic& stop_flag,
const ProgressCallback& progress_callback,
const RecategorizationCallback& recategorization_callback,
SessionHistoryMap& session_history) const;
/**
* @brief Combines language, whitelist, and hint blocks into a single prompt context.
* @param hint_block Consistency hint block.
* @return Combined prompt context.
*/
std::string build_combined_context(const std::string& hint_block) const;
DatabaseManager::ResolvedCategory localize_resolved_category(
ILLMClient& llm,
const DatabaseManager::ResolvedCategory& resolved) const;
std::optional translate_resolved_category(
ILLMClient& llm,
const DatabaseManager::ResolvedCategory& resolved) const;
/**
* @brief Runs the categorization flow with cache handling for a single entry.
* @param llm LLM client used for the request.
* @param is_local_llm True when using a local LLM backend.
* @param entry File entry to categorize.
* @param display_path Display path for logging.
* @param dir_path Full directory path for cache lookup.
* @param prompt_name Name used in the prompt.
* @param prompt_path Path used in the prompt.
* @param progress_callback Progress updates callback.
* @param combined_context Combined prompt context.
* @return Resolved category for the item.
*/
DatabaseManager::ResolvedCategory run_categorization_with_cache(
ILLMClient& llm,
bool is_local_llm,
const FileEntry& entry,
const std::string& display_path,
const std::string& dir_path,
const std::string& prompt_name,
const std::string& prompt_path,
const ProgressCallback& progress_callback,
const std::string& combined_context) const;
/**
* @brief Handles empty or invalid categorization results.
* @param entry File entry being categorized.
* @param dir_path Directory path of the entry.
* @param resolved Resolved category data.
* @param used_consistency_hints True if hints were applied.
* @param is_local_llm True when using a local LLM backend.
* @param recategorization_callback Callback for re-categorization events.
* @return Optional replacement categorization when a retry is needed.
*/
std::optional handle_empty_result(
const FileEntry& entry,
const std::string& dir_path,
const DatabaseManager::ResolvedCategory& resolved,
bool used_consistency_hints,
bool is_local_llm,
const RecategorizationCallback& recategorization_callback) const;
/**
* @brief Persists categorization results and updates session hint history.
* @param entry File entry being categorized.
* @param dir_path Directory path of the entry.
* @param resolved Resolved category data.
* @param used_consistency_hints True if hints were applied.
* @param suggested_name Suggested rename value.
* @param session_history Session history for consistency hints.
*/
void update_storage_with_result(const FileEntry& entry,
const std::string& dir_path,
const DatabaseManager::ResolvedCategory& resolved,
bool used_consistency_hints,
const std::string& suggested_name,
SessionHistoryMap& session_history) const;
/**
* @brief Runs the LLM request with a timeout for the given item.
* @param llm LLM client used for the request.
* @param item_name Display name for the item.
* @param item_path Display path for the item.
* @param file_type File or directory.
* @param is_local_llm True when using a local LLM backend.
* @param consistency_context Consistency hints block.
* @return Raw LLM response string.
*/
std::string run_llm_with_timeout(
ILLMClient& llm,
const std::string& item_name,
const std::string& item_path,
FileType file_type,
bool is_local_llm,
const std::string& consistency_context) const;
/**
* @brief Resolves the LLM timeout based on runtime and environment settings.
* @param is_local_llm True when using a local LLM backend.
* @return Timeout in seconds.
*/
int resolve_llm_timeout(bool is_local_llm) const;
/**
* @brief Launches an asynchronous LLM categorization request.
* @param llm LLM client used for the request.
* @param item_name Display name for the item.
* @param item_path Display path for the item.
* @param file_type File or directory.
* @param consistency_context Consistency hints block.
* @return Future that yields the raw LLM response.
*/
std::future start_llm_future(ILLMClient& llm,
const std::string& item_name,
const std::string& item_path,
FileType file_type,
const std::string& consistency_context) const;
/**
* @brief Builds a whitelist context block for the prompt.
* @return Whitelist prompt section.
*/
std::string build_whitelist_context() const;
/**
* @brief Builds a prompt instruction for non-English category languages.
* @return Language instruction block or empty string.
*/
std::string build_category_language_context() const;
/**
* @brief Collects recent category assignments to provide consistency hints.
* @param signature Signature key for the file type/extension.
* @param session_history In-memory history of assignments.
* @param extension File extension.
* @param file_type File or directory.
* @return List of up to kMaxConsistencyHints pairs.
*/
std::vector collect_consistency_hints(
const std::string& signature,
const SessionHistoryMap& session_history,
const std::string& extension,
FileType file_type) const;
/**
* @brief Returns a cached categorization if it is valid for the entry.
* @param item_name Display name for the item.
* @param current_path Display path for the current on-disk location.
* @param categorization_path Effective path used for categorization context.
* @param dir_path Full directory path for cache lookup.
* @param file_type File or directory.
* @param progress_callback Progress updates callback.
* @return Resolved category when cache is valid.
*/
std::optional try_cached_categorization(
const std::string& item_name,
const std::string& current_path,
const std::string& categorization_path,
const std::string& dir_path,
FileType file_type,
const ProgressCallback& progress_callback) const;
/**
* @brief Ensures remote credentials are present and reports errors via progress callback.
* @param item_name Display name for the item.
* @param progress_callback Progress updates callback.
* @return True when credentials are present or not required.
*/
bool ensure_remote_credentials_for_request(
const std::string& item_name,
const ProgressCallback& progress_callback) const;
/**
* @brief Categorizes a single item by calling the LLM and validating the response.
* @param llm LLM client used for the request.
* @param is_local_llm True when using a local LLM backend.
* @param display_name Display name for logging.
* @param display_path Display path for logging.
* @param prompt_name Name used in the prompt.
* @param prompt_path Path used in the prompt.
* @param file_type File or directory.
* @param progress_callback Progress updates callback.
* @param consistency_context Consistency hints block.
* @return Resolved category for the item.
*/
DatabaseManager::ResolvedCategory categorize_via_llm(
ILLMClient& llm,
bool is_local_llm,
const std::string& display_name,
const std::string& display_path,
const std::string& prompt_name,
const std::string& prompt_path,
FileType file_type,
const ProgressCallback& progress_callback,
const std::string& consistency_context) const;
/**
* @brief Emits a formatted progress message for a categorization event.
* @param progress_callback Progress updates callback.
* @param source Label for the progress source.
* @param item_name Display name for the item.
* @param resolved Resolved category data.
* @param current_path Display path for the current on-disk location.
* @param categorization_path Effective path used for categorization context.
*/
void emit_progress_message(const ProgressCallback& progress_callback,
std::string_view source,
const std::string& item_name,
const DatabaseManager::ResolvedCategory& resolved,
const std::string& current_path,
const std::string& categorization_path) const;
/**
* @brief Builds a signature key for consistency hints.
* @param file_type File or directory.
* @param extension File extension.
* @return Signature key for consistency lookup.
*/
static std::string make_file_signature(FileType file_type, const std::string& extension);
/**
* @brief Extracts a lowercase file extension (including the dot).
* @param file_name File name to inspect.
* @return Lowercase extension with dot, or empty string when none exists.
*/
static std::string extract_extension(const std::string& file_name);
/**
* @brief Appends a unique, sanitized hint to the target list.
* @param target Hint list to update.
* @param candidate Candidate pair to append.
* @return True when the hint was added.
*/
static bool append_unique_hint(std::vector& target, const CategoryPair& candidate);
/**
* @brief Updates in-memory hint history with the latest assignment.
* @param history Hint history to update.
* @param assignment Category/subcategory assignment to record.
*/
static void record_session_assignment(HintHistory& history, const CategoryPair& assignment);
/**
* @brief Formats consistency hints into a prompt block.
* @param hints Consistency hints to format.
* @return Prompt block string.
*/
std::string format_hint_block(const std::vector& hints) const;
#ifdef AI_FILE_SORTER_TEST_BUILD
friend class CategorizationServiceTestAccess;
#endif
Settings& settings;
DatabaseManager& db_manager;
std::shared_ptr core_logger;
};
#endif
================================================
FILE: app/include/CategorizationServiceTestAccess.hpp
================================================
#pragma once
#ifdef AI_FILE_SORTER_TEST_BUILD
#include "CategorizationService.hpp"
/**
* @brief Test-only accessors for CategorizationService internals.
*/
class CategorizationServiceTestAccess {
public:
/**
* @brief Returns the whitelist context string used in prompts.
* @param service CategorizationService instance under test.
* @return Prompt snippet describing allowed categories/subcategories.
*/
static std::string build_whitelist_context(const CategorizationService& service) {
return service.build_whitelist_context();
}
/**
* @brief Returns the category-language context string used in prompts.
* @param service CategorizationService instance under test.
* @return Prompt snippet describing the required category language.
*/
static std::string build_category_language_context(const CategorizationService& service) {
return service.build_category_language_context();
}
};
#endif // AI_FILE_SORTER_TEST_BUILD
================================================
FILE: app/include/CategorizationSession.hpp
================================================
#ifndef CATEGORIZATIONSESSION_HPP
#define CATEGORIZATIONSESSION_HPP
#include
#include
class CategorizationSession {
std::string key;
std::string model;
std::string base_url;
public:
/**
* @brief Construct a session for OpenAI-compatible requests.
*/
CategorizationSession(std::string api_key, std::string model, std::string base_url = std::string());
~CategorizationSession();
LLMClient create_llm_client() const;
};
#endif
================================================
FILE: app/include/CategoryLanguage.hpp
================================================
#ifndef CATEGORYLANGUAGE_HPP
#define CATEGORYLANGUAGE_HPP
#include
#include
#include
enum class CategoryLanguage {
Dutch,
English,
French,
German,
Italian,
Polish,
Portuguese,
Spanish,
Turkish
};
inline QString categoryLanguageToString(CategoryLanguage language)
{
static const std::array names = {
"Dutch",
"English",
"French",
"German",
"Italian",
"Polish",
"Portuguese",
"Spanish",
"Turkish"
};
const auto idx = static_cast(language);
if (idx < names.size()) {
return QString::fromUtf8(names[idx]);
}
return QStringLiteral("English");
}
inline CategoryLanguage categoryLanguageFromString(const QString& value)
{
const QString lowered = value.toLower();
static const std::array, 9> mapping = {{
{QStringLiteral("dutch"), CategoryLanguage::Dutch},
{QStringLiteral("english"), CategoryLanguage::English},
{QStringLiteral("french"), CategoryLanguage::French},
{QStringLiteral("german"), CategoryLanguage::German},
{QStringLiteral("italian"), CategoryLanguage::Italian},
{QStringLiteral("polish"), CategoryLanguage::Polish},
{QStringLiteral("portuguese"), CategoryLanguage::Portuguese},
{QStringLiteral("spanish"), CategoryLanguage::Spanish},
{QStringLiteral("turkish"), CategoryLanguage::Turkish},
}};
for (const auto& entry : mapping) {
if (lowered == entry.first) {
return entry.second;
}
}
return CategoryLanguage::English;
}
inline std::string categoryLanguageDisplay(CategoryLanguage lang) {
return categoryLanguageToString(lang).toStdString();
}
#endif // CATEGORYLANGUAGE_HPP
================================================
FILE: app/include/ConsistencyPassService.hpp
================================================
#ifndef CONSISTENCY_PASS_SERVICE_HPP
#define CONSISTENCY_PASS_SERVICE_HPP
#include "CategoryLanguage.hpp"
#include "DatabaseManager.hpp"
#include "Types.hpp"
#include
#include
#include
#include
#include
#include
#include
class ILLMClient;
namespace spdlog { class logger; }
class ConsistencyPassService {
public:
using ProgressCallback = std::function;
ConsistencyPassService(DatabaseManager& db_manager,
std::shared_ptr logger);
void set_prompt_logging_enabled(bool enabled);
void run(std::vector& categorized_files,
std::vector& newly_categorized_files,
std::function()> llm_factory,
std::atomic& stop_flag,
CategoryLanguage category_language,
const ProgressCallback& progress_callback) const;
private:
std::unique_ptr create_llm(std::function()> llm_factory) const;
void process_chunks(ILLMClient& llm,
const std::vector>& taxonomy,
std::vector& categorized_files,
std::unordered_map& items_by_key,
std::unordered_map& new_items_by_key,
std::atomic& stop_flag,
CategoryLanguage category_language,
const ProgressCallback& progress_callback) const;
void process_chunk(const std::vector& chunk,
size_t start_index,
size_t end_index,
size_t total_items,
ILLMClient& llm,
const std::vector>& taxonomy,
std::unordered_map& items_by_key,
std::unordered_map& new_items_by_key,
CategoryLanguage category_language,
const ProgressCallback& progress_callback) const;
void log_chunk_items(const std::vector& chunk, const char* stage) const;
bool apply_harmonized_response(const std::string& response,
const std::vector& chunk,
std::unordered_map& items_by_key,
std::unordered_map& new_items_by_key,
const ProgressCallback& progress_callback,
DatabaseManager& db_manager,
CategoryLanguage category_language) const;
DatabaseManager& db_manager;
std::shared_ptr logger;
mutable bool prompt_logging_enabled{false};
};
#endif
================================================
FILE: app/include/CustomApiDialog.hpp
================================================
#ifndef CUSTOMAPIDIALOG_HPP
#define CUSTOMAPIDIALOG_HPP
#include "Types.hpp"
#include
#include
class QCheckBox;
class QLineEdit;
class QPushButton;
class QTextEdit;
/**
* @brief Dialog for creating or editing custom OpenAI-compatible API entries.
*/
class CustomApiDialog : public QDialog
{
Q_DECLARE_TR_FUNCTIONS(CustomApiDialog)
public:
/**
* @brief Construct a dialog for a new custom API entry.
* @param parent Parent widget.
*/
explicit CustomApiDialog(QWidget* parent = nullptr);
/**
* @brief Construct a dialog pre-populated with an existing entry.
* @param parent Parent widget.
* @param existing Existing custom API values to edit.
*/
explicit CustomApiDialog(QWidget* parent, const CustomApiEndpoint& existing);
/**
* @brief Return the dialog values as a CustomApiEndpoint entry.
*/
CustomApiEndpoint result() const;
private:
/**
* @brief Build the dialog layout and widgets.
*/
void setup_ui();
/**
* @brief Connect widget signals to validation and handlers.
*/
void wire_signals();
/**
* @brief Apply existing values to the input fields.
* @param existing Existing custom API values to load.
*/
void apply_existing(const CustomApiEndpoint& existing);
/**
* @brief Validate inputs and update the ok button state.
*/
void validate_inputs();
QLineEdit* name_edit{nullptr};
QTextEdit* description_edit{nullptr};
QLineEdit* base_url_edit{nullptr};
QLineEdit* model_edit{nullptr};
QLineEdit* api_key_edit{nullptr};
QCheckBox* show_api_key_checkbox{nullptr};
QPushButton* ok_button{nullptr};
};
#endif // CUSTOMAPIDIALOG_HPP
================================================
FILE: app/include/CustomLLMDialog.hpp
================================================
#ifndef CUSTOMLLMDIALOG_HPP
#define CUSTOMLLMDIALOG_HPP
#include "Types.hpp"
#include
#include
class QLineEdit;
class QPushButton;
class QTextEdit;
/**
* @brief Dialog for creating or editing custom local LLM entries.
*/
class CustomLLMDialog : public QDialog
{
Q_DECLARE_TR_FUNCTIONS(CustomLLMDialog)
public:
/**
* @brief Construct a dialog for a new custom LLM entry.
* @param parent Parent widget.
*/
explicit CustomLLMDialog(QWidget* parent = nullptr);
/**
* @brief Construct a dialog pre-populated with an existing entry.
* @param parent Parent widget.
* @param existing Existing custom LLM values to edit.
*/
explicit CustomLLMDialog(QWidget* parent, const CustomLLM& existing);
/**
* @brief Return the dialog values as a CustomLLM entry.
*/
CustomLLM result() const;
private:
/**
* @brief Build the dialog layout and widgets.
*/
void setup_ui();
/**
* @brief Connect widget signals to validation and handlers.
*/
void wire_signals();
/**
* @brief Apply existing values to the input fields.
* @param existing Existing custom LLM values to load.
*/
void apply_existing(const CustomLLM& existing);
/**
* @brief Validate inputs and update the ok button state.
*/
void validate_inputs();
/**
* @brief Open a file picker to select a local model file.
*/
void browse_for_file();
QLineEdit* name_edit{nullptr};
QTextEdit* description_edit{nullptr};
QLineEdit* path_edit{nullptr};
QPushButton* browse_button{nullptr};
QPushButton* ok_button{nullptr};
};
#endif // CUSTOMLLMDIALOG_HPP
================================================
FILE: app/include/DatabaseManager.hpp
================================================
#ifndef DATABASEMANAGER_HPP
#define DATABASEMANAGER_HPP
#include "CategoryLanguage.hpp"
#include "Types.hpp"
#include
#include