diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 65adca0..4e9193f 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -16,7 +16,7 @@ on: default: "dev" env: QT_VERSION: "6.7.2" - GO_VERSION: "1.23.0" + GO_VERSION: "1.23.4" LIBPLIST_VER: "2.7.0" LIBTATSU_VER: "1.0.5" LIBIMOBILEDEVICE_GLUE_VER: "1.3.2" diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 5709f83..1e6e04e 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -16,7 +16,7 @@ on: default: "dev" env: QT_VERSION: "6.7.2" - GO_VERSION: "1.23.0" + GO_VERSION: "1.23.4" LIBPLIST_VER: "2.7.0" LIBTATSU_VER: "1.0.5" LIBIMOBILEDEVICE_GLUE_VER: "1.3.2" @@ -55,7 +55,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: "^1.23.0" + go-version: "^${{ env.GO_VERSION }}" - name: Install macOS dependencies run: | diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index a23e6f9..8270adb 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -15,8 +15,8 @@ on: type: string default: "dev" env: - QT_VERSION: "6.8.0" - GO_VERSION: "1.23.0" + QT_VERSION: "6.8.3" + GO_VERSION: "1.23.4" LIBPLIST_VER: "2.7.0" LIBTATSU_VER: "1.0.5" LIBIMOBILEDEVICE_GLUE_VER: "1.3.2" @@ -26,7 +26,7 @@ env: jobs: build-windows: - runs-on: windows-latest + runs-on: windows-2022 defaults: run: shell: msys2 {0} @@ -43,31 +43,41 @@ jobs: msystem: mingw64 release: false update: false - install: >- - coreutils - base-devel - git - make - libtool - autoconf - automake-wrapper - p7zip - mingw-w64-x86_64-gcc - mingw-w64-x86_64-cmake - mingw-w64-x86_64-pugixml - mingw-w64-x86_64-libusb - mingw-w64-x86_64-qrencode - mingw-w64-x86_64-curl - mingw-w64-x86_64-openssl - mingw-w64-x86_64-libzip - mingw-w64-x86_64-go - mingw-w64-x86_64-gstreamer - mingw-w64-x86_64-gst-plugins-base - mingw-w64-x86_64-gst-plugins-good - mingw-w64-x86_64-gst-plugins-bad - mingw-w64-x86_64-gst-plugins-ugly - mingw-w64-x86_64-gst-libav - mingw-w64-x86_64-libheif + + - name: Use msys2 archive + shell: pwsh + run: | + $date = "2025-06-22" + & "./util/get-msys2-archive.ps1" -Date $date + + - name: Install deps using pacman + run: | + pacman -S --needed --noconfirm \ + coreutils \ + base-devel \ + git \ + make \ + libtool \ + autoconf \ + automake-wrapper \ + p7zip \ + mingw-w64-x86_64-gcc \ + mingw-w64-x86_64-cmake \ + mingw-w64-x86_64-pugixml \ + mingw-w64-x86_64-libusb \ + mingw-w64-x86_64-qrencode \ + mingw-w64-x86_64-curl \ + mingw-w64-x86_64-openssl \ + mingw-w64-x86_64-libzip \ + mingw-w64-x86_64-go \ + mingw-w64-x86_64-gstreamer \ + mingw-w64-x86_64-gst-plugins-base \ + mingw-w64-x86_64-gst-plugins-good \ + mingw-w64-x86_64-gst-plugins-bad \ + mingw-w64-x86_64-gst-plugins-ugly \ + mingw-w64-x86_64-gst-libav \ + mingw-w64-x86_64-libheif \ + mingw-w64-x86_64-libarchive - uses: actions/setup-dotnet@v5 with: @@ -92,7 +102,31 @@ jobs: - name: Install WinFsp shell: pwsh run: | - choco install winfsp -y + $maxRetries = 3 + $retryDelaySeconds = 10 + $attempt = 0 + $success = $false + + while ($attempt -lt $maxRetries -and -not $success) { + $attempt++ + Write-Host "Attempt $attempt to install WinFsp..." + choco install winfsp -y + if ($LASTEXITCODE -eq 0) { + Write-Host "WinFsp installed successfully on attempt $attempt." + $success = $true + } else { + Write-Warning "WinFsp installation failed on attempt $attempt (exit code: $LASTEXITCODE)." + if ($attempt -lt $maxRetries) { + Write-Host "Retrying in $retryDelaySeconds seconds..." + Start-Sleep -Seconds $retryDelaySeconds + } + } + } + + if (-not $success) { + Write-Error "Failed to install WinFsp after $maxRetries attempts." + exit 1 # Explicitly fail the step if all retries fail + } - name: Download and Extract Bonjour SDK run: | diff --git a/.gitignore b/.gitignore index fe4b57c..449b962 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,7 @@ build-dir *.so *.exe *.dll -devdiskimgs \ No newline at end of file +devdiskimgs +.qt +.qtcreator +CMakeFiles \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index a5f05f3..2735eb4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "lib/airplay"] - path = lib/airplay - url = https://github.com/uncor3/airplay [submodule "lib/ipatool-go"] path = lib/ipatool-go url = https://github.com/uncor3/libipatool-go.git @@ -10,3 +7,6 @@ [submodule "lib/zupdater"] path = lib/zupdater url = https://github.com/libZQT/ZUpdater +[submodule "lib/uxplay"] + path = lib/uxplay + url = https://github.com/iDescriptor/uxplay diff --git a/CMakeLists.txt b/CMakeLists.txt index 327ce56..d57be51 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,9 @@ cmake_minimum_required(VERSION 3.16) -project(iDescriptor VERSION 0.1.2 LANGUAGES CXX) +project(iDescriptor VERSION 0.2.0 LANGUAGES CXX) + +if(WIN32) + set(PKG_CONFIG_EXECUTABLE "C:/msys64/mingw64/bin/pkg-config.exe" CACHE FILEPATH "" FORCE) +endif() # Feature options option(ENABLE_RECOVERY_DEVICE_SUPPORT "Enable recovery device support (requires libirecovery)" ON) @@ -115,11 +119,14 @@ find_library(TATSU_LIBRARY REQUIRED ) -# Add QR code generation library pkg_check_modules(QRENCODE REQUIRED IMPORTED_TARGET libqrencode) pkg_check_modules(HEIF REQUIRED IMPORTED_TARGET libheif) pkg_check_modules(ZIP REQUIRED IMPORTED_TARGET libzip) +if(WIN32) + pkg_check_modules(LIBARCHIVE REQUIRED IMPORTED_TARGET libarchive) +endif() + # Add FFmpeg libraries for video thumbnail generation pkg_check_modules(AVFORMAT REQUIRED IMPORTED_TARGET libavformat) pkg_check_modules(AVCODEC REQUIRED IMPORTED_TARGET libavcodec) @@ -129,8 +136,7 @@ pkg_check_modules(SWSCALE REQUIRED IMPORTED_TARGET libswscale) if(ENABLE_RECOVERY_DEVICE_SUPPORT) find_library(IRECOVERY_LIBRARY NAMES irecovery-1.0 - PATHS ${CUSTOM_LIB_PATH} - NO_DEFAULT_PATH + ${CUSTOM_FIND_LIB_ARGS} ) if(IRECOVERY_LIBRARY) message(STATUS "Building with recovery device support enabled") @@ -217,7 +223,7 @@ if (NOT ENABLE_RECOVERY_DEVICE_SUPPORT) ) endif() -add_subdirectory(lib/airplay) +add_subdirectory(lib/uxplay) add_subdirectory(lib/ipatool-go) add_subdirectory(lib/zupdater) @@ -283,7 +289,7 @@ target_link_libraries(iDescriptor PRIVATE PkgConfig::AVCODEC PkgConfig::AVUTIL PkgConfig::SWSCALE - airplay + uxplay ipatool-go ZUpdater ) @@ -294,7 +300,9 @@ if(ENABLE_RECOVERY_DEVICE_SUPPORT) endif() target_include_directories(iDescriptor PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/lib/zupdater/src + ${CMAKE_CURRENT_SOURCE_DIR}/lib/zupdater/src + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/lib ) if(APPLE) @@ -303,9 +311,12 @@ if(APPLE) ${CORE_SERVICES_FRAMEWORK}) message(STATUS "Using macOS Bonjour framework for network service discovery") elseif (WIN32) + target_link_libraries(iDescriptor PRIVATE PkgConfig::LIBARCHIVE) find_path(DNSSD_INCLUDE_DIR dns_sd.h HINTS ${BONJOUR_SDK}/Include ) - target_include_directories( iDescriptor PRIVATE ${DNSSD_INCLUDE_DIR} ) - message( STATUS "Using Bonjour SDK for network service discovery" ) + # $<$ fixes winres compiler errors + target_include_directories(iDescriptor PRIVATE + $<$:${DNSSD_INCLUDE_DIR}> + ) else() pkg_check_modules(AVAHI_CLIENT REQUIRED IMPORTED_TARGET avahi-client) diff --git a/README.md b/README.md index 4abae7c..22877e2 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Open the `.dmg` and drag iDescriptor to Applications.
- After moving the app to Applications, run the code below +After moving the app to Applications, run the code below
```shell @@ -105,14 +105,28 @@ make sure to do "sudo pacman -Syu" otherwise it's not going to find libimobilede

+
+
+
+ +## Good News! + +### iDescriptor v0.3.0 will feature **WIRELESS CONNECTION** support! + +Learn more about our roadmap [here](#roadmap). + +
+
+
+ ## Features ### Connection -| Feature | Status | Notes | -| --------------------------- | -------------------- | --------------------------------------------- | -| USB Connection | ✅ Implemented | Fully supported on Windows, macOS, and Linux. | -| Wireless Connection (Wi‑Fi) | ⚠️ To be implemented | - | +| Feature | Status | Notes | +| ------------------- | -------------------- | --------------------------------------------------------------- | +| USB Connection | ✅ Implemented | Fully supported on Windows, macOS, and Linux. | +| Wireless Connection | ⚠️ Under Development | Currently under development, planned to be released in v0.3.0 | ### Tools @@ -243,9 +257,11 @@ You might get this pop-up on any platform this is because this app uses secure b ## Become a Sponsor -Please support us at +
+You can become a sponsor from GitHub Sponsors or AppImage +
## Thanks @@ -277,13 +293,26 @@ sudo udevadm trigger Contributions are welcome! -You can check the source code in some places we have TODOs and FIXMEs that you can work on. +We actively develop on dev branch, so please base your pull requests off of that branch. -For example +You can also send a pr to main branch but it should be something related to building, publish to some package manager or documentation. -- [Photos.sqlite](https://github.com/iDescriptor/iDescriptor/blob/main/src/gallerywidget.cpp) -Or if you'd like to introduce new features, feel free to open an issue or a pull request! +## Roadmap + +**Planned Features for v0.3.0:** + +- Migrate to idevice-rs `99% done` +- Wireless Connection Support `DONE` +- Install IPA files directly from the app +- Virtual Location Support for iOS 17 and above +- Complete iOS 26 Support +- Migrate to UCRT for better performance and stability ``Windows`` +- New UI/UX improvements +- Read gallery from Photos.sqlite (maybe delayed to v0.4.0) + + +**You can always become a sponsor/donate to request/prioritize a feature or speed up the development process!** ## Damaged Error on macOS diff --git a/cmake/win-deploy.cmake b/cmake/win-deploy.cmake index 04eb3d8..115ea9b 100644 --- a/cmake/win-deploy.cmake +++ b/cmake/win-deploy.cmake @@ -1,5 +1,3 @@ -# Windows deployment script for Qt applications with MinGW/MSYS2 -# This script handles Qt deployment, runtime DLL copying, and GStreamer plugins # Strip quotes from all path variables if they exist string(REPLACE "\"" "" EXECUTABLE_PATH "${EXECUTABLE_PATH}") @@ -47,6 +45,8 @@ message("SUCCESS: Executable found at: ${EXECUTABLE_PATH}") message("Running windeployqt6 to deploy Qt dependencies (without compiler runtime)...") +# required if Qt is installed via MSYS2 +set(ENV{PATH} "/c/msys64/mingw64/bin:/c/msys64/mingw64/share/qt6/bin:$ENV{PATH}") message("Executing: ${QT_BIN_PATH}/windeployqt6.exe --qmldir ${QML_SOURCE_DIR} --dir ${OUTPUT_DIR} --plugindir ${OUTPUT_DIR}/plugins ${EXECUTABLE_PATH}") @@ -141,6 +141,11 @@ set(WANTED_PLUGINS "libgstvideorate" "libgstoverlaycomposition" "libgstfaad" + "libgstvideoparsersbad" + "libgstvideofilter" + "libgstvideoconvertscale" + "libgstmultifile" + "libgstjpeg" ) file(MAKE_DIRECTORY "${OUTPUT_DIR}/gstreamer-1.0") @@ -162,14 +167,14 @@ endforeach() message("Successfully copied ${COPIED_PLUGIN_COUNT} requested GStreamer plugins") -# Step 4: Manually copy the correct MSYS2 MinGW runtime DLLs. -# This ensures the versions required by GStreamer/FFmpeg are used. set(ADDITIONAL_DLLS "libgcc_s_seh-1.dll" "libstdc++-6.dll" "libwinpthread-1.dll" "libgstreamer-1.0-0.dll" "libgstbase-1.0-0.dll" + "libgstcodecparsers-1.0-0.dll" + "libgstcodecs-1.0-0.dll" "libgobject-2.0-0.dll" "libglib-2.0-0.dll" "libintl-8.dll" @@ -216,6 +221,57 @@ set(ADDITIONAL_DLLS "libpcre2-8-0.dll" "libffi-8.dll" "libgmodule-2.0-0.dll" + "libhwy.dll" + "libmp3lame-0.dll" + "librsvg-2-2.dll" + "libwebp-7.dll" + "libthai-0.dll" + "libjxl.dll" + "libdatrie-1.dll" + "libwebpmux-3.dll" + "libx264-164.dll" + "libtasn1-6.dll" + "libgsm.dll" + "libcairo-gobject-2.dll" + "libvorbis-0.dll" + "libgio-2.0-0.dll" + "libgmp-10.dll" + "libmodplug-1.dll" + "libopus-0.dll" + "libpangowin32-1.0-0.dll" + "libspeex-1.dll" + "libogg-0.dll" + "libzvbi-0.dll" + "libpixman-1-0.dll" + "libsrt.dll" + "libjxl_threads.dll" + "libgnutls-30.dll" + "libp11-kit-0.dll" + "libopencore-amrwb-0.dll" + "libtheoradec-2.dll" + "libvpx-1.dll" + "libgme.dll" + "libhogweed-6.dll" + "liblc3-1.dll" + "libpango-1.0-0.dll" + "xvidcore.dll" + "libopencore-amrnb-0.dll" + "libtiff-6.dll" + "libxml2-2.dll" + "libjbig-0.dll" + "libLerc.dll" + "libjxl_cms.dll" + "libgdk_pixbuf-2.0-0.dll" + "libvorbisenc-2.dll" + "libsoxr.dll" + "librtmp-1.dll" + "libcairo-2.dll" + "libdeflate.dll" + "libpangocairo-1.0-0.dll" + "libpangoft2-1.0-0.dll" + "libtheoraenc-2.dll" + "libbluray-2.dll" + "libnettle-8.dll" ) message("Copying additional MinGW runtime DLLs from MSYS2...") @@ -224,6 +280,8 @@ foreach(DLL_NAME ${ADDITIONAL_DLLS}) if(EXISTS ${DLL_PATH}) message("Copying additional DLL: ${DLL_NAME}") file(COPY ${DLL_PATH} DESTINATION ${OUTPUT_DIR}) + else() + message(WARNING "Additional DLL not found: ${DLL_NAME} (searched ${MSYS2_BIN_PATH})") endif() endforeach() diff --git a/lib/airplay b/lib/airplay deleted file mode 160000 index 9fe5788..0000000 --- a/lib/airplay +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9fe5788667f0faab9722bbd9a7887eba7a2a4088 diff --git a/lib/uxplay b/lib/uxplay new file mode 160000 index 0000000..758fec4 --- /dev/null +++ b/lib/uxplay @@ -0,0 +1 @@ +Subproject commit 758fec4be61f854dd22fe2c16b625ca3053adb4d diff --git a/lib/zupdater b/lib/zupdater index 61aea85..8ff5248 160000 --- a/lib/zupdater +++ b/lib/zupdater @@ -1 +1 @@ -Subproject commit 61aea855c82a67a3536ec00aca7acd583bcbfc83 +Subproject commit 8ff5248c442a3b176149b7779fa4ce3518c80b12 diff --git a/resources.qrc b/resources.qrc index 7bc83ba..fc960e4 100644 --- a/resources.qrc +++ b/resources.qrc @@ -61,7 +61,7 @@ resources/iphone-mockups/iphone-15.png resources/iphone-mockups/iphone-16.png resources/connect.png - resources/airplayer-tutorial.mp4 + resources/airplay-tutorial.mp4 resources/ipad-mockups/ipad.png DeveloperDiskImages.json resources/keychain.mp4 diff --git a/resources/airplay-tutorial.mp4 b/resources/airplay-tutorial.mp4 new file mode 100644 index 0000000..860dcc1 Binary files /dev/null and b/resources/airplay-tutorial.mp4 differ diff --git a/resources/airplayer-tutorial.mp4 b/resources/airplayer-tutorial.mp4 deleted file mode 100755 index 16d39f0..0000000 Binary files a/resources/airplayer-tutorial.mp4 and /dev/null differ diff --git a/scripts/deploy-appimage.sh b/scripts/deploy-appimage.sh index a8d511d..8c44421 100755 --- a/scripts/deploy-appimage.sh +++ b/scripts/deploy-appimage.sh @@ -90,6 +90,24 @@ plugins=( "libgstapp.so" "libgstautodetect.so" "libgstaudioresample.so" + "libgstvideoparsersbad.so" + "libgstvaapi.so" + "libgstva.so" + "libgstvideo4linux2.so" + "libgstvideoconvertscale.so" + "libgstvideoconvert.so" + "libgstvideoscale.so" + "libgstvideofilter.so" + "libgstjpeg.so" + "libgstimagefreeze.so" + "libgstximagesink.so" + "libgstxvimagesink.so" + "libgstgtk.so" + "libgstgl.so" + "libgstrtp.so" + "libgstrtpmanager.so" + "libgsttypefindfunctions.so" + "libgstisomp4.so" ) for i in "${plugins[@]}"; do diff --git a/scripts/deploy-dmg.sh b/scripts/deploy-dmg.sh index d64e795..c30e205 100755 --- a/scripts/deploy-dmg.sh +++ b/scripts/deploy-dmg.sh @@ -34,14 +34,22 @@ mkdir -p "${GST_PLUGIN_DIR}" PLUGINS=( "libgstapp" "libgstaudioconvert" + "libgstaudioresample" "libgstautodetect" "libgstavi" "libgstcoreelements" + "libgstimagefreeze" + "libgstjpeg" "libgstlevel" "libgstlibav" "libgstosxaudio" "libgstplayback" + "libgstvideobox" + "libgstvideofilter" + "libgstvideoparsersbad" "libgstvolume" + "libgstvideoconvertscale" + "libgstvideorate" ) BREW_PREFIX="$(brew --prefix)" @@ -74,6 +82,7 @@ GST_LIBS=( "libgsttag-1.0.0.dylib" "libgstriff-1.0.0.dylib" "libgstcodecparsers-1.0.0.dylib" + "libgstcodecs-1.0.0.dylib" "libgstrtp-1.0.0.dylib" "libgstsdp-1.0.0.dylib" "libglib-2.0.0.dylib" @@ -89,7 +98,7 @@ for lib in "${GST_LIBS[@]}"; do if [ -f "${BREW_PREFIX}/lib/${lib}" ]; then cp "${BREW_PREFIX}/lib/${lib}" "${FRAMEWORKS_DIR}/" install_name_tool -id "@rpath/${lib}" "${FRAMEWORKS_DIR}/${lib}" - echo "✓ Copied and fixed ID for ${lib}" + echo "Fixed rpath for ${lib}" fi done @@ -111,12 +120,22 @@ for lib_base in "${FFMPEG_LIBS[@]}"; do cp "$lib_path" "${FRAMEWORKS_DIR}/" #These maybe unneeded, macdeployqt already does this but just in case install_name_tool -id "@rpath/${lib_name}" "${FRAMEWORKS_DIR}/${lib_name}" - echo "Copied and fixed rpath for ${lib_name}" + echo "Fixed rpath for ${lib_name}" else echo "Warning: ${lib_base} library not found in ${FFMPEG_LIB_DIR}" fi done +echo "Bundling iproxy..." +IPROXY_PATH="$(which iproxy)" +if [ -z "${IPROXY_PATH}" ]; then + echo "Error: iproxy not found in PATH" + exit 1 +fi + +cp "${IPROXY_PATH}" "${APP_PATH}/Contents/MacOS/" +chmod +x "${APP_PATH}/Contents/MacOS/iproxy" + macdeployqt "${APP_PATH}" -qmldir=qml -verbose=2 codesign --force --deep -s - "${APP_PATH}" diff --git a/src/afcexplorerwidget.cpp b/src/afcexplorerwidget.cpp index df98b72..8ad9928 100644 --- a/src/afcexplorerwidget.cpp +++ b/src/afcexplorerwidget.cpp @@ -206,8 +206,9 @@ void AfcExplorerWidget::loadPath(const QString &path) updateAddressBar(path); updateNavigationButtons(); - AFCFileTree tree = - ServiceManager::safeGetFileTree(m_device, path.toStdString(), m_afc); + AFCFileTree tree = ServiceManager::safeGetFileTree( + m_device, path.toStdString(), true, m_afc); + if (!tree.success) { showErrorState(); return; @@ -525,8 +526,6 @@ void AfcExplorerWidget::setupFileExplorer() m_navWidget = new QWidget(); m_navWidget->setObjectName("navWidget"); m_navWidget->setFocusPolicy(Qt::StrongFocus); // Make it focusable - connect(qApp, &QApplication::paletteChanged, this, - &AfcExplorerWidget::updateNavStyles); m_navWidget->setMaximumWidth(500); m_navWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); @@ -693,6 +692,8 @@ void AfcExplorerWidget::onAddToFavoritesClicked() void AfcExplorerWidget::updateNavStyles() { + if (!m_navWidget || !m_addressBar) + return; bool isDark = isDarkMode(); QColor lightColor = qApp->palette().color(QPalette::Light); QColor darkColor = qApp->palette().color(QPalette::Dark); diff --git a/src/afcexplorerwidget.h b/src/afcexplorerwidget.h index 7c0e8e9..6959b91 100644 --- a/src/afcexplorerwidget.h +++ b/src/afcexplorerwidget.h @@ -23,6 +23,7 @@ #include "iDescriptor-ui.h" #include "iDescriptor.h" #include +#include #include #include #include @@ -117,6 +118,15 @@ private: void updateNavStyles(); void updateButtonStates(); void goUp(); + +protected: + void changeEvent(QEvent *event) override + { + if (event->type() == QEvent::PaletteChange) { + updateNavStyles(); + } + QWidget::changeEvent(event); + } }; #endif // AFCEXPLORER_H diff --git a/src/airplaywindow.cpp b/src/airplaywindow.cpp index 80a4cf4..1c9de58 100644 --- a/src/airplaywindow.cpp +++ b/src/airplaywindow.cpp @@ -21,9 +21,14 @@ #include #include #include +#include #include +#include +#include #include #include +#include +#include #include #include #include @@ -31,10 +36,11 @@ #include #include #include +#include +#include #include #include #include - #ifdef Q_OS_LINUX // V4L2 includes #include @@ -44,12 +50,91 @@ #include #include #endif +#include "settingsmanager.h" -// Include the rpiplay server functions -#include "../lib/airplay/renderers/video_renderer.h" -extern "C" { -int start_server_qt(const char *name, void *callbacks); -int stop_server_qt(); +#include +#include + +#include "diagnosedialog.h" +#ifdef WIN32 +#include "platform/windows/check_deps.h" +#endif +#include "toolboxwidget.h" + +AirPlaySettings::AirPlaySettings() + : fps(SettingsManager::sharedInstance()->airplayFps()), + noHold(SettingsManager::sharedInstance()->airplayNoHold()) +{ +} + +QStringList AirPlaySettings::toArgs() const +{ + QStringList args; + + // FPS + args << "-fps" << QString::number(fps); + + // Allow new connections to take over + if (noHold) + args << "-nohold"; + + return args; +} + +AirPlaySettingsDialog::AirPlaySettingsDialog(QWidget *parent) + : QDialog(parent), m_settings(AirPlaySettings()) +{ + setupUI(); + setWindowTitle("AirPlay Settings"); + resize(300, 300); +} + +void AirPlaySettingsDialog::setupUI() +{ + QVBoxLayout *mainLayout = new QVBoxLayout(this); + + // Video Settings Group + QGroupBox *videoGroup = new QGroupBox("Video Settings"); + QFormLayout *videoLayout = new QFormLayout(videoGroup); + + // FPS Layout + QVBoxLayout *fpsLayout = new QVBoxLayout(); + m_fpsComboBox = new QComboBox(); + m_fpsComboBox->addItems({"24", "30", "60", "120"}); + m_fpsComboBox->setCurrentText( + QString::number(SettingsManager::sharedInstance()->airplayFps())); + m_fpsComboBox->setToolTip("Set maximum allowed streaming framerate"); + + QLabel *fpsFootnote = + new QLabel("Note: Older devices may not support higher framerates. If " + "you are experiencing issues, set this to 30 FPS or lower."); + fpsFootnote->setWordWrap(true); + fpsFootnote->setStyleSheet("color: #666; font-size: 12px;"); + fpsLayout->addWidget(m_fpsComboBox); + fpsLayout->addWidget(fpsFootnote); + + videoLayout->addRow("Max FPS:", fpsLayout); + + m_noHoldCheckbox = new QCheckBox("Allow New Connections to Take Over"); + m_noHoldCheckbox->setChecked( + SettingsManager::sharedInstance()->airplayNoHold()); + videoLayout->addRow(m_noHoldCheckbox); + + mainLayout->addWidget(videoGroup); + // Buttons + QDialogButtonBox *buttonBox = + new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + mainLayout->addWidget(buttonBox); +} + +AirPlaySettings AirPlaySettingsDialog::getSettings() const +{ + AirPlaySettings settings; + settings.fps = m_fpsComboBox->currentText().toInt(); + settings.noHold = m_noHoldCheckbox->isChecked(); + return settings; } AirPlayWindow::AirPlayWindow(QWidget *parent) @@ -57,16 +142,35 @@ AirPlayWindow::AirPlayWindow(QWidget *parent) m_streamingWidget(nullptr), m_loadingIndicator(nullptr), m_loadingLabel(nullptr), m_tutorialPlayer(nullptr), m_tutorialVideoWidget(nullptr), m_videoLabel(nullptr), - m_tutorialLayout(nullptr), m_v4l2Checkbox(nullptr), - m_serverThread(nullptr), m_serverRunning(false) -#ifdef Q_OS_LINUX - , - m_v4l2_fd(-1), m_v4l2_width(0), m_v4l2_height(0), m_v4l2_enabled(false) + m_tutorialLayout(nullptr), m_settingsButton(nullptr), +#ifdef __linux__ + m_v4l2Checkbox(nullptr), m_v4l2_fd(-1), m_v4l2_width(0), m_v4l2_height(0), + m_v4l2_enabled(false), #endif + m_serverThread(nullptr), m_serverRunning(false), m_clientConnected(false) { setupUI(); + setMinimumSize(800, 600); + QTimer::singleShot(0, this, [this]() { + /* HACK: qt ignores resize() calls so let's workaround */ + setMinimumSize(0, 0); + }); - // Auto-start server after UI setup +/* FIXME: this can be handled better, add linux support */ +#ifdef WIN32 + bool bonjour = IsBonjourServiceInstalled(); + if (!bonjour) { + QMessageBox::warning( + this, "Bonjour Service Not Installed", + "Bonjour service is not installed on your system. Please install " + "it to enable AirPlay functionality."); + + DiagnoseDialog *diagnoseDialog = new DiagnoseDialog(); + diagnoseDialog->show(); + QTimer::singleShot(0, this, &AirPlayWindow::close); + return; + } +#endif QTimer::singleShot(500, this, &AirPlayWindow::startAirPlayServer); } @@ -81,8 +185,6 @@ AirPlayWindow::~AirPlayWindow() void AirPlayWindow::setupUI() { setWindowTitle("AirPlay Receiver - iDescriptor"); - setMinimumSize(800, 600); - resize(1000, 700); // Create stacked widget m_stackedWidget = new QStackedWidget(this); @@ -90,25 +192,37 @@ void AirPlayWindow::setupUI() m_tutorialWidget = new QWidget(); m_tutorialLayout = new QVBoxLayout(m_tutorialWidget); - m_tutorialLayout->setContentsMargins(40, 40, 40, 40); + m_tutorialLayout->setContentsMargins(0, 0, 0, 0); m_tutorialLayout->setSpacing(20); m_loadingIndicator = new QProcessIndicator(); m_loadingIndicator->setType(QProcessIndicator::line_rotate); - m_loadingIndicator->setFixedSize(64, 32); + m_loadingIndicator->setFixedSize(24, 24); m_loadingIndicator->start(); QHBoxLayout *loadingLayout = new QHBoxLayout(); - loadingLayout->setSpacing(1); m_loadingLabel = new QLabel("Starting AirPlay Server..."); - m_loadingLabel->setAlignment(Qt::AlignCenter); - + loadingLayout->setContentsMargins(0, 40, 0, 0); + loadingLayout->addStretch(); loadingLayout->addWidget(m_loadingLabel); + loadingLayout->addSpacing(5); loadingLayout->addWidget(m_loadingIndicator); + loadingLayout->addStretch(); m_tutorialLayout->addLayout(loadingLayout); m_tutorialLayout->addSpacing(1); + // Settings button (shown when no client connected) + m_settingsButton = new QPushButton("Settings"); + m_settingsButton->setVisible(false); + connect(m_settingsButton, &QPushButton::clicked, this, + &AirPlayWindow::showSettingsDialog); + QHBoxLayout *settingsLayout = new QHBoxLayout(); + settingsLayout->addStretch(); + settingsLayout->addWidget(m_settingsButton); + settingsLayout->addStretch(); + m_tutorialLayout->addLayout(settingsLayout); + QTimer::singleShot(100, this, &AirPlayWindow::setupTutorialVideo); m_streamingWidget = new QWidget(); @@ -116,7 +230,7 @@ void AirPlayWindow::setupUI() streamingLayout->setContentsMargins(10, 10, 10, 10); streamingLayout->setSpacing(10); -#ifdef Q_OS_LINUX +#ifdef __linux__ // Add V4L2 checkbox at the top of streaming view setupV4L2Checkbox(); if (m_v4l2Checkbox) { @@ -126,7 +240,6 @@ void AirPlayWindow::setupUI() // Video display m_videoLabel = new QLabel(); - m_videoLabel->setMinimumSize(640, 480); m_videoLabel->setAlignment(Qt::AlignCenter); m_videoLabel->setScaledContents(false); streamingLayout->addWidget(m_videoLabel, 1); @@ -151,7 +264,7 @@ void AirPlayWindow::setupTutorialVideo() QSizePolicy::Expanding); m_tutorialPlayer->setVideoOutput(m_tutorialVideoWidget); - m_tutorialPlayer->setSource(QUrl("qrc:/resources/airplayer-tutorial.mp4")); + m_tutorialPlayer->setSource(QUrl("qrc:/resources/airplay-tutorial.mp4")); m_tutorialVideoWidget->setAspectRatioMode( Qt::AspectRatioMode::KeepAspectRatioByExpanding); m_tutorialVideoWidget->setStyleSheet( @@ -181,6 +294,7 @@ void AirPlayWindow::showTutorialView() m_stackedWidget->setCurrentWidget(m_tutorialWidget); if (m_tutorialPlayer) { m_tutorialPlayer->play(); + m_loadingIndicator->start(); } } @@ -193,6 +307,23 @@ void AirPlayWindow::showStreamingView() } } +void AirPlayWindow::showSettingsDialog() +{ + AirPlaySettingsDialog dialog(this); + if (dialog.exec() == QDialog::Accepted) { + AirPlaySettings newSettings = dialog.getSettings(); + + // Save settings + SettingsManager::sharedInstance()->setAirplayFps(newSettings.fps); + SettingsManager::sharedInstance()->setAirplayNoHold(newSettings.noHold); + + QMessageBox::information(this, "Settings Saved", + "AirPlay will be restarted to apply the new " + "settings."); + ToolboxWidget::sharedInstance()->restartAirPlayWindow(); + } +} + void AirPlayWindow::startAirPlayServer() { if (m_serverRunning) @@ -205,15 +336,21 @@ void AirPlayWindow::startAirPlayServer() &AirPlayWindow::updateVideoFrame); connect(m_serverThread, &AirPlayServerThread::clientConnectionChanged, this, &AirPlayWindow::onClientConnectionChanged); + connect(m_serverThread, &AirPlayServerThread::errorOccurred, this, + [this](const QString &message) { + QMessageBox::critical(this, "AirPlay Server Error", message); + close(); + }); + QStringList args = m_settings.toArgs(); + m_serverThread->setArguments(args); m_serverThread->start(); } void AirPlayWindow::stopAirPlayServer() { if (m_serverThread) { - m_serverThread->stopServer(); - m_serverThread->wait(3000); + m_serverThread->quit(); m_serverThread->deleteLater(); m_serverThread = nullptr; } @@ -223,8 +360,10 @@ void AirPlayWindow::stopAirPlayServer() void AirPlayWindow::updateVideoFrame(QByteArray frameData, int width, int height) { - if (frameData.size() != width * height * 3) + if (frameData.size() != width * height * 3) { + qDebug() << "Invalid frame data size"; return; + } #ifdef __linux__ // V4L2 output if enabled @@ -254,10 +393,15 @@ void AirPlayWindow::onServerStatusChanged(bool running) if (running) { // Server started successfully, hide loading indicator and show tutorial // video - m_loadingLabel->setText("Waiting for device connection..."); + m_loadingLabel->setText("Waiting for device connection"); // Show tutorial video and instructions m_tutorialVideoWidget->setVisible(true); + + // Show settings button when server is running but no client connected + m_settingsButton->setVisible(!m_clientConnected); + + // Show tutorial video and instructions QLabel *instructionLabel = m_tutorialWidget->findChild(); if (instructionLabel && !instructionLabel->text().contains("Follow")) { // Find the instruction label (not title or loading label) @@ -279,12 +423,17 @@ void AirPlayWindow::onServerStatusChanged(bool running) void AirPlayWindow::onClientConnectionChanged(bool connected) { m_clientConnected = connected; + + // Hide settings button when client is connected + m_settingsButton->setVisible(!connected && m_serverRunning); + if (connected) { m_loadingLabel->setText("Device connected - receiving stream..."); showStreamingView(); } else { m_loadingLabel->setText("Waiting for device connection..."); + m_videoLabel->clear(); showTutorialView(); } } @@ -340,57 +489,79 @@ AirPlayServerThread::AirPlayServerThread(QObject *parent) AirPlayServerThread::~AirPlayServerThread() { - stopServer(); + uxplay_cleanup(); wait(); } -void AirPlayServerThread::stopServer() +void AirPlayServerThread::setArguments(const QStringList &args) { QMutexLocker locker(&m_mutex); - m_shouldStop = true; - m_waitCondition.wakeAll(); + + m_argData.clear(); + m_argv.clear(); + + m_argData.append("uxplay"); + + // Add all arguments + for (const QString &arg : args) { + m_argData.append(arg.toUtf8()); + } + + // Build argv array with persistent pointers + for (QByteArray &data : m_argData) { + m_argv.append(data.data()); + } } // Global pointer to current server thread for callbacks static AirPlayServerThread *g_currentServerThread = nullptr; -// Static callback wrappers for C interface -extern "C" void qt_video_callback(uint8_t *data, int width, int height) +void frame_callback(const unsigned char *data, int width, int height, + int stride, int format) { - if (g_currentServerThread) { - QByteArray frameData((const char *)data, width * height * 3); - emit g_currentServerThread->videoFrameReady(frameData, width, height); - } + if (!g_currentServerThread) + return; + QByteArray frameData((const char *)data, width * height * 3); + emit g_currentServerThread->videoFrameReady(frameData, width, height); } -extern "C" void qt_connection_callback(bool connected) +void connection_callback(bool connected) { - if (g_currentServerThread) { - emit g_currentServerThread->clientConnectionChanged(connected); - } + qDebug() << "Connection callback: " + << (connected ? "Connected" : "Disconnected"); + if (!g_currentServerThread) + return; + emit g_currentServerThread->clientConnectionChanged(connected); } void AirPlayServerThread::run() { g_currentServerThread = this; emit statusChanged(true); + callbacks_t callbacks; + callbacks.frame_callback = frame_callback; + callbacks.connection_callback = connection_callback; + uxplay_callbacks = &callbacks; - // Create callbacks structure - video_renderer_qt_callbacks_t callbacks; - callbacks.video_callback = qt_video_callback; - callbacks.connection_callback = qt_connection_callback; - - start_server_qt("iDescriptor", &callbacks); - - // Wait efficiently until stopServer() is called - QMutexLocker locker(&m_mutex); - while (!m_shouldStop) { - m_waitCondition.wait(&m_mutex); + qDebug() << "Starting AirPlay server with arguments:" << m_argv.size(); + for (int i = 0; i < m_argv.size(); ++i) { + qDebug() << " argv[" << i << "] =" << m_argv[i]; } - stop_server_qt(); + try { + int res = init_uxplay(m_argv.size(), m_argv.data()); + qDebug() << "AirPlay server exited with code: " << res; + if (res != 0) { + emit errorOccurred("AirPlay server exited unexpectedly."); + } + } catch (const std::exception &e) { + qDebug() << "Exception in AirPlay server thread: " << e.what(); + emit errorOccurred( + QString("AirPlay server encountered an error: %1").arg(e.what())); + } + + uxplay_callbacks = nullptr; g_currentServerThread = nullptr; - emit statusChanged(false); } #ifdef __linux__ @@ -511,6 +682,9 @@ bool AirPlayWindow::createV4L2Loopback() void AirPlayWindow::setupV4L2Checkbox() { + if (!SettingsManager::sharedInstance()->showV4L2()) + return; + try { m_v4l2Checkbox = new QCheckBox("Enable V4L2 Virtual Camera Output"); m_v4l2Checkbox->setToolTip("Enable output to virtual camera device " @@ -524,4 +698,4 @@ void AirPlayWindow::setupV4L2Checkbox() qWarning("Exception occurred while setting up V4L2 checkbox"); } } -#endif \ No newline at end of file +#endif diff --git a/src/airplaywindow.h b/src/airplaywindow.h index 0b94d7a..1e6e5c0 100644 --- a/src/airplaywindow.h +++ b/src/airplaywindow.h @@ -23,10 +23,16 @@ #include "qprocessindicator.h" #include #include +#include +#include +#include +#include +#include #include #include #include #include +#include #include #include #include @@ -40,22 +46,51 @@ class AirPlayServerThread : public QThread public: explicit AirPlayServerThread(QObject *parent = nullptr); - ~AirPlayServerThread() override; + ~AirPlayServerThread(); - void stopServer(); + // void stopServer(); + void setArguments(const QStringList &args); signals: void statusChanged(bool running); void videoFrameReady(QByteArray frameData, int width, int height); void clientConnectionChanged(bool connected); + void errorOccurred(const QString &message); protected: void run() override; private: - bool m_shouldStop; QMutex m_mutex; QWaitCondition m_waitCondition; + bool m_shouldStop; + QVector m_argData; + QVector m_argv; +}; + +class AirPlaySettings +{ +public: + explicit AirPlaySettings(); + int fps; + bool noHold; + + QStringList toArgs() const; +}; + +class AirPlaySettingsDialog : public QDialog +{ + Q_OBJECT +public: + explicit AirPlaySettingsDialog(QWidget *parent = nullptr); + AirPlaySettings getSettings() const; + +private: + void setupUI(); + + QComboBox *m_fpsComboBox; + QCheckBox *m_noHoldCheckbox; + AirPlaySettings m_settings; }; class AirPlayWindow : public QMainWindow @@ -66,22 +101,31 @@ public: explicit AirPlayWindow(QWidget *parent = nullptr); ~AirPlayWindow(); -public slots: - void updateVideoFrame(QByteArray frameData, int width, int height); - void onClientConnectionChanged(bool connected); - private slots: + void updateVideoFrame(QByteArray frameData, int width, int height); void onServerStatusChanged(bool running); + void onClientConnectionChanged(bool connected); + void showSettingsDialog(); #ifdef __linux__ void onV4L2CheckboxToggled(bool enabled); #endif + private: void setupUI(); - void startAirPlayServer(); - void stopAirPlayServer(); void setupTutorialVideo(); void showTutorialView(); void showStreamingView(); + void startAirPlayServer(); + void stopAirPlayServer(); + +#ifdef __linux__ + void initV4L2(int width, int height, const char *device = "/dev/video0"); + void closeV4L2(); + void writeFrameToV4L2(uint8_t *data, int width, int height); + bool checkV4L2LoopbackExists(); + bool createV4L2Loopback(); + void setupV4L2Checkbox(); +#endif // UI Components QStackedWidget *m_stackedWidget; @@ -94,30 +138,20 @@ private: QVideoWidget *m_tutorialVideoWidget; QLabel *m_videoLabel; QVBoxLayout *m_tutorialLayout; - QCheckBox *m_v4l2Checkbox; - - AirPlayServerThread *m_serverThread; - bool m_serverRunning; - bool m_clientConnected = false; + QPushButton *m_settingsButton; #ifdef __linux__ -public: - // V4L2 members - public for C callback access + QCheckBox *m_v4l2Checkbox; int m_v4l2_fd; int m_v4l2_width; int m_v4l2_height; bool m_v4l2_enabled = false; - - // V4L2 methods - void writeFrameToV4L2(uint8_t *data, int width, int height); - -private: - void initV4L2(int width, int height, const char *device); - void closeV4L2(); - bool checkV4L2LoopbackExists(); - bool createV4L2Loopback(); - void setupV4L2Checkbox(); #endif + + AirPlayServerThread *m_serverThread; + bool m_serverRunning; + bool m_clientConnected; + AirPlaySettings m_settings; }; #endif // AIRPLAYWINDOW_H diff --git a/src/appcontext.cpp b/src/appcontext.cpp index e7b9ab6..208bd25 100644 --- a/src/appcontext.cpp +++ b/src/appcontext.cpp @@ -104,7 +104,6 @@ void AppContext::addDevice(QString udid, idevice_connection_type conn_type, .deviceInfo = initResult.deviceInfo, .afcClient = initResult.afcClient, .afc2Client = initResult.afc2Client, - .mutex = new std::recursive_mutex(), }; m_devices[device->udid] = device; if (addType == AddType::Regular) { @@ -172,14 +171,15 @@ void AppContext::removeDevice(QString _udid) emit deviceRemoved(udid); emit deviceChange(); - std::lock_guard lock(*device->mutex); + std::lock_guard lock(device->mutex); if (device->afcClient) afc_client_free(device->afcClient); if (device->afc2Client) afc_client_free(device->afc2Client); + idevice_free(device->device); - delete device->mutex; + delete device; } @@ -201,8 +201,7 @@ void AppContext::removeRecoveryDevice(uint64_t ecid) emit recoveryDeviceRemoved(ecid); emit deviceChange(); - std::lock_guard lock(*deviceInfo->mutex); - delete deviceInfo->mutex; + std::lock_guard lock(deviceInfo->mutex); delete deviceInfo; } #endif @@ -254,7 +253,6 @@ void AppContext::addRecoveryDevice(uint64_t ecid) recoveryDevice->cpid = res.deviceInfo.cpid; recoveryDevice->bdid = res.deviceInfo.bdid; recoveryDevice->displayName = res.displayName; - recoveryDevice->mutex = new std::recursive_mutex(); m_recoveryDevices[res.deviceInfo.ecid] = recoveryDevice; emit recoveryDeviceAdded(recoveryDevice); @@ -271,14 +269,12 @@ AppContext::~AppContext() if (device->afc2Client) afc_client_free(device->afc2Client); idevice_free(device->device); - delete device->mutex; delete device; } #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT for (auto recoveryDevice : m_recoveryDevices) { emit recoveryDeviceRemoved(recoveryDevice->ecid); - delete recoveryDevice->mutex; delete recoveryDevice; } #endif diff --git a/src/appswidget.cpp b/src/appswidget.cpp index 28bfac8..b8664db 100644 --- a/src/appswidget.cpp +++ b/src/appswidget.cpp @@ -107,20 +107,14 @@ void AppsWidget::setupUI() mainLayout->addWidget(headerWidget); - static ZIcon searchIcon(":/resources/icons/MdiLightMagnify.png"); + m_searchIcon = ZIcon(":/resources/icons/MdiLightMagnify.png"); m_searchAction = m_searchEdit->addAction( - searchIcon.getThemedPixmap(QSize(16, 16), palette()), + m_searchIcon.getThemedPixmap(QSize(16, 16), palette()), QLineEdit::TrailingPosition); m_searchAction->setToolTip("Search"); connect(m_searchAction, &QAction::triggered, this, &AppsWidget::performSearch); - // Update search icon when theme changes - connect(qApp, &QApplication::paletteChanged, this, [this]() { - m_searchAction->setIcon( - searchIcon.getThemedPixmap(QSize(16, 16), palette())); - }); - headerLayout->addWidget(m_searchEdit); headerLayout->addStretch(); headerLayout->addWidget(m_statusLabel); diff --git a/src/appswidget.h b/src/appswidget.h index a16cbd0..95b5e80 100644 --- a/src/appswidget.h +++ b/src/appswidget.h @@ -21,10 +21,12 @@ #define APPSWIDGET_H #include "appstoremanager.h" +#include "iDescriptor-ui.h" #include "qprocessindicator.h" #include #include #include +#include #include #include #include @@ -136,6 +138,19 @@ private: QJsonArray m_goldSponsors; QJsonArray m_silverSponsors; QJsonArray m_bronzeSponsors; + ZIcon m_searchIcon; + +protected: + void changeEvent(QEvent *event) override + { + if (event->type() == QEvent::PaletteChange) { + if (m_searchAction && !m_searchIcon.isNull()) { + m_searchAction->setIcon( + m_searchIcon.getThemedPixmap(QSize(16, 16), palette())); + } + } + QWidget::changeEvent(event); + } }; #endif // APPSWIDGET_H diff --git a/src/batterywidget.cpp b/src/batterywidget.cpp index aae1e1e..15b2404 100644 --- a/src/batterywidget.cpp +++ b/src/batterywidget.cpp @@ -32,8 +32,14 @@ BatteryWidget::BatteryWidget(float value, bool isCharging, QWidget *parent) { setMinimumSize(30, 30); setMaximumSize(40, 40); +} - connect(qApp, &QApplication::paletteChanged, this, [this]() { update(); }); +void BatteryWidget::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::PaletteChange) { + update(); + } + QWidget::changeEvent(event); } void BatteryWidget::resizeEvent(QResizeEvent *) diff --git a/src/batterywidget.h b/src/batterywidget.h index 3f2cb22..e2bc277 100644 --- a/src/batterywidget.h +++ b/src/batterywidget.h @@ -20,6 +20,7 @@ #ifndef BATTERYWIDGET_H #define BATTERYWIDGET_H +#include #include class BatteryWidget : public QWidget @@ -35,6 +36,9 @@ public: void setValue(float newValue); float getValue() const; +protected: + void changeEvent(QEvent *event) override; + private: QRectF widgetFrame; QRectF mainBatteryFrame; diff --git a/src/core/services/get_file_tree.cpp b/src/core/services/get_file_tree.cpp index 9796205..2c7bba5 100644 --- a/src/core/services/get_file_tree.cpp +++ b/src/core/services/get_file_tree.cpp @@ -24,7 +24,8 @@ #include #include -AFCFileTree get_file_tree(afc_client_t afcClient, const std::string &path) +AFCFileTree get_file_tree(afc_client_t afcClient, const std::string &path, + bool checkDir) { AFCFileTree result; @@ -47,9 +48,11 @@ AFCFileTree get_file_tree(afc_client_t afcClient, const std::string &path) fullPath += "/"; fullPath += entryName; bool isDir = false; - if (afc_get_file_info(afcClient, fullPath.c_str(), &info) == - AFC_E_SUCCESS && - info) { + if (!checkDir) { + isDir = false; + } else if (afc_get_file_info(afcClient, fullPath.c_str(), &info) == + AFC_E_SUCCESS && + info) { if (entryName == "var") { qDebug() << "File info for var:" << info[0] << info[1] << info[2] << info[3] << info[4] << info[5]; diff --git a/src/devicemanagerwidget.cpp b/src/devicemanagerwidget.cpp index 508f209..eba3500 100644 --- a/src/devicemanagerwidget.cpp +++ b/src/devicemanagerwidget.cpp @@ -280,11 +280,6 @@ void DeviceManagerWidget::removeDevice(const std::string &uuid) m_stackedWidget->removeWidget(deviceWidget); m_sidebar->removeDevice(uuid); deviceWidget->deleteLater(); - - // // TODO: - // if (m_deviceWidgets.count() > 0) { - // setCurrentDevice(m_deviceWidgets.firstKey()); - // } } } diff --git a/src/devicesidebarwidget.cpp b/src/devicesidebarwidget.cpp index d3afda0..a77dc88 100644 --- a/src/devicesidebarwidget.cpp +++ b/src/devicesidebarwidget.cpp @@ -283,7 +283,7 @@ DeviceSidebarWidget::DeviceSidebarWidget(QWidget *parent) // Set minimum width setMinimumWidth(200); - setMaximumWidth(250); + setMaximumWidth(200); // Listen to AppContext selection changes connect(AppContext::sharedInstance(), diff --git a/src/diagnosedialog.cpp b/src/diagnosedialog.cpp index 0c5d925..598e501 100644 --- a/src/diagnosedialog.cpp +++ b/src/diagnosedialog.cpp @@ -18,34 +18,29 @@ */ #include "diagnosedialog.h" +#include "iDescriptor-ui.h" #include DiagnoseDialog::DiagnoseDialog(QWidget *parent) : QDialog(parent) { setupUI(); - setWindowTitle("System Dependencies"); setModal(true); - resize(500, 400); - - // Set clean close behavior setAttribute(Qt::WA_DeleteOnClose, true); } void DiagnoseDialog::setupUI() { + setMinimumSize(MIN_MAIN_WINDOW_SIZE.width(), MIN_MAIN_WINDOW_SIZE.height() / 2); QVBoxLayout *mainLayout = new QVBoxLayout(this); - QScrollArea *scrollArea = new QScrollArea(this); - scrollArea->setWidgetResizable(true); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); mainLayout->setContentsMargins(10, 10, 10, 10); - // Add the main diagnose widget + /* + TODO: either subclass DiagnoseWidget or + modify its layout to better fit dialog + */ m_diagnoseWidget = new DiagnoseWidget(); - scrollArea->setWidget(m_diagnoseWidget); - // Close button QHBoxLayout *buttonLayout = new QHBoxLayout(); buttonLayout->addStretch(); @@ -56,8 +51,7 @@ void DiagnoseDialog::setupUI() buttonLayout->addWidget(m_closeButton); - // Layout assembly - mainLayout->addWidget(scrollArea); + mainLayout->addWidget(m_diagnoseWidget); mainLayout->addLayout(buttonLayout); } diff --git a/src/diagnosewidget.cpp b/src/diagnosewidget.cpp index 789425f..77f9c2f 100644 --- a/src/diagnosewidget.cpp +++ b/src/diagnosewidget.cpp @@ -20,16 +20,23 @@ #include "diagnosewidget.h" #ifdef WIN32 #include "platform/windows/check_deps.h" +#include +#include #endif #include #include +#include #include #include #include #include #include +#include +#include +#include #include #include +#include #include #include #include @@ -41,8 +48,7 @@ DependencyItem::DependencyItem(const QString &name, const QString &description, QHBoxLayout *layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); - // Left side - info - QVBoxLayout *infoLayout = new QVBoxLayout(); + QHBoxLayout *infoLayout = new QHBoxLayout(); m_nameLabel = new QLabel(name); QFont nameFont = m_nameLabel->font(); @@ -50,8 +56,8 @@ DependencyItem::DependencyItem(const QString &name, const QString &description, nameFont.setPointSize(nameFont.pointSize() + 1); m_nameLabel->setFont(nameFont); - m_descriptionLabel = new QLabel(description); - m_descriptionLabel->setWordWrap(true); + m_descriptionLabel = new QLabel(QString("(%1)").arg(description)); + m_descriptionLabel->setWordWrap(false); infoLayout->addWidget(m_nameLabel); infoLayout->addWidget(m_descriptionLabel); @@ -78,9 +84,9 @@ DependencyItem::DependencyItem(const QString &name, const QString &description, actionLayout->addWidget(m_processIndicator); actionLayout->addWidget(m_installButton); - actionLayout->addStretch(); - layout->addLayout(infoLayout, 1); + layout->addLayout(infoLayout); + layout->addStretch(); layout->addWidget(m_statusLabel); layout->addLayout(actionLayout); } @@ -135,6 +141,12 @@ void DependencyItem::setInstalling(bool installing) } } +void DependencyItem::setProgress(const QString &message) +{ + m_statusLabel->setText(message); + m_statusLabel->setStyleSheet("color: gray;"); +} + void DependencyItem::onInstallClicked() { emit installRequested(m_name); } DiagnoseWidget::DiagnoseWidget(QWidget *parent) @@ -143,6 +155,8 @@ DiagnoseWidget::DiagnoseWidget(QWidget *parent) setupUI(); #ifdef WIN32 + addDependencyItem("Bonjour Service", + "Required for AirPlay and network service discovery"); addDependencyItem("Apple Mobile Device Support", "Required for iOS device communication"); addDependencyItem("WinFsp", "Required for mounting your device as a drive"); @@ -240,7 +254,9 @@ void DiagnoseWidget::checkDependencies(bool autoExpand) QString itemName = item->property("name").toString(); #ifdef WIN32 - if (itemName == "Apple Mobile Device Support") { + if (itemName == "Bonjour Service") { + installed = IsBonjourServiceInstalled(); + } else if (itemName == "Apple Mobile Device Support") { installed = IsAppleMobileDeviceSupportInstalled(); } else if (itemName == "WinFsp") { installed = IsWinFspInstalled(); @@ -287,6 +303,11 @@ void DiagnoseWidget::checkDependencies(bool autoExpand) void DiagnoseWidget::onInstallRequested(const QString &name) { #ifdef WIN32 + if (name == "Bonjour Service") { + installBonjourRuntime(); + return; + } + if (name == "Apple Mobile Device Support") { DependencyItem *itemToInstall = nullptr; for (DependencyItem *item : m_dependencyItems) { @@ -634,3 +655,178 @@ void DiagnoseWidget::onToggleExpand() m_itemsWidget->updateGeometry(); adjustSize(); } + +#ifdef WIN32 +void DiagnoseWidget::installBonjourRuntime() +{ + DependencyItem *itemToInstall = nullptr; + for (DependencyItem *item : m_dependencyItems) { + if (item->property("name").toString() == "Bonjour Service") { + itemToInstall = item; + break; + } + } + + if (!itemToInstall) + return; + + itemToInstall->setInstalling(true); + itemToInstall->setProgress("Downloading..."); + + // Download Bonjour SDK + QNetworkAccessManager *manager = new QNetworkAccessManager(this); + QNetworkRequest request( + QUrl("https://github.com/tempx-x/bonjour-sdk/raw/refs/heads/main/" + "bonjoursdksetup.exe")); + + QNetworkReply *reply = manager->get(request); + + connect(reply, &QNetworkReply::downloadProgress, this, + [itemToInstall](qint64 bytesReceived, qint64 bytesTotal) { + if (bytesTotal > 0) { + int percent = (bytesReceived * 100) / bytesTotal; + itemToInstall->setProgress( + QString("Downloading... %1%").arg(percent)); + } + }); + + connect( + reply, &QNetworkReply::finished, this, + [this, reply, manager, itemToInstall]() { + reply->deleteLater(); + manager->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + QMessageBox::critical(this, "Download Failed", + "Failed to download Bonjour SDK: " + + reply->errorString()); + checkDependencies(false); + return; + } + + itemToInstall->setProgress("Verifying..."); + + // Verify MD5 checksum + QByteArray data = reply->readAll(); + QByteArray hash = + QCryptographicHash::hash(data, QCryptographicHash::Md5); + QString actualHash = hash.toHex(); + QString expectedHash = "4ff2aae8205aec31b06743782cfcadce"; + + if (actualHash != expectedHash) { + QMessageBox::critical( + this, "Checksum Mismatch", + QString("Downloaded file checksum does not match!\n" + "Expected: %1\n" + "Got: %2") + .arg(expectedHash, actualHash)); + checkDependencies(false); + return; + } + + itemToInstall->setProgress("Extracting..."); + + // Create temp directory + QString tempDir = + QStandardPaths::writableLocation(QStandardPaths::TempLocation) + + "/bonjour_install"; + QDir().mkpath(tempDir); + + // Save the downloaded file + QString exePath = tempDir + "/bonjoursdksetup.exe"; + QFile file(exePath); + if (!file.open(QIODevice::WriteOnly)) { + QMessageBox::critical(this, "Error", + "Failed to save downloaded file"); + checkDependencies(false); + return; + } + file.write(data); + file.close(); + + // Extract using libarchive + struct archive *a = archive_read_new(); + archive_read_support_format_all(a); + archive_read_support_filter_all(a); + + struct archive *ext = archive_write_disk_new(); + archive_write_disk_set_options(ext, ARCHIVE_EXTRACT_TIME); + + if (archive_read_open_filename(a, exePath.toUtf8().constData(), + 10240) != ARCHIVE_OK) { + QMessageBox::critical(this, "Extraction Failed", + QString("Failed to open archive: %1") + .arg(archive_error_string(a))); + archive_read_free(a); + archive_write_free(ext); + checkDependencies(false); + return; + } + + struct archive_entry *entry; + QString msiPath; + + while (archive_read_next_header(a, &entry) == ARCHIVE_OK) { + QString entryName = + QString::fromUtf8(archive_entry_pathname(entry)); + + if (entryName.endsWith("Bonjour64.msi", Qt::CaseInsensitive)) { + QString fullPath = tempDir + "/" + entryName; + archive_entry_set_pathname(entry, + fullPath.toUtf8().constData()); + + if (archive_write_header(ext, entry) != ARCHIVE_OK) { + qWarning() << "Failed to write header for" << entryName; + } else { + const void *buff; + size_t size; + la_int64_t offset; + + while (archive_read_data_block(a, &buff, &size, + &offset) == ARCHIVE_OK) { + archive_write_data_block(ext, buff, size, offset); + } + } + archive_write_finish_entry(ext); + msiPath = fullPath; + break; // Only need Bonjour64.msi + } else { + archive_read_data_skip(a); + } + } + + archive_read_free(a); + archive_write_free(ext); + + if (msiPath.isEmpty()) { + QMessageBox::critical(this, "Extraction Failed", + "Could not find Bonjour64.msi in the " + "archive"); + QDir(tempDir).removeRecursively(); + checkDependencies(false); + return; + } + + itemToInstall->setProgress("Installing..."); + + // Launch the MSI via the shell (same behavior as double-click) + itemToInstall->setInstalling(false); // we can't track MSI process + + if (!QDesktopServices::openUrl(QUrl::fromLocalFile(msiPath))) { + QMessageBox::warning(this, "Installation Failed", + "Failed to launch Bonjour installer.\n\n" + "You can also run it manually from:\n" + + msiPath); + checkDependencies(false); + return; + } + + QMessageBox::information( + this, "Installation Started", + "The Bonjour installer has been launched.\n" + "Please complete the setup, then re-run the dependency check."); + + itemToInstall->setProgress("Refresh to verify installation."); + }); +} +#endif diff --git a/src/diagnosewidget.h b/src/diagnosewidget.h index f24093c..fe195a9 100644 --- a/src/diagnosewidget.h +++ b/src/diagnosewidget.h @@ -41,6 +41,7 @@ public: void setInstalled(bool installed); void setChecking(bool checking); void setInstalling(bool installing); + void setProgress(const QString &message); signals: void installRequested(const QString &name); @@ -75,6 +76,10 @@ private: void setupUI(); void addDependencyItem(const QString &name, const QString &description); +#ifdef WIN32 + void installBonjourRuntime(); +#endif + #ifdef __linux__ bool checkUdevRulesInstalled(); bool checkAvahiDaemonRunning(); diff --git a/src/exportmanager.cpp b/src/exportmanager.cpp index 9f6713f..13c7e1a 100644 --- a/src/exportmanager.cpp +++ b/src/exportmanager.cpp @@ -62,7 +62,7 @@ QUuid ExportManager::startExport(iDescriptorDevice *device, const QString &destinationPath, std::optional altAfc) { - if (!device || !device->mutex) { + if (!device) { qWarning() << "Invalid device provided to ExportManager"; return QUuid(); } @@ -251,8 +251,8 @@ ExportResult ExportManager::exportSingleItem(iDescriptorDevice *device, } uint64_t modTimeNs = fileInfo["st_mtime"].getUInt(); - // The timestamp from the device is in nanoseconds, convert to seconds - modificationTime = QDateTime::fromSecsSinceEpoch(modTimeNs / 1000000000); + // The timestamp from the device is in nanoseconds, convert to seconds (UTC) + modificationTime = QDateTime::fromSecsSinceEpoch(modTimeNs / 1000000000, Qt::UTC); valid = fileInfo["st_birthtime"].valid(); if (!valid) { @@ -263,7 +263,7 @@ ExportResult ExportManager::exportSingleItem(iDescriptorDevice *device, return result; } uint64_t birthTimeNs = fileInfo["st_birthtime"].getUInt(); - birthTime = QDateTime::fromSecsSinceEpoch(birthTimeNs / 1000000000); + birthTime = QDateTime::fromSecsSinceEpoch(birthTimeNs / 1000000000, Qt::UTC); plist_free(info); @@ -333,28 +333,24 @@ ExportResult ExportManager::exportSingleItem(iDescriptorDevice *device, } } - outputFile.close(); ServiceManager::safeAfcFileClose(device, handle, altAfc); - // reopen is required for timestamps - QFile reopen(outputPath); - reopen.open(QIODevice::ReadOnly); + outputFile.flush(); if (modificationTime.isValid()) { - if (!reopen.setFileTime(modificationTime, - QFileDevice::FileModificationTime)) { + if (!outputFile.setFileTime(modificationTime, QFileDevice::FileModificationTime)) { qWarning() << "Could not set modification time for" << outputPath; } } if (birthTime.isValid()) { - // fails on linux - if (!reopen.setFileTime(birthTime, QFileDevice::FileBirthTime)) { + if (!outputFile.setFileTime(birthTime, QFileDevice::FileBirthTime)) { qWarning() << "Could not set birth time for" << outputPath; } } + outputFile.close(); if (totalBytes == 0) { result.errorMessage = "No data read from device file"; - outputFile.remove(); // Clean up empty file + QFile::remove(outputPath); // Clean up empty file return result; } diff --git a/src/exportprogressdialog.cpp b/src/exportprogressdialog.cpp index b740223..9c3903b 100644 --- a/src/exportprogressdialog.cpp +++ b/src/exportprogressdialog.cpp @@ -60,9 +60,9 @@ ExportProgressDialog::ExportProgressDialog(ExportManager *exportManager, connect(m_transferRateTimer, &QTimer::timeout, this, &ExportProgressDialog::updateTransferRate); - // Listen for palette changes - connect(qApp, &QApplication::paletteChanged, this, - &ExportProgressDialog::updateColors); + // FIXME:Listen for palette changes + // connect(qApp, &QApplication::paletteChanged, this, + // &ExportProgressDialog::updateColors); updateColors(); } diff --git a/src/gallerywidget.cpp b/src/gallerywidget.cpp index de862f5..48d4aed 100644 --- a/src/gallerywidget.cpp +++ b/src/gallerywidget.cpp @@ -505,8 +505,8 @@ void GalleryWidget::setControlsEnabled(bool enabled) QIcon GalleryWidget::loadAlbumThumbnail(const QString &albumPath) { // Get album directory contents - AFCFileTree albumTree = - ServiceManager::safeGetFileTree(m_device, albumPath.toStdString()); + AFCFileTree albumTree = ServiceManager::safeGetFileTree( + m_device, albumPath.toStdString(), false); if (!albumTree.success) { qDebug() << "Failed to read album directory:" << albumPath; diff --git a/src/httpserver.cpp b/src/httpserver.cpp index 453a1c2..2af451c 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -19,6 +19,7 @@ #include "httpserver.h" #include "iDescriptor.h" +#include "settingsmanager.h" #include #include #include @@ -48,8 +49,10 @@ void HttpServer::start(const QStringList &files) QDateTime::currentDateTime().toString("yyyyMMdd-hhmmss"); jsonFileName = QString("%1-idescriptor-import.json").arg(timestamp); - // Try to bind to port 8080, if fails try other ports - for (int tryPort = 8080; tryPort <= 8090; ++tryPort) { + // Try to bind to port from settings, if fails try other ports + int startPort = SettingsManager::sharedInstance()->wirelessFileServerPort(); + qDebug() << "Starting HTTP server on port" << startPort; + for (int tryPort = startPort; tryPort <= startPort + 10; ++tryPort) { if (server->listen(QHostAddress::Any, tryPort)) { port = tryPort; emit serverStarted(); @@ -57,7 +60,9 @@ void HttpServer::start(const QStringList &files) } } - emit serverError("Could not bind to any port between 8080-8090"); + emit serverError(QString("Could not bind to any port between %1-%2") + .arg(startPort) + .arg(startPort + 10)); } void HttpServer::stop() diff --git a/src/iDescriptor-ui.h b/src/iDescriptor-ui.h index 7140264..1d7e53d 100644 --- a/src/iDescriptor-ui.h +++ b/src/iDescriptor-ui.h @@ -21,6 +21,7 @@ #include "settingsmanager.h" #include #include +#include #include #include #include @@ -44,6 +45,7 @@ #define COLOR_RED QColor(255, 0, 0) // Red #define COLOR_BLUE QColor("#2b5693") #define COLOR_ACCENT_BLUE QColor("#0b5ed7") +#define MIN_MAIN_WINDOW_SIZE QSize(900, 600) class ResponsiveGraphicsView : public QGraphicsView { @@ -152,11 +154,6 @@ public: updateIconSize(); setCursor(Qt::PointingHandCursor); - - connect(qApp, &QApplication::paletteChanged, this, - [this] { update(); }); - connect(qApp, &QApplication::fontChanged, this, - [this] { updateIconSize(); }); } void setIcon(const ZIcon &icon) @@ -193,6 +190,19 @@ protected: m_icon.paint(&painter, iconRect, palette(), devicePixelRatioF()); } + void changeEvent(QEvent *event) override + { + if (event->type() == QEvent::ApplicationFontChange) { + updateIconSize(); + /* TODO: may be use PaletteChange event? + but ApplicationPaletteChange seems to be a better fit here than + PaletteChange*/ + } else if (event->type() == QEvent::ApplicationPaletteChange) { + update(); + } + QAbstractButton::changeEvent(event); + } + private: void updateIconSize() { @@ -224,10 +234,6 @@ public: { setToolTip(tooltip); updateIconSize(); - connect(qApp, &QApplication::paletteChanged, this, - [this]() { update(); }); - connect(qApp, &QApplication::fontChanged, this, - [this]() { updateIconSize(); }); } void setIcon(const QIcon &icon) { @@ -261,6 +267,16 @@ protected: m_icon.paint(&painter, iconRect, palette(), devicePixelRatioF()); } + void changeEvent(QEvent *event) override + { + if (event->type() == QEvent::ApplicationFontChange) { + updateIconSize(); + } else if (event->type() == QEvent::ApplicationPaletteChange) { + update(); + } + QLabel::changeEvent(event); + } + private: void updateIconSize() { diff --git a/src/iDescriptor.h b/src/iDescriptor.h index d00dc82..dbae75b 100644 --- a/src/iDescriptor.h +++ b/src/iDescriptor.h @@ -184,7 +184,7 @@ struct iDescriptorDevice { afc_client_t afcClient; afc_client_t afc2Client; bool is_iPhone; - std::recursive_mutex *mutex; + std::recursive_mutex mutex; }; struct iDescriptorInitDeviceResult { @@ -202,7 +202,7 @@ struct iDescriptorRecoveryDevice { uint32_t cpid; uint32_t bdid; std::string displayName; - std::recursive_mutex *mutex; + std::recursive_mutex mutex; }; #endif @@ -316,7 +316,7 @@ struct AFCFileTree { }; AFCFileTree get_file_tree(afc_client_t afcClient, - const std::string &path = "/"); + const std::string &path = "/", bool checkDir = true); bool detect_jailbroken(afc_client_t afc); diff --git a/src/infolabel.cpp b/src/infolabel.cpp index 4c604b2..ef5f411 100644 --- a/src/infolabel.cpp +++ b/src/infolabel.cpp @@ -20,10 +20,13 @@ #include "infolabel.h" #include #include +#include #include -InfoLabel::InfoLabel(const QString &text, QWidget *parent) - : QLabel(text, parent), m_originalText(text) +InfoLabel::InfoLabel(const QString &text, const QString &textToCopy, + QWidget *parent) + : QLabel(text, parent), m_originalText(text), + m_textToCopy(!textToCopy.isEmpty() ? textToCopy : text) { setCursor(Qt::PointingHandCursor); setStyleSheet("QLabel:hover { background-color: rgba(255, 255, 255, 0.1); " @@ -38,9 +41,13 @@ InfoLabel::InfoLabel(const QString &text, QWidget *parent) void InfoLabel::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { - QClipboard *clipboard = QApplication::clipboard(); - clipboard->setText(m_originalText); + int originalWidth = width(); + QClipboard *clipboard = QApplication::clipboard(); + clipboard->setText(m_textToCopy); + + // prevent layout shifts + setMinimumWidth(originalWidth); setText("Copied!"); setStyleSheet("QLabel { color: #4CAF50; font-weight: bold; } " "QLabel:hover { background-color: rgba(255, 255, 255, " @@ -72,8 +79,14 @@ void InfoLabel::leaveEvent(QEvent *event) void InfoLabel::restoreOriginalText() { setText(m_originalText); + setMinimumWidth(0); setStyleSheet("QLabel:hover { background-color: rgba(255, 255, 255, 0.1); " "border-radius: 2px; }"); } void InfoLabel::setOriginalText(const QString &text) { m_originalText = text; } + +void InfoLabel::setTextToCopy(const QString &textToCopy) +{ + m_textToCopy = textToCopy; +} \ No newline at end of file diff --git a/src/infolabel.h b/src/infolabel.h index 6a6d723..18e55d9 100644 --- a/src/infolabel.h +++ b/src/infolabel.h @@ -29,10 +29,11 @@ class InfoLabel : public QLabel public: explicit InfoLabel(const QString &text = QString(), + const QString &textToCopy = QString(), QWidget *parent = nullptr); - // Allow updating the original text (useful for PrivateInfoLabel) void setOriginalText(const QString &text); + void setTextToCopy(const QString &textToCopy); protected: void mousePressEvent(QMouseEvent *event) override; @@ -44,6 +45,7 @@ private slots: private: QString m_originalText; + QString m_textToCopy; QTimer *m_restoreTimer; }; diff --git a/src/installedappswidget.cpp b/src/installedappswidget.cpp index 3786b4e..3f6b9a6 100644 --- a/src/installedappswidget.cpp +++ b/src/installedappswidget.cpp @@ -154,7 +154,10 @@ void AppTabWidget::updateStyles() "; border-radius: 10px; border: 1px solid " + bgColor.lighter().name() + "; }"; } - setStyleSheet(style); + // prevent infinite loop + if (style != styleSheet()) { + setStyleSheet(style); + } } InstalledAppsWidget::InstalledAppsWidget(iDescriptorDevice *device, @@ -196,12 +199,6 @@ void InstalledAppsWidget::setupUI() // Start in loading state showLoadingState(); - - connect(qApp, &QApplication::paletteChanged, this, [this]() { - for (AppTabWidget *tab : m_appTabs) { - tab->updateStyles(); - } - }); } void InstalledAppsWidget::showLoadingState() @@ -739,18 +736,6 @@ void InstalledAppsWidget::loadAppContainer(const QString &bundleId) return result; } - QStringList files; - if (list) { - for (int i = 0; list[i]; i++) { - QString fileName = QString::fromUtf8(list[i]); - if (fileName != "." && fileName != "..") { - qDebug() << "Found file:" << fileName; - files.append(fileName); - } - } - afc_dictionary_free(list); - } - result["files"] = files; result["afcClient"] = QVariant::fromValue(reinterpret_cast(afcClient)); result["houseArrestClient"] = QVariant::fromValue( diff --git a/src/installedappswidget.h b/src/installedappswidget.h index 1f41a83..66f13c9 100644 --- a/src/installedappswidget.h +++ b/src/installedappswidget.h @@ -24,6 +24,7 @@ #include "zlineedit.h" #include #include +#include #include #include #include @@ -66,6 +67,13 @@ signals: protected: void mousePressEvent(QMouseEvent *event) override; + void changeEvent(QEvent *event) override + { + if (event->type() == QEvent::PaletteChange) { + updateStyles(); + } + QGroupBox::changeEvent(event); + }; private: void fetchAppIcon(); @@ -79,7 +87,6 @@ private: QLabel *m_iconLabel; QLabel *m_nameLabel; QLabel *m_versionLabel; - QList m_appTabs; QNetworkAccessManager *m_networkManager = new QNetworkAccessManager(this); }; diff --git a/src/main.cpp b/src/main.cpp index 7cb2815..2723eea 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -77,9 +77,6 @@ int main(int argc, char *argv[]) setenv("GST_PLUGIN_PATH", gstPluginPath.toUtf8().constData(), 1); setenv("GST_PLUGIN_SYSTEM_PATH", gstPluginPath.toUtf8().constData(), 1); setenv("GST_PLUGIN_SCANNER", gstPluginScannerPath.toUtf8().constData(), 1); -#endif -#ifndef __APPLE__ - QApplication::setStyle(QStyleFactory::create("Fusion")); #endif MainWindow *w = MainWindow::sharedInstance(); w->show(); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index c82d111..58cf5ef 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -136,9 +136,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); - const QSize minSize(900, 600); - setMinimumSize(minSize); - resize(minSize); + setMinimumSize(MIN_MAIN_WINDOW_SIZE); + resize(MIN_MAIN_WINDOW_SIZE); m_ZTabWidget = new ZTabWidget(this); m_ZTabWidget->setAttribute(Qt::WA_ContentsMarginsRespectsSafeArea, false); @@ -162,7 +161,7 @@ MainWindow::MainWindow(QWidget *parent) m_ZTabWidget->addTab(m_mainStackedWidget, "iDevice"); auto *appsWidgetTab = m_ZTabWidget->addTab(AppsWidget::sharedInstance(), "Apps"); - m_ZTabWidget->addTab(new ToolboxWidget(this), "Toolbox"); + m_ZTabWidget->addTab(ToolboxWidget::sharedInstance(), "Toolbox"); auto *jailbrokenWidget = new JailbrokenWidget(this); m_ZTabWidget->addTab(jailbrokenWidget, "Jailbroken"); @@ -201,6 +200,10 @@ MainWindow::MainWindow(QWidget *parent) ui->statusbar->addPermanentWidget(appVersionLabel); ui->statusbar->addPermanentWidget(githubButton); ui->statusbar->addPermanentWidget(settingsButton); +#ifdef WIN32 + ui->statusbar->setStyleSheet( + "QStatusBar { border-top: 1px solid #dcdcdc; }"); +#endif #ifdef __linux__ QList mounted_iFusePaths = iFuseManager::getMountPoints(); diff --git a/src/networkdeviceswidget.cpp b/src/networkdeviceswidget.cpp index 151c30b..5799dea 100644 --- a/src/networkdeviceswidget.cpp +++ b/src/networkdeviceswidget.cpp @@ -99,11 +99,6 @@ void NetworkDevicesWidget::setupUI() m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); m_scrollArea->setStyleSheet( "QScrollArea { background: transparent; border: none; }"); - /* FIXME: We need a better approach to theme awareness */ - connect(qApp, &QApplication::paletteChanged, this, [this]() { - m_scrollArea->setStyleSheet( - "QScrollArea { background: transparent; border: none; }"); - }); // Scroll content m_scrollContent = new QWidget(); diff --git a/src/networkdeviceswidget.h b/src/networkdeviceswidget.h index 62a70aa..8762f94 100644 --- a/src/networkdeviceswidget.h +++ b/src/networkdeviceswidget.h @@ -26,6 +26,7 @@ #include "core/services/dnssd/dnssd_service.h" #endif +#include #include #include #include @@ -63,6 +64,18 @@ private: #endif QList m_deviceCards; + +protected: + void changeEvent(QEvent *event) override + { + if (event->type() == QEvent::PaletteChange) { + if (m_scrollArea) { + m_scrollArea->setStyleSheet( + "QScrollArea { background: transparent; border: none; }"); + } + } + QWidget::changeEvent(event); + }; }; #endif // NETWORKDEVICESWIDGET_H \ No newline at end of file diff --git a/src/photoimportdialog.cpp b/src/photoimportdialog.cpp index c131c67..aaf2541 100644 --- a/src/photoimportdialog.cpp +++ b/src/photoimportdialog.cpp @@ -27,21 +27,28 @@ #include #include #include +#include +#include #include PhotoImportDialog::PhotoImportDialog(const QStringList &files, bool hasDirectories, QWidget *parent) : QDialog(parent), selectedFiles(files), - containsDirectories(hasDirectories), m_httpServer(nullptr) + containsDirectories(hasDirectories), m_httpServer(nullptr), + m_mediaPlayer(nullptr) { setupUI(); setModal(true); - resize(600, 500); + resize(600, 700); setWindowTitle("Import Photos to iDevice - iDescriptor"); } PhotoImportDialog::~PhotoImportDialog() { + if (m_mediaPlayer) { + m_mediaPlayer->stop(); + delete m_mediaPlayer; + } if (m_httpServer) { m_httpServer->stop(); delete m_httpServer; @@ -76,26 +83,73 @@ void PhotoImportDialog::setupUI() } mainLayout->addWidget(fileList); + // Horizontal layout for QR code and instructions + QHBoxLayout *contentLayout = new QHBoxLayout(); + // QR Code area qrCodeLabel = new QLabel(this); qrCodeLabel->setAlignment(Qt::AlignCenter); qrCodeLabel->setMinimumSize(200, 200); + qrCodeLabel->setMaximumSize(200, 200); qrCodeLabel->setText("QR Code will appear here after starting server"); - mainLayout->addWidget(qrCodeLabel); + contentLayout->addWidget(qrCodeLabel); - // Instructions - instructionLabel = new QLabel("Loading", this); - mainLayout->addWidget(instructionLabel); + // Instructions container + QVBoxLayout *instructionContainer = new QVBoxLayout(); + + // Stacked widget for switchable instructions + m_instructionStack = new QStackedWidget(this); + m_instructionStack->setSizePolicy(QSizePolicy::Expanding, + QSizePolicy::Expanding); + + // Text instructions + m_instructionLabel = new QLabel("Loading", this); + m_instructionLabel->setWordWrap(true); + m_instructionStack->addWidget(m_instructionLabel); + + // Video instructions + m_instructionVideo = new QVideoWidget(this); + m_instructionVideo->setMinimumSize(300, 500); + m_instructionVideo->setSizePolicy(QSizePolicy::Expanding, + QSizePolicy::Expanding); + m_mediaPlayer = new QMediaPlayer(this); + m_mediaPlayer->setVideoOutput(m_instructionVideo); + m_instructionStack->addWidget(m_instructionVideo); + + m_instructionVideo->setAspectRatioMode( + Qt::AspectRatioMode::KeepAspectRatioByExpanding); + m_instructionVideo->setStyleSheet( + "QVideoWidget { background-color: transparent; }"); + + instructionContainer->addWidget(m_instructionStack); + + // Toggle button + m_toggleInstructionButton = + new QPushButton("Show Video Instructions", this); + connect(m_toggleInstructionButton, &QPushButton::clicked, this, + &PhotoImportDialog::toggleInstructionMode); + + instructionContainer->addSpacing(10); + + QHBoxLayout *buttonContainer = new QHBoxLayout(); + buttonContainer->addStretch(); + buttonContainer->addWidget(m_toggleInstructionButton); + buttonContainer->addStretch(); + instructionContainer->addLayout(buttonContainer); + + contentLayout->addLayout(instructionContainer); + mainLayout->addLayout(contentLayout); // Progress tracking - progressLabel = new QLabel("Download progress will appear here", this); - progressLabel->setVisible(false); - mainLayout->addWidget(progressLabel); + m_progressLabel = new QLabel("Download progress will appear here", this); + m_progressLabel->setVisible(false); + mainLayout->addWidget(m_progressLabel, Qt::AlignCenter); - // Progress bar - progressBar = new QProgressBar(this); - progressBar->setVisible(false); - mainLayout->addWidget(progressBar); + mainLayout->addSpacing(5); + + m_serverAddress = new QLabel("", this); + m_serverAddress->setVisible(false); + mainLayout->addWidget(m_serverAddress, Qt::AlignCenter); // Buttons QHBoxLayout *buttonLayout = new QHBoxLayout(); @@ -108,13 +162,22 @@ void PhotoImportDialog::setupUI() connect(m_cancelButton, &QPushButton::clicked, this, &QDialog::reject); + // Setup video looping + connect(m_mediaPlayer, + QOverload::of( + &QMediaPlayer::mediaStatusChanged), + [this](QMediaPlayer::MediaStatus status) { + if (status == QMediaPlayer::EndOfMedia) { + m_mediaPlayer->setPosition(0); + m_mediaPlayer->play(); + } + }); + QTimer::singleShot(0, this, &PhotoImportDialog::init); } void PhotoImportDialog::init() { - progressBar->setVisible(true); - progressBar->setRange(0, 0); // Indeterminate progress // Create and start HTTP server m_httpServer = new HttpServer(this); @@ -130,7 +193,6 @@ void PhotoImportDialog::init() void PhotoImportDialog::onServerStarted() { - progressBar->setVisible(false); QString localIP = getLocalIP(); int port = m_httpServer->getPort(); @@ -144,28 +206,37 @@ void PhotoImportDialog::onServerStarted() generateQRCode(url); - instructionLabel->setText( - QString("Server started at %1:%2\n\n1. Scan the QR code to open the " - "web interface\n2. Copy the server address and download the " - "shortcut\n3. Run the shortcut on your iOS device") - .arg(localIP) - .arg(port)); + m_instructionLabel->setText( + "Instructions on How to Import\n\n1.Scan the QR code to open the " + "web interface\n2.Click on \"Copy Server Address\"\n3.Click on " + "\"Import and Run Shortcut\" if you have not installed the " + "shortcut before or \"Run Shortcut\" if you have installed it " + "before. \n4.Run the shortcut in the Shortcuts app. Once the " + "shortcut imports to your device, it will automatically run " + "\"Photos app\" \n\n Switch to video tutorial if you want to see a " + "video tutorial."); - progressLabel->setVisible(true); - progressLabel->setText("Waiting for downloads..."); + m_mediaPlayer->setSource( + QUrl("qrc:/resources/wireless-gallery-import.mp4")); + + m_progressLabel->setText("Waiting for downloads..."); + m_progressLabel->setVisible(true); + + m_serverAddress->setText( + QString("Server started at %1:%2").arg(localIP).arg(port)); + m_serverAddress->setVisible(true); } void PhotoImportDialog::onDownloadProgress(const QString &fileName, int bytesDownloaded, int totalBytes) { - progressLabel->setText(QString("Downloaded: %1 (%2 KB)") - .arg(fileName) - .arg(bytesDownloaded / 1024)); + m_progressLabel->setText(QString("Downloaded: %1 (%2 KB)") + .arg(fileName) + .arg(bytesDownloaded / 1024)); } void PhotoImportDialog::onServerError(const QString &error) { - progressBar->setVisible(false); m_cancelButton->setEnabled(true); QMessageBox::critical(this, "Server Error", @@ -224,3 +295,18 @@ QString PhotoImportDialog::getLocalIP() const } return "127.0.0.1"; } + +void PhotoImportDialog::toggleInstructionMode() +{ + if (m_instructionStack->currentIndex() == 0) { + // Switch to video + m_instructionStack->setCurrentIndex(1); + m_toggleInstructionButton->setText("Show Text Instructions"); + m_mediaPlayer->play(); + } else { + // Switch to text + m_instructionStack->setCurrentIndex(0); + m_toggleInstructionButton->setText("Show Video Instructions"); + m_mediaPlayer->stop(); + } +} diff --git a/src/photoimportdialog.h b/src/photoimportdialog.h index 975d950..a8ca23a 100644 --- a/src/photoimportdialog.h +++ b/src/photoimportdialog.h @@ -29,6 +29,9 @@ #include #include #include +#include +#include +#include class PhotoImportDialog : public QDialog { @@ -45,6 +48,7 @@ private slots: void onServerError(const QString &error); void onDownloadProgress(const QString &fileName, int bytesDownloaded, int totalBytes); + void toggleInstructionMode(); private: QStringList selectedFiles; @@ -53,10 +57,14 @@ private: QListWidget *fileList; QLabel *warningLabel; QLabel *qrCodeLabel; - QLabel *instructionLabel; + QStackedWidget *m_instructionStack; + QLabel *m_instructionLabel; + QVideoWidget *m_instructionVideo; + QMediaPlayer *m_mediaPlayer; + QPushButton *m_toggleInstructionButton; QPushButton *m_cancelButton; - QProgressBar *progressBar; - QLabel *progressLabel; + QLabel *m_progressLabel; + QLabel *m_serverAddress; HttpServer *m_httpServer; diff --git a/src/platform/windows/check_deps.cpp b/src/platform/windows/check_deps.cpp index c3cc616..b2a1062 100644 --- a/src/platform/windows/check_deps.cpp +++ b/src/platform/windows/check_deps.cpp @@ -56,6 +56,17 @@ bool CheckRegistry(HKEY hKeyRoot, LPCSTR subKey, LPCSTR displayNameToFind) return false; } +bool CheckRegistryKeyExists(HKEY hKeyRoot, LPCSTR subKey) +{ + HKEY hKey; + LONG result = RegOpenKeyExA(hKeyRoot, subKey, 0, KEY_READ, &hKey); + if (result == ERROR_SUCCESS) { + RegCloseKey(hKey); + return true; + } + return false; +} + bool IsAppleMobileDeviceSupportInstalled() { if (CheckRegistry(HKEY_LOCAL_MACHINE, @@ -101,5 +112,15 @@ bool is_iDescriptorInstalled() "iDescriptor")) { return true; } + return false; +} + +bool IsBonjourServiceInstalled() +{ + if (CheckRegistryKeyExists(HKEY_LOCAL_MACHINE, + "SOFTWARE\\Apple Inc.\\Bonjour")) { + return true; + } + return false; } \ No newline at end of file diff --git a/src/platform/windows/check_deps.h b/src/platform/windows/check_deps.h index 63d8b54..df85ea2 100644 --- a/src/platform/windows/check_deps.h +++ b/src/platform/windows/check_deps.h @@ -23,5 +23,6 @@ bool IsAppleMobileDeviceSupportInstalled(); bool IsWinFspInstalled(); bool is_iDescriptorInstalled(); +bool IsBonjourServiceInstalled(); #endif // CHECK_DEPS_H \ No newline at end of file diff --git a/src/privateinfolabel.cpp b/src/privateinfolabel.cpp index dcc188f..4a92719 100644 --- a/src/privateinfolabel.cpp +++ b/src/privateinfolabel.cpp @@ -28,7 +28,7 @@ PrivateInfoLabel::PrivateInfoLabel(const QString &fullText, QWidget *parent) layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(5); - m_textLabel = new InfoLabel(m_maskedText, this); + m_textLabel = new InfoLabel(m_maskedText, m_fullText, this); layout->addWidget(m_textLabel); m_toggleButton = new ZIconWidget( @@ -55,12 +55,10 @@ void PrivateInfoLabel::toggleVisibility() m_isVisible = !m_isVisible; if (m_isVisible) { m_textLabel->setText(m_fullText); - m_textLabel->setOriginalText(m_fullText); m_toggleButton->setIcon(QIcon(":/resources/icons/ClarityEyeLine.png")); m_toggleButton->setToolTip("Hide"); } else { m_textLabel->setText(m_maskedText); - m_textLabel->setOriginalText(m_fullText); m_toggleButton->setIcon( QIcon(":/resources/icons/ClarityEyeHideLine.png")); m_toggleButton->setToolTip("Show"); diff --git a/src/qprocessindicator.cpp b/src/qprocessindicator.cpp index 8eb8479..78ccdff 100644 --- a/src/qprocessindicator.cpp +++ b/src/qprocessindicator.cpp @@ -35,8 +35,6 @@ QProcessIndicator::QProcessIndicator(QWidget *parent) m_timer = new QTimer(); connect(m_timer, SIGNAL(timeout()), this, SLOT(onTimeout())); - connect(qApp, &QApplication::paletteChanged, this, - &QProcessIndicator::updateStyle); } void QProcessIndicator::updateStyle() { diff --git a/src/qprocessindicator.h b/src/qprocessindicator.h index cf8b2d6..11c75fd 100644 --- a/src/qprocessindicator.h +++ b/src/qprocessindicator.h @@ -22,6 +22,7 @@ #define QPROCESSINDICATOR_H #include +#include #include #include #include @@ -77,6 +78,15 @@ private: qreal m_scale; QTimer *m_timer; + +protected: + void changeEvent(QEvent *event) override + { + if (event->type() == QEvent::PaletteChange) { + updateStyle(); + } + QWidget::changeEvent(event); + }; }; #endif // QPROCESSINDICATOR_H \ No newline at end of file diff --git a/src/servicemanager.cpp b/src/servicemanager.cpp index 1fe4772..cbe9411 100644 --- a/src/servicemanager.cpp +++ b/src/servicemanager.cpp @@ -151,13 +151,13 @@ ServiceManager::safeReadAfcFileToByteArray(iDescriptorDevice *device, } AFCFileTree ServiceManager::safeGetFileTree(iDescriptorDevice *device, - const std::string &path, + const std::string &path, bool checkDir, std::optional altAfc) { return executeOperation( device, - [path](afc_client_t client) -> AFCFileTree { - return get_file_tree(client, path.c_str()); + [path, checkDir](afc_client_t client) -> AFCFileTree { + return get_file_tree(client, path.c_str(), checkDir); }, altAfc); } \ No newline at end of file diff --git a/src/servicemanager.h b/src/servicemanager.h index 446489a..7802e3a 100644 --- a/src/servicemanager.h +++ b/src/servicemanager.h @@ -43,11 +43,11 @@ public: std::function operation, std::optional altAfc = std::nullopt) { - if (!device || !device->mutex) { + if (!device) { return T{}; // Return default-constructed value for the type } - std::lock_guard lock(*device->mutex); + std::lock_guard lock(device->mutex); // Double-check device is still valid after acquiring lock if (!device->afcClient) { @@ -70,11 +70,11 @@ public: std::function operation, std::optional altAfc = std::nullopt) { - if (!device || !device->mutex) { + if (!device) { return T{}; // Return default-constructed value for the type } - std::lock_guard lock(*device->mutex); + std::lock_guard lock(device->mutex); // Double-check device is still valid after acquiring lock if (!device->afcClient) { @@ -94,11 +94,11 @@ public: std::function operation, T failureValue, std::optional altAfc = std::nullopt) { - if (!device || !device->mutex) { + if (!device) { return failureValue; } - std::lock_guard lock(*device->mutex); + std::lock_guard lock(device->mutex); // Double-check device is still valid after acquiring lock if (!device->afcClient) { @@ -118,11 +118,11 @@ public: executeOperation(iDescriptorDevice *device, std::function operation, std::optional altAfc = std::nullopt) { - if (!device || !device->mutex) { + if (!device) { return; } - std::lock_guard lock(*device->mutex); + std::lock_guard lock(device->mutex); // Double-check device is still valid after acquiring lock if (!device->afcClient) { @@ -144,11 +144,11 @@ public: std::optional altAfc = std::nullopt) { try { - if (!device || !device->mutex) { + if (!device) { return AFC_E_UNKNOWN_ERROR; } - std::lock_guard lock(*device->mutex); + std::lock_guard lock(device->mutex); // Double-check device is still valid after acquiring lock if (!device->afcClient) { @@ -215,6 +215,7 @@ public: std::optional altAfc = std::nullopt); static AFCFileTree safeGetFileTree(iDescriptorDevice *device, const std::string &path = "/", + bool checkDir = true, std::optional altAfc = std::nullopt); }; diff --git a/src/settingsmanager.cpp b/src/settingsmanager.cpp index a8be4f6..65c688b 100644 --- a/src/settingsmanager.cpp +++ b/src/settingsmanager.cpp @@ -168,6 +168,17 @@ void SettingsManager::setConnectionTimeout(int seconds) m_settings->sync(); } +int SettingsManager::wirelessFileServerPort() const +{ + return m_settings->value("wirelessFileServerPort", 8080).toInt(); +} + +void SettingsManager::setWirelessFileServerPort(int port) +{ + m_settings->setValue("wirelessFileServerPort", port); + m_settings->sync(); +} + bool SettingsManager::showKeychainDialog() const { return m_settings->value("showKeychainDialog", true).toBool(); @@ -235,6 +246,13 @@ void SettingsManager::resetToDefaults() setConnectionTimeout(30); setShowKeychainDialog(true); setDefaultJailbrokenRootPassword("alpine"); + setIconSizeBaseMultiplier(1.0); + setAirplayFps(60); + setAirplayNoHold(true); + setWirelessFileServerPort(8080); +#ifdef __linux__ + setShowV4L2(false); +#endif } void SettingsManager::saveFavoritePlace(const QString &path, @@ -390,4 +408,38 @@ void SettingsManager::setIconSizeBaseMultiplier(double multiplier) { m_settings->setValue("iconSizeBaseMultiplier", multiplier); m_settings->sync(); -} \ No newline at end of file +} + +int SettingsManager::airplayFps() const +{ + return m_settings->value("airplayFps", 60).toInt(); +} + +void SettingsManager::setAirplayFps(int fps) +{ + m_settings->setValue("airplayFps", fps); + m_settings->sync(); +} + +bool SettingsManager::airplayNoHold() const +{ + return m_settings->value("airplayNoHold", true).toBool(); +} +void SettingsManager::setAirplayNoHold(bool noHold) +{ + m_settings->setValue("airplayNoHold", noHold); + m_settings->sync(); +} + +#ifdef __linux__ +bool SettingsManager::showV4L2() const +{ + return m_settings->value("showV4L2", false).toBool(); +} + +void SettingsManager::setShowV4L2(bool show) +{ + m_settings->setValue("showV4L2", show); + m_settings->sync(); +} +#endif \ No newline at end of file diff --git a/src/settingsmanager.h b/src/settingsmanager.h index 0e3b47a..372ddff 100644 --- a/src/settingsmanager.h +++ b/src/settingsmanager.h @@ -86,6 +86,9 @@ public: int connectionTimeout() const; void setConnectionTimeout(int seconds); + int wirelessFileServerPort() const; + void setWirelessFileServerPort(int port); + bool showKeychainDialog() const; void setShowKeychainDialog(bool show); @@ -105,6 +108,17 @@ public: double iconSizeBaseMultiplier() const; void setIconSizeBaseMultiplier(double multiplier); + + int airplayFps() const; + void setAirplayFps(int fps); + + bool airplayNoHold() const; + void setAirplayNoHold(bool noHold); + +#ifdef __linux__ + bool showV4L2() const; + void setShowV4L2(bool show); +#endif signals: void favoritePlacesChanged(); void recentLocationsChanged(); diff --git a/src/settingswidget.cpp b/src/settingswidget.cpp index 1c06e78..6a2736a 100644 --- a/src/settingswidget.cpp +++ b/src/settingswidget.cpp @@ -42,6 +42,10 @@ SettingsWidget::SettingsWidget(QWidget *parent) : QDialog{parent} setupUI(); loadSettings(); connectSignals(); + // due to scrollbar add 10px on windows +#ifdef WIN32 + resize(sizeHint().width() + 10, sizeHint().height()); +#endif } void SettingsWidget::setupUI() @@ -54,6 +58,7 @@ void SettingsWidget::setupUI() auto *scrollArea = new QScrollArea(); auto *scrollWidget = new QWidget(); auto *scrollLayout = new QVBoxLayout(scrollWidget); + scrollLayout->setContentsMargins(10, 10, 10, 10); // === GENERAL SETTINGS === auto *generalGroup = new QGroupBox("General"); @@ -70,6 +75,18 @@ void SettingsWidget::setupUI() downloadLayout->addWidget(browseButton); generalLayout->addLayout(downloadLayout); + // Wireless file server port + auto *portLayout = new QHBoxLayout(); + portLayout->addWidget(new QLabel("Wireless File Server Port:")); + m_wirelessFileServerPort = new QSpinBox(); + m_wirelessFileServerPort->setRange(1024, 65535); + m_wirelessFileServerPort->setToolTip( + "The starting port for the wireless file server. If this port is " + "unavailable, it will try the next 10 ports."); + portLayout->addWidget(m_wirelessFileServerPort); + portLayout->addStretch(); + generalLayout->addLayout(portLayout); + // Unmount iFuse drives on exit (not implemented on macOS) // TODO: Implement #ifndef __APPLE__ @@ -153,6 +170,34 @@ void SettingsWidget::setupUI() scrollLayout->addWidget(jailbrokenGroup); + // === AirPlay SETTINGS === + auto *airplayGroup = new QGroupBox("AirPlay"); + auto *airplayLayout = new QVBoxLayout(airplayGroup); + + auto *fpsLayout = new QHBoxLayout(); + + auto *fpsLabel = new QLabel("Fps:"); + m_fpsComboBox = new QComboBox(); + m_fpsComboBox->addItems({"24", "30", "60", "120"}); + m_fpsComboBox->setToolTip( + "Set the fps for AirPlay. Go with 30 fps if have an older device."); + + fpsLayout->addWidget(fpsLabel); + fpsLayout->addWidget(m_fpsComboBox); + fpsLayout->addStretch(); + airplayLayout->addLayout(fpsLayout); + + m_noHoldCheckbox = new QCheckBox("Allow New Connections to Take Over"); + airplayLayout->addWidget(m_noHoldCheckbox); + + +#ifdef __linux__ + m_showV4L2CheckBox = new QCheckBox("Show V4L2 Button on AirPlay Widget"); + airplayLayout->addWidget(m_showV4L2CheckBox); +#endif + + scrollLayout->addWidget(airplayGroup); + // === MISCELLANEOUS SETTINGS === auto *miscGroup = new QGroupBox("Miscellaneous"); auto *miscLayout = new QVBoxLayout(miscGroup); @@ -183,7 +228,7 @@ void SettingsWidget::setupUI() QString( "iDescriptor v%1\n" "A free, open-source, and cross-platform iDevice management tool.\n" - "© 2025 See AUTHORS for details. Licensed under AGPLv3.") + "© 2026 See AUTHORS for details. Licensed under AGPLv3.") .arg(APP_VERSION)); footerLabel->setAlignment(Qt::AlignCenter); footerLabel->setStyleSheet("color: gray; font-size: 8pt;"); @@ -229,6 +274,7 @@ void SettingsWidget::loadSettings() m_autoUpdateCheck->setChecked(sm->autoCheckUpdates()); m_autoRaiseWindow->setChecked(sm->autoRaiseWindow()); m_switchToNewDevice->setChecked(sm->switchToNewDevice()); + m_wirelessFileServerPort->setValue(sm->wirelessFileServerPort()); #ifndef __APPLE__ m_unmount_iFuseDrives->setChecked(sm->unmountiFuseOnExit()); @@ -250,6 +296,11 @@ void SettingsWidget::loadSettings() m_applyButton->setEnabled(false); m_iconSizeBaseMultiplier->setValue(sm->iconSizeBaseMultiplier()); + m_fpsComboBox->setCurrentText(QString::number(sm->airplayFps())); + m_noHoldCheckbox->setChecked(sm->airplayNoHold()); +#ifdef __linux__ + m_showV4L2CheckBox->setChecked(sm->showV4L2()); +#endif } void SettingsWidget::connectSignals() @@ -269,6 +320,9 @@ void SettingsWidget::connectSignals() this, &SettingsWidget::onSettingChanged); connect(m_connectionTimeout, QOverload::of(&QSpinBox::valueChanged), this, &SettingsWidget::onSettingChanged); + connect(m_wirelessFileServerPort, + QOverload::of(&QSpinBox::valueChanged), this, + &SettingsWidget::onSettingChanged); connect(m_iconSizeBaseMultiplier, QOverload::of(&QDoubleSpinBox::valueChanged), this, @@ -301,6 +355,14 @@ void SettingsWidget::connectSignals() connect(m_defaultJailbrokenRootPassword, &QLineEdit::textChanged, this, &SettingsWidget::onSettingChanged); + connect(m_fpsComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &SettingsWidget::onSettingChanged); + connect(m_noHoldCheckbox, &QCheckBox::toggled, this, + &SettingsWidget::onSettingChanged); +#ifdef __linux__ + connect(m_showV4L2CheckBox, &QCheckBox::toggled, this, + &SettingsWidget::onSettingChanged); +#endif } void SettingsWidget::onBrowseButtonClicked() @@ -320,13 +382,20 @@ void SettingsWidget::onCheckUpdatesClicked() m_checkUpdatesButton->setText("Checking..."); m_checkUpdatesButton->setEnabled(false); - MainWindow::sharedInstance()->m_updater->checkForUpdates(); + connect( + MainWindow::sharedInstance()->m_updater, &ZUpdater::dataAvailable, this, + [this](const QJsonDocument data, bool isUpdateAvailable) { + if (!isUpdateAvailable) { + QMessageBox::information(this, "No Updates", + "You are using the latest version of " + "iDescriptor."); + } + m_checkUpdatesButton->setText("Check for Updates"); + m_checkUpdatesButton->setEnabled(true); + }, + Qt::SingleShotConnection); - // Simulate check (replace with actual update check) - QTimer::singleShot(2000, this, [this]() { - m_checkUpdatesButton->setText("Check for Updates"); - m_checkUpdatesButton->setEnabled(true); - }); + MainWindow::sharedInstance()->m_updater->checkForUpdates(); } void SettingsWidget::onResetToDefaultsClicked() @@ -367,6 +436,7 @@ void SettingsWidget::saveSettings() sm->setAutoCheckUpdates(m_autoUpdateCheck->isChecked()); sm->setAutoRaiseWindow(m_autoRaiseWindow->isChecked()); sm->setSwitchToNewDevice(m_switchToNewDevice->isChecked()); + sm->setWirelessFileServerPort(m_wirelessFileServerPort->value()); #ifndef __APPLE__ sm->setUnmountiFuseOnExit(m_unmount_iFuseDrives->isChecked()); @@ -380,6 +450,11 @@ void SettingsWidget::saveSettings() sm->setIconSizeBaseMultiplier(m_iconSizeBaseMultiplier->value()); + sm->setAirplayFps(m_fpsComboBox->currentText().toInt()); + sm->setAirplayNoHold(m_noHoldCheckbox->isChecked()); +#ifdef __linux__ + sm->setShowV4L2(m_showV4L2CheckBox->isChecked()); +#endif m_applyButton->setEnabled(false); } diff --git a/src/settingswidget.h b/src/settingswidget.h index 82276d9..1ecccd6 100644 --- a/src/settingswidget.h +++ b/src/settingswidget.h @@ -52,6 +52,7 @@ private: // UI Elements // General QLineEdit *m_downloadPathEdit; + QSpinBox *m_wirelessFileServerPort; QCheckBox *m_autoUpdateCheck; QComboBox *m_themeCombo; QCheckBox *m_autoRaiseWindow; @@ -68,6 +69,14 @@ private: QDoubleSpinBox *m_iconSizeBaseMultiplier; + // Airplay + QComboBox *m_fpsComboBox; + QCheckBox *m_noHoldCheckbox; + +#ifdef __linux__ + QCheckBox *m_showV4L2CheckBox; +#endif + // Buttons QPushButton *m_checkUpdatesButton; QPushButton *m_resetButton; diff --git a/src/sshterminalwidget.cpp b/src/sshterminalwidget.cpp index 7c043d1..d6bd2f6 100644 --- a/src/sshterminalwidget.cpp +++ b/src/sshterminalwidget.cpp @@ -283,12 +283,15 @@ void SSHTerminalWidget::initWiredDevice() qDebug() << "Starting iproxy with args:" << args; QString iproxyPath; + QString appDirPath = QCoreApplication::applicationDirPath(); + QString bundledIproxyPath = appDirPath + "/iproxy"; - /* - Check if running in AppImage - this is set by the plugin script - */ - if (qEnvironmentVariableIsSet("IPROXY_BIN_APPIMAGE")) { + /* MacOS bundled iproxy */ + if (QFileInfo(bundledIproxyPath).isExecutable()) { + iproxyPath = bundledIproxyPath; + } + /* AppImage - this is set by the plugin script */ + else if (qEnvironmentVariableIsSet("IPROXY_BIN_APPIMAGE")) { iproxyPath = qgetenv("IPROXY_BIN_APPIMAGE"); if (iproxyPath.isEmpty()) { showError("Error: Running in AppImage mode, but " diff --git a/src/toolboxwidget.cpp b/src/toolboxwidget.cpp index d6afc3d..abc35c6 100644 --- a/src/toolboxwidget.cpp +++ b/src/toolboxwidget.cpp @@ -79,6 +79,12 @@ bool enterRecoveryMode(iDescriptorDevice *device) } } +ToolboxWidget *ToolboxWidget::sharedInstance() +{ + static ToolboxWidget *instance = new ToolboxWidget(); + return instance; +} + ToolboxWidget::ToolboxWidget(QWidget *parent) : QWidget{parent} { setupUI(); @@ -307,9 +313,10 @@ ClickableWidget *ToolboxWidget::createToolbox(iDescriptorTool tool, b->setCursor(Qt::PointingHandCursor); m_toolboxes.append(b); - m_requiresDevice.append(requiresDevice); - connect(b, &ClickableWidget::clicked, - [this, tool]() { onToolboxClicked(tool); }); + b->setProperty("requiresDevice", requiresDevice); + connect(b, &ClickableWidget::clicked, [this, tool, requiresDevice]() { + onToolboxClicked(tool, requiresDevice); + }); return b; } @@ -324,6 +331,7 @@ void ToolboxWidget::updateDeviceList() if (devices.isEmpty()) { m_deviceCombo->addItem("No device connected"); m_deviceCombo->setEnabled(false); + m_uuid.clear(); } else { m_deviceCombo->setEnabled(true); for (iDescriptorDevice *device : devices) { @@ -348,7 +356,7 @@ void ToolboxWidget::updateToolboxStates() for (int i = 0; i < m_toolboxes.size(); ++i) { QWidget *toolbox = m_toolboxes[i]; - bool requiresDevice = m_requiresDevice[i]; + bool requiresDevice = toolbox->property("requiresDevice").toBool(); bool enabled = !requiresDevice || hasDevice; toolbox->setEnabled(enabled); @@ -374,9 +382,21 @@ void ToolboxWidget::onDeviceSelectionChanged() { QString selectedUdid = m_deviceCombo->currentData().toString(); if (selectedUdid.isEmpty()) { + m_uuid.clear(); return; } + if (AppContext::sharedInstance()->getDevice(selectedUdid.toStdString()) == + nullptr) { + QMessageBox::warning(this, "Device Not Found", + "The selected device is no longer connected."); + m_uuid.clear(); // Clear stale UUID + updateDeviceList(); + return; + } + + m_uuid = selectedUdid.toStdString(); + // Update the selected device in main menu AppContext::sharedInstance()->setCurrentDeviceSelection( DeviceSelection(selectedUdid.toStdString())); @@ -394,14 +414,19 @@ void ToolboxWidget::onCurrentDeviceChanged(const DeviceSelection &selection) m_deviceCombo->blockSignals(false); m_uuid = selection.udid; - m_currentDevice = - AppContext::sharedInstance()->getDevice(selection.udid); } } } -void ToolboxWidget::onToolboxClicked(iDescriptorTool tool) +void ToolboxWidget::onToolboxClicked(iDescriptorTool tool, bool requiresDevice) { + iDescriptorDevice *device = AppContext::sharedInstance()->getDevice(m_uuid); + if (!device && requiresDevice) { + QMessageBox::warning( + this, "Device Disconnected ?", + "Device just disconnected, please select a device."); + return; + } switch (tool) { case iDescriptorTool::Airplayer: { @@ -420,25 +445,16 @@ void ToolboxWidget::onToolboxClicked(iDescriptorTool tool) } break; case iDescriptorTool::LiveScreen: { - LiveScreenWidget *liveScreen = new LiveScreenWidget(m_currentDevice); + LiveScreenWidget *liveScreen = new LiveScreenWidget(device); liveScreen->setAttribute(Qt::WA_DeleteOnClose); liveScreen->show(); } break; case iDescriptorTool::RecoveryMode: { - // Handle entering recovery mode - bool success = enterRecoveryMode(m_currentDevice); - QMessageBox msgBox; - msgBox.setWindowTitle("Recovery Mode"); - if (success) { - msgBox.setText("Successfully entered recovery mode."); - } else { - msgBox.setText("Failed to enter recovery mode."); - } - msgBox.exec(); + _enterRecoveryMode(device); } break; case iDescriptorTool::MountDevImage: { DevDiskImageHelper *devDiskImageHelper = - new DevDiskImageHelper(m_currentDevice, this); + new DevDiskImageHelper(device, this); connect(devDiskImageHelper, &DevDiskImageHelper::mountingCompleted, this, [this, devDiskImageHelper](bool success) { @@ -457,22 +473,22 @@ void ToolboxWidget::onToolboxClicked(iDescriptorTool tool) } break; case iDescriptorTool::VirtualLocation: { // Handle virtual location functionality - VirtualLocation *virtualLocation = new VirtualLocation(m_currentDevice); + VirtualLocation *virtualLocation = new VirtualLocation(device); virtualLocation->setAttribute(Qt::WA_DeleteOnClose); virtualLocation->setWindowFlag(Qt::Window); virtualLocation->resize(800, 600); virtualLocation->show(); } break; case iDescriptorTool::Restart: { - restartDevice(m_currentDevice); + restartDevice(device); } break; case iDescriptorTool::Shutdown: { - shutdownDevice(m_currentDevice); + shutdownDevice(device); } break; case iDescriptorTool::QueryMobileGestalt: { // Handle querying MobileGestalt QueryMobileGestaltWidget *queryMobileGestaltWidget = - new QueryMobileGestaltWidget(m_currentDevice); + new QueryMobileGestaltWidget(device); queryMobileGestaltWidget->setAttribute(Qt::WA_DeleteOnClose); queryMobileGestaltWidget->setWindowFlag(Qt::Window); queryMobileGestaltWidget->resize(800, 600); @@ -480,7 +496,7 @@ void ToolboxWidget::onToolboxClicked(iDescriptorTool tool) } break; case iDescriptorTool::DeveloperDiskImages: { if (!m_devDiskImagesWidget) { - m_devDiskImagesWidget = new DevDiskImagesWidget(m_currentDevice); + m_devDiskImagesWidget = new DevDiskImagesWidget(device); m_devDiskImagesWidget->setAttribute(Qt::WA_DeleteOnClose); m_devDiskImagesWidget->setWindowFlag(Qt::Window); m_devDiskImagesWidget->resize(800, 600); @@ -509,9 +525,9 @@ void ToolboxWidget::onToolboxClicked(iDescriptorTool tool) #ifndef __APPLE__ case iDescriptorTool::iFuse: { if (!m_ifuseWidget) { - m_ifuseWidget = new iFuseWidget(m_currentDevice); + m_ifuseWidget = new iFuseWidget(device); qDebug() << "Created iFuseWidget" - << m_currentDevice->deviceInfo.productType.c_str(); + << device->deviceInfo.productType.c_str(); m_ifuseWidget->setAttribute(Qt::WA_DeleteOnClose); connect(m_ifuseWidget, &QObject::destroyed, this, [this]() { m_ifuseWidget = nullptr; }); @@ -525,7 +541,7 @@ void ToolboxWidget::onToolboxClicked(iDescriptorTool tool) } break; #endif case iDescriptorTool::CableInfoWidget: { - CableInfoWidget *cableInfoWidget = new CableInfoWidget(m_currentDevice); + CableInfoWidget *cableInfoWidget = new CableInfoWidget(device); cableInfoWidget->setAttribute(Qt::WA_DeleteOnClose); cableInfoWidget->setWindowFlag(Qt::Window); cableInfoWidget->resize(600, 400); @@ -624,4 +640,24 @@ void ToolboxWidget::_enterRecoveryMode(iDescriptorDevice *device) _msgBox.setText("Failed to enter recovery mode."); } _msgBox.exec(); +} + +void ToolboxWidget::restartAirPlayWindow() +{ + if (!m_airplayWindow) { + onToolboxClicked(iDescriptorTool::Airplayer, false); + return; + } + + connect( + m_airplayWindow, &QObject::destroyed, this, + [this]() { + // give some time for cleanup + QTimer::singleShot(100, this, [this]() { + onToolboxClicked(iDescriptorTool::Airplayer, false); + }); + }, + Qt::SingleShotConnection); + + m_airplayWindow->close(); } \ No newline at end of file diff --git a/src/toolboxwidget.h b/src/toolboxwidget.h index 1aa65b6..4320564 100644 --- a/src/toolboxwidget.h +++ b/src/toolboxwidget.h @@ -47,9 +47,11 @@ public: static void restartDevice(iDescriptorDevice *device); static void shutdownDevice(iDescriptorDevice *device); static void _enterRecoveryMode(iDescriptorDevice *device); + static ToolboxWidget *sharedInstance(); + void restartAirPlayWindow(); private slots: void onDeviceSelectionChanged(); - void onToolboxClicked(iDescriptorTool tool); + void onToolboxClicked(iDescriptorTool tool, bool requiresDevice); void onCurrentDeviceChanged(const DeviceSelection &selection); private: @@ -66,8 +68,6 @@ private: QWidget *m_contentWidget; QGridLayout *m_gridLayout; QList m_toolboxes; - QList m_requiresDevice; - iDescriptorDevice *m_currentDevice; std::string m_uuid; DevDiskImagesWidget *m_devDiskImagesWidget = nullptr; NetworkDevicesWidget *m_networkDevicesWidget = nullptr; diff --git a/src/welcomewidget.cpp b/src/welcomewidget.cpp index 57bb706..05764ba 100644 --- a/src/welcomewidget.cpp +++ b/src/welcomewidget.cpp @@ -79,15 +79,11 @@ void WelcomeWidget::setupUI() connect(m_githubLabel, &ZLabel::clicked, this, []() { QDesktopServices::openUrl(QUrl(REPO_URL)); }); - // Make it look like a link QPalette githubPalette = m_githubLabel->palette(); githubPalette.setColor(QPalette::WindowText, QColor(0, 122, 255)); // Apple blue m_githubLabel->setPalette(githubPalette); - // Connect click functionality using installEventFilter - m_githubLabel->installEventFilter(this); - m_mainLayout->addWidget(m_githubLabel, 0, Qt::AlignCenter); // no additional deps needed on macOS diff --git a/src/wirelessgalleryimportwidget.cpp b/src/wirelessgalleryimportwidget.cpp index ebc5808..25e9029 100644 --- a/src/wirelessgalleryimportwidget.cpp +++ b/src/wirelessgalleryimportwidget.cpp @@ -29,25 +29,13 @@ #include WirelessGalleryImportWidget::WirelessGalleryImportWidget(QWidget *parent) - : QWidget(parent), m_leftPanel(nullptr), m_scrollArea(nullptr), - m_scrollContent(nullptr), m_fileListLayout(nullptr), - m_browseButton(nullptr), m_importButton(nullptr), m_statusLabel(nullptr), - m_rightPanel(nullptr), m_tutorialPlayer(nullptr), - m_tutorialVideoWidget(nullptr), m_loadingIndicator(nullptr), - m_loadingLabel(nullptr), m_tutorialLayout(nullptr) + : QWidget(parent), m_scrollArea(nullptr), m_scrollContent(nullptr), + m_fileListLayout(nullptr), m_browseButton(nullptr), + m_importButton(nullptr), m_statusLabel(nullptr) { setupUI(); - setMinimumSize(800, 600); + setMinimumSize(400, 400); setWindowTitle("Wireless Gallery Import - iDescriptor"); - QTimer::singleShot(100, this, - &WirelessGalleryImportWidget::setupTutorialVideo); -} - -WirelessGalleryImportWidget::~WirelessGalleryImportWidget() -{ - if (m_tutorialPlayer) { - m_tutorialPlayer->stop(); - } } void WirelessGalleryImportWidget::setupUI() @@ -57,21 +45,20 @@ void WirelessGalleryImportWidget::setupUI() mainLayout->setSpacing(10); // Left panel - file selection - m_leftPanel = new QWidget(); - QVBoxLayout *leftLayout = new QVBoxLayout(m_leftPanel); - leftLayout->setContentsMargins(0, 0, 0, 0); - leftLayout->setSpacing(10); + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(10); // Browse button m_browseButton = new QPushButton("Select Files"); connect(m_browseButton, &QPushButton::clicked, this, &WirelessGalleryImportWidget::onBrowseFiles); - leftLayout->addWidget(m_browseButton); + layout->addWidget(m_browseButton); // Status label m_statusLabel = new QLabel("No files selected"); m_statusLabel->setWordWrap(true); - leftLayout->addWidget(m_statusLabel); + layout->addWidget(m_statusLabel); // Scroll area for file list m_scrollArea = new QScrollArea(); @@ -86,87 +73,16 @@ void WirelessGalleryImportWidget::setupUI() m_fileListLayout->addStretch(); m_scrollArea->setWidget(m_scrollContent); - leftLayout->addWidget(m_scrollArea, 1); + layout->addWidget(m_scrollArea, 1); // Import button m_importButton = new QPushButton("Import to Gallery"); m_importButton->setEnabled(false); connect(m_importButton, &QPushButton::clicked, this, &WirelessGalleryImportWidget::onImportPhotos); - leftLayout->addWidget(m_importButton); + layout->addWidget(m_importButton); - mainLayout->addWidget(m_leftPanel, 1); - - // Right panel - tutorial video - m_rightPanel = new QWidget(); - m_tutorialLayout = new QVBoxLayout(m_rightPanel); - m_tutorialLayout->setContentsMargins(0, 0, 0, 0); - m_tutorialLayout->setSpacing(10); - - // Loading indicator - m_loadingIndicator = new QProcessIndicator(); - m_loadingIndicator->setType(QProcessIndicator::line_rotate); - m_loadingIndicator->setFixedSize(64, 32); - m_loadingIndicator->start(); - - QHBoxLayout *loadingLayout = new QHBoxLayout(); - m_loadingLabel = new QLabel("Loading tutorial..."); - m_loadingLabel->setAlignment(Qt::AlignCenter); - - loadingLayout->addWidget(m_loadingLabel); - loadingLayout->addWidget(m_loadingIndicator); - loadingLayout->setAlignment(Qt::AlignCenter); - - m_tutorialLayout->addStretch(); - m_tutorialLayout->addLayout(loadingLayout); - m_tutorialLayout->addStretch(); - - mainLayout->addWidget(m_rightPanel, 1); -} - -void WirelessGalleryImportWidget::setupTutorialVideo() -{ - m_tutorialPlayer = new QMediaPlayer(this); - m_tutorialVideoWidget = new QVideoWidget(); - m_tutorialVideoWidget->setSizePolicy(QSizePolicy::Expanding, - QSizePolicy::Expanding); - - m_tutorialPlayer->setVideoOutput(m_tutorialVideoWidget); - m_tutorialPlayer->setSource( - QUrl("qrc:/resources/wireless-gallery-import.mp4")); - m_tutorialVideoWidget->setAspectRatioMode( - Qt::AspectRatioMode::KeepAspectRatioByExpanding); - - // Loop the tutorial video - connect(m_tutorialPlayer, &QMediaPlayer::mediaStatusChanged, this, - [this](QMediaPlayer::MediaStatus status) { - if (status == QMediaPlayer::EndOfMedia) { - m_tutorialPlayer->setPosition(0); - m_tutorialPlayer->play(); - } - }); - - // Auto-play when ready and hide loading indicator - connect(m_tutorialPlayer, &QMediaPlayer::mediaStatusChanged, this, - [this](QMediaPlayer::MediaStatus status) { - if (status == QMediaPlayer::LoadedMedia) { - m_loadingIndicator->stop(); - m_loadingIndicator->setVisible(false); - m_loadingLabel->setVisible(false); - m_tutorialPlayer->play(); - } - }); - - // Clear the loading layout and add video widget - QLayoutItem *child; - while ((child = m_tutorialLayout->takeAt(0)) != nullptr) { - if (child->widget()) { - child->widget()->setParent(nullptr); - } - delete child; - } - - m_tutorialLayout->addWidget(m_tutorialVideoWidget); + mainLayout->addLayout(layout); } void WirelessGalleryImportWidget::onBrowseFiles() diff --git a/src/wirelessgalleryimportwidget.h b/src/wirelessgalleryimportwidget.h index 972a024..30ae68c 100644 --- a/src/wirelessgalleryimportwidget.h +++ b/src/wirelessgalleryimportwidget.h @@ -38,7 +38,6 @@ class WirelessGalleryImportWidget : public QWidget public: explicit WirelessGalleryImportWidget(QWidget *parent = nullptr); - ~WirelessGalleryImportWidget(); QStringList getSelectedFiles() const; @@ -46,11 +45,8 @@ private slots: void onBrowseFiles(); void onImportPhotos(); void onRemoveFile(int index); - void setupTutorialVideo(); private: - // Left panel - file selection - QWidget *m_leftPanel; QScrollArea *m_scrollArea; QWidget *m_scrollContent; QVBoxLayout *m_fileListLayout; @@ -58,14 +54,6 @@ private: QPushButton *m_importButton; QLabel *m_statusLabel; - // Right panel - tutorial video - QWidget *m_rightPanel; - QMediaPlayer *m_tutorialPlayer; - QVideoWidget *m_tutorialVideoWidget; - QProcessIndicator *m_loadingIndicator; - QLabel *m_loadingLabel; - QVBoxLayout *m_tutorialLayout; - QStringList m_selectedFiles; void setupUI(); diff --git a/src/zlineedit.cpp b/src/zlineedit.cpp index c311253..d90a47a 100644 --- a/src/zlineedit.cpp +++ b/src/zlineedit.cpp @@ -28,14 +28,7 @@ ZLineEdit::ZLineEdit(const QString &text, QWidget *parent) setupStyles(); } -void ZLineEdit::setupStyles() -{ - updateStyles(); - - // Connect to palette changes for dynamic theme updates - connect(qApp, &QApplication::paletteChanged, this, - &ZLineEdit::updateStyles); -} +void ZLineEdit::setupStyles() { updateStyles(); } void ZLineEdit::updateStyles() { diff --git a/src/zlineedit.h b/src/zlineedit.h index 026ba8f..a5699fe 100644 --- a/src/zlineedit.h +++ b/src/zlineedit.h @@ -20,6 +20,7 @@ #pragma once #include +#include #include class ZLineEdit : public QLineEdit @@ -35,4 +36,13 @@ private slots: private: void setupStyles(); + +protected: + void changeEvent(QEvent *event) override + { + if (event->type() == QEvent::PaletteChange) { + updateStyles(); + } + QLineEdit::changeEvent(event); + } }; \ No newline at end of file diff --git a/util/get-msys2-archive.ps1 b/util/get-msys2-archive.ps1 new file mode 100644 index 0000000..7918e44 --- /dev/null +++ b/util/get-msys2-archive.ps1 @@ -0,0 +1,41 @@ +param( + [Parameter(Mandatory = $true)] + [string]$Date +) + +$ErrorActionPreference = "Stop" + +if (-not $Date) { + Write-Error "MSYS2 archive date is required. Usage: get-msys2-archive.ps1 -Date " + exit 1 +} + +Write-Host "Using MSYS2 archive release date: $Date" + +# Base URL for the MSYS2 archive release +$baseUrl = "https://github.com/msys2/msys2-archive/releases/download/$Date" + +$databases = @( + "clang64", + "clangarm64", + "mingw32", + "mingw64", + "msys", + "ucrt64" +) + +$targetDir = "C:\msys64\var\lib\pacman\sync" + +foreach ($db in $databases) { + $dbUrl = "$baseUrl/$db.db" + $sigUrl = "$baseUrl/$db.db.sig" + + $dbFile = Join-Path $targetDir "$db.db" + $sigFile = Join-Path $targetDir "$db.db.sig" + + Write-Host "Downloading $db.db ..." + Invoke-WebRequest -Uri $dbUrl -OutFile $dbFile + + Write-Host "Downloading $db.db.sig ..." + Invoke-WebRequest -Uri $sigUrl -OutFile $sigFile +} \ No newline at end of file