mirror of
https://github.com/iDescriptor/iDescriptor.git
synced 2026-06-21 19:35:49 +08:00
Merge branch 'iDescriptor:main' into main
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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: |
|
||||
|
||||
+4
-1
@@ -10,4 +10,7 @@ build-dir
|
||||
*.so
|
||||
*.exe
|
||||
*.dll
|
||||
devdiskimgs
|
||||
devdiskimgs
|
||||
.qt
|
||||
.qtcreator
|
||||
CMakeFiles
|
||||
+3
-3
@@ -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
|
||||
|
||||
+20
-9
@@ -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" )
|
||||
# $<$<COMPILE_LANGUAGE:CXX> fixes winres compiler errors
|
||||
target_include_directories(iDescriptor PRIVATE
|
||||
$<$<COMPILE_LANGUAGE:CXX>:${DNSSD_INCLUDE_DIR}>
|
||||
)
|
||||
else()
|
||||
pkg_check_modules(AVAHI_CLIENT REQUIRED IMPORTED_TARGET avahi-client)
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
Open the `.dmg` and drag iDescriptor to Applications.
|
||||
<br/>
|
||||
|
||||
After moving the app to Applications, run the code below
|
||||
After moving the app to Applications, run the code below
|
||||
<br/>
|
||||
|
||||
```shell
|
||||
@@ -105,14 +105,28 @@ make sure to do "sudo pacman -Syu" otherwise it's not going to find libimobilede
|
||||
<img src="./resources/repo/crossplatform.png"><br/>
|
||||
</p>
|
||||
|
||||
</br>
|
||||
</br>
|
||||
</br>
|
||||
|
||||
## Good News!
|
||||
|
||||
### iDescriptor v0.3.0 will feature **WIRELESS CONNECTION** support!
|
||||
|
||||
Learn more about our roadmap [here](#roadmap).
|
||||
|
||||
</br>
|
||||
</br>
|
||||
</br>
|
||||
|
||||
## 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 <a href="https://opencollective.com/idescriptor">
|
||||
<div style="flex-direction: row; align-items: center; display: flex; gap: 10px;">
|
||||
<span>You can become a sponsor from GitHub Sponsors or</span> <a href="https://opencollective.com/idescriptor">
|
||||
<img src="https://img.shields.io/badge/OpenCollective-1F87FF?style=for-the-badge&logo=OpenCollective&logoColor=white" alt="AppImage"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
+62
-4
@@ -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()
|
||||
|
||||
|
||||
-1
Submodule lib/airplay deleted from 9fe5788667
Submodule
+1
Submodule lib/uxplay added at 758fec4be6
+1
-1
Submodule lib/zupdater updated: 61aea855c8...8ff5248c44
+1
-1
@@ -61,7 +61,7 @@
|
||||
<file>resources/iphone-mockups/iphone-15.png</file>
|
||||
<file>resources/iphone-mockups/iphone-16.png</file>
|
||||
<file>resources/connect.png</file>
|
||||
<file>resources/airplayer-tutorial.mp4</file>
|
||||
<file>resources/airplay-tutorial.mp4</file>
|
||||
<file>resources/ipad-mockups/ipad.png</file>
|
||||
<file>DeveloperDiskImages.json</file>
|
||||
<file>resources/keychain.mp4</file>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
+21
-2
@@ -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}"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "iDescriptor-ui.h"
|
||||
#include "iDescriptor.h"
|
||||
#include <QAction>
|
||||
#include <QEvent>
|
||||
#include <QHBoxLayout>
|
||||
#include <QInputDialog>
|
||||
#include <QLabel>
|
||||
@@ -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
|
||||
|
||||
+228
-54
@@ -21,9 +21,14 @@
|
||||
#include <QApplication>
|
||||
#include <QCheckBox>
|
||||
#include <QCloseEvent>
|
||||
#include <QComboBox>
|
||||
#include <QDebug>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QFileInfo>
|
||||
#include <QFont>
|
||||
#include <QFormLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMediaPlayer>
|
||||
@@ -31,10 +36,11 @@
|
||||
#include <QPalette>
|
||||
#include <QPixmap>
|
||||
#include <QProcess>
|
||||
#include <QPushButton>
|
||||
#include <QSpinBox>
|
||||
#include <QStackedWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QVideoWidget>
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
// V4L2 includes
|
||||
#include <cstring>
|
||||
@@ -44,12 +50,91 @@
|
||||
#include <sys/ioctl.h>
|
||||
#include <unistd.h>
|
||||
#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 <uxplay/renderers/video_renderer.h>
|
||||
#include <uxplay/uxplay.h>
|
||||
|
||||
#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<QLabel *>();
|
||||
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
|
||||
#endif
|
||||
|
||||
+60
-26
@@ -23,10 +23,16 @@
|
||||
#include "qprocessindicator.h"
|
||||
#include <QCheckBox>
|
||||
#include <QCloseEvent>
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QMainWindow>
|
||||
#include <QMediaPlayer>
|
||||
#include <QMutex>
|
||||
#include <QPushButton>
|
||||
#include <QStackedWidget>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
@@ -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<QByteArray> m_argData;
|
||||
QVector<char *> 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
|
||||
|
||||
+4
-8
@@ -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<std::recursive_mutex> lock(*device->mutex);
|
||||
std::lock_guard<std::recursive_mutex> 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<std::recursive_mutex> lock(*deviceInfo->mutex);
|
||||
delete deviceInfo->mutex;
|
||||
std::lock_guard<std::recursive_mutex> 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
|
||||
|
||||
+2
-8
@@ -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);
|
||||
|
||||
@@ -21,10 +21,12 @@
|
||||
#define APPSWIDGET_H
|
||||
|
||||
#include "appstoremanager.h"
|
||||
#include "iDescriptor-ui.h"
|
||||
#include "qprocessindicator.h"
|
||||
#include <QAction>
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
#include <QEvent>
|
||||
#include <QFile>
|
||||
#include <QGridLayout>
|
||||
#include <QHBoxLayout>
|
||||
@@ -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
|
||||
|
||||
@@ -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 *)
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#ifndef BATTERYWIDGET_H
|
||||
#define BATTERYWIDGET_H
|
||||
|
||||
#include <QEvent>
|
||||
#include <QWidget>
|
||||
|
||||
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;
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
#include <libimobiledevice/lockdown.h>
|
||||
#include <string.h>
|
||||
|
||||
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];
|
||||
|
||||
@@ -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());
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
+7
-13
@@ -18,34 +18,29 @@
|
||||
*/
|
||||
|
||||
#include "diagnosedialog.h"
|
||||
#include "iDescriptor-ui.h"
|
||||
#include <QApplication>
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
+203
-7
@@ -20,16 +20,23 @@
|
||||
#include "diagnosewidget.h"
|
||||
#ifdef WIN32
|
||||
#include "platform/windows/check_deps.h"
|
||||
#include <archive.h>
|
||||
#include <archive_entry.h>
|
||||
#endif
|
||||
#include <QApplication>
|
||||
#include <QCoreApplication>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFrame>
|
||||
#include <QMessageBox>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QStandardPaths>
|
||||
#include <QTextStream>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
+9
-13
@@ -62,7 +62,7 @@ QUuid ExportManager::startExport(iDescriptorDevice *device,
|
||||
const QString &destinationPath,
|
||||
std::optional<afc_client_t> 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
+8
-3
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "httpserver.h"
|
||||
#include "iDescriptor.h"
|
||||
#include "settingsmanager.h"
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
@@ -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()
|
||||
|
||||
+25
-9
@@ -21,6 +21,7 @@
|
||||
#include "settingsmanager.h"
|
||||
#include <QAbstractButton>
|
||||
#include <QApplication>
|
||||
#include <QEvent>
|
||||
#include <QGraphicsView>
|
||||
#include <QGuiApplication>
|
||||
#include <QLabel>
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
+3
-3
@@ -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);
|
||||
|
||||
|
||||
+17
-4
@@ -20,10 +20,13 @@
|
||||
#include "infolabel.h"
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QFontMetrics>
|
||||
#include <QMouseEvent>
|
||||
|
||||
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;
|
||||
}
|
||||
+3
-1
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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<void *>(afcClient));
|
||||
result["houseArrestClient"] = QVariant::fromValue(
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "zlineedit.h"
|
||||
#include <QCheckBox>
|
||||
#include <QEnterEvent>
|
||||
#include <QEvent>
|
||||
#include <QFrame>
|
||||
#include <QFutureWatcher>
|
||||
#include <QGroupBox>
|
||||
@@ -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<AppTabWidget *> m_appTabs;
|
||||
QNetworkAccessManager *m_networkManager = new QNetworkAccessManager(this);
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
+7
-4
@@ -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<QString> mounted_iFusePaths = iFuseManager::getMountPoints();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "core/services/dnssd/dnssd_service.h"
|
||||
#endif
|
||||
|
||||
#include <QEvent>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QScrollArea>
|
||||
@@ -63,6 +64,18 @@ private:
|
||||
#endif
|
||||
|
||||
QList<QWidget *> 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
|
||||
+114
-28
@@ -27,21 +27,28 @@
|
||||
#include <QPainter>
|
||||
#include <QPixmap>
|
||||
#include <QRandomGenerator>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
#include <qrencode.h>
|
||||
|
||||
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<QMediaPlayer::MediaStatus>::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();
|
||||
}
|
||||
}
|
||||
|
||||
+11
-3
@@ -29,6 +29,9 @@
|
||||
#include <QPushButton>
|
||||
#include <QStringList>
|
||||
#include <QVBoxLayout>
|
||||
#include <QStackedWidget>
|
||||
#include <QVideoWidget>
|
||||
#include <QMediaPlayer>
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -23,5 +23,6 @@
|
||||
bool IsAppleMobileDeviceSupportInstalled();
|
||||
bool IsWinFspInstalled();
|
||||
bool is_iDescriptorInstalled();
|
||||
bool IsBonjourServiceInstalled();
|
||||
|
||||
#endif // CHECK_DEPS_H
|
||||
@@ -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");
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#define QPROCESSINDICATOR_H
|
||||
|
||||
#include <QColor>
|
||||
#include <QEvent>
|
||||
#include <QPaintEvent>
|
||||
#include <QPainter>
|
||||
#include <QTimer>
|
||||
@@ -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
|
||||
@@ -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<afc_client_t> altAfc)
|
||||
{
|
||||
return executeOperation<AFCFileTree>(
|
||||
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);
|
||||
}
|
||||
+11
-10
@@ -43,11 +43,11 @@ public:
|
||||
std::function<T(afc_client_t)> operation,
|
||||
std::optional<afc_client_t> altAfc = std::nullopt)
|
||||
{
|
||||
if (!device || !device->mutex) {
|
||||
if (!device) {
|
||||
return T{}; // Return default-constructed value for the type
|
||||
}
|
||||
|
||||
std::lock_guard<std::recursive_mutex> lock(*device->mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(device->mutex);
|
||||
|
||||
// Double-check device is still valid after acquiring lock
|
||||
if (!device->afcClient) {
|
||||
@@ -70,11 +70,11 @@ public:
|
||||
std::function<T()> operation,
|
||||
std::optional<afc_client_t> altAfc = std::nullopt)
|
||||
{
|
||||
if (!device || !device->mutex) {
|
||||
if (!device) {
|
||||
return T{}; // Return default-constructed value for the type
|
||||
}
|
||||
|
||||
std::lock_guard<std::recursive_mutex> lock(*device->mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(device->mutex);
|
||||
|
||||
// Double-check device is still valid after acquiring lock
|
||||
if (!device->afcClient) {
|
||||
@@ -94,11 +94,11 @@ public:
|
||||
std::function<T()> operation, T failureValue,
|
||||
std::optional<afc_client_t> altAfc = std::nullopt)
|
||||
{
|
||||
if (!device || !device->mutex) {
|
||||
if (!device) {
|
||||
return failureValue;
|
||||
}
|
||||
|
||||
std::lock_guard<std::recursive_mutex> lock(*device->mutex);
|
||||
std::lock_guard<std::recursive_mutex> 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<void()> operation,
|
||||
std::optional<afc_client_t> altAfc = std::nullopt)
|
||||
{
|
||||
if (!device || !device->mutex) {
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::recursive_mutex> lock(*device->mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(device->mutex);
|
||||
|
||||
// Double-check device is still valid after acquiring lock
|
||||
if (!device->afcClient) {
|
||||
@@ -144,11 +144,11 @@ public:
|
||||
std::optional<afc_client_t> altAfc = std::nullopt)
|
||||
{
|
||||
try {
|
||||
if (!device || !device->mutex) {
|
||||
if (!device) {
|
||||
return AFC_E_UNKNOWN_ERROR;
|
||||
}
|
||||
|
||||
std::lock_guard<std::recursive_mutex> lock(*device->mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(device->mutex);
|
||||
|
||||
// Double-check device is still valid after acquiring lock
|
||||
if (!device->afcClient) {
|
||||
@@ -215,6 +215,7 @@ public:
|
||||
std::optional<afc_client_t> altAfc = std::nullopt);
|
||||
static AFCFileTree
|
||||
safeGetFileTree(iDescriptorDevice *device, const std::string &path = "/",
|
||||
bool checkDir = true,
|
||||
std::optional<afc_client_t> altAfc = std::nullopt);
|
||||
};
|
||||
|
||||
|
||||
+53
-1
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -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();
|
||||
|
||||
+82
-7
@@ -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<int>::of(&QSpinBox::valueChanged),
|
||||
this, &SettingsWidget::onSettingChanged);
|
||||
connect(m_wirelessFileServerPort,
|
||||
QOverload<int>::of(&QSpinBox::valueChanged), this,
|
||||
&SettingsWidget::onSettingChanged);
|
||||
|
||||
connect(m_iconSizeBaseMultiplier,
|
||||
QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
|
||||
@@ -301,6 +355,14 @@ void SettingsWidget::connectSignals()
|
||||
|
||||
connect(m_defaultJailbrokenRootPassword, &QLineEdit::textChanged, this,
|
||||
&SettingsWidget::onSettingChanged);
|
||||
connect(m_fpsComboBox, QOverload<int>::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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 "
|
||||
|
||||
+63
-27
@@ -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();
|
||||
}
|
||||
+3
-3
@@ -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<QWidget *> m_toolboxes;
|
||||
QList<bool> m_requiresDevice;
|
||||
iDescriptorDevice *m_currentDevice;
|
||||
std::string m_uuid;
|
||||
DevDiskImagesWidget *m_devDiskImagesWidget = nullptr;
|
||||
NetworkDevicesWidget *m_networkDevicesWidget = nullptr;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -29,25 +29,13 @@
|
||||
#include <QTimer>
|
||||
|
||||
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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
+1
-8
@@ -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()
|
||||
{
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <QApplication>
|
||||
#include <QEvent>
|
||||
#include <QLineEdit>
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
@@ -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 <YYYY-MM-DD>"
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user