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
+
## 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