Merge branch 'iDescriptor:main' into main

This commit is contained in:
Art P.
2026-02-07 05:24:24 +04:00
committed by GitHub
67 changed files with 1364 additions and 477 deletions
+1 -1
View File
@@ -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"
+2 -2
View File
@@ -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: |
+63 -29
View File
@@ -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
View File
@@ -10,4 +10,7 @@ build-dir
*.so
*.exe
*.dll
devdiskimgs
devdiskimgs
.qt
.qtcreator
CMakeFiles
+3 -3
View File
@@ -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
View File
@@ -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)
+39 -10
View File
@@ -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 (WiFi) | ⚠️ 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
View File
@@ -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()
Submodule lib/airplay deleted from 9fe5788667
Submodule
+1
Submodule lib/uxplay added at 758fec4be6
+1 -1
View File
@@ -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.
+18
View File
@@ -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
View File
@@ -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}"
+5 -4
View File
@@ -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);
+10
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+15
View File
@@ -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
+7 -1
View File
@@ -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 *)
+4
View File
@@ -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;
+7 -4
View File
@@ -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];
-5
View File
@@ -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());
// }
}
}
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+5
View File
@@ -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
View File
@@ -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;
}
+3 -3
View File
@@ -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();
}
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
};
+4 -19
View File
@@ -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(
+8 -1
View File
@@ -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);
};
-3
View File
@@ -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
View File
@@ -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();
-5
View File
@@ -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();
+13
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+21
View File
@@ -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;
}
+1
View File
@@ -23,5 +23,6 @@
bool IsAppleMobileDeviceSupportInstalled();
bool IsWinFspInstalled();
bool is_iDescriptorInstalled();
bool IsBonjourServiceInstalled();
#endif // CHECK_DEPS_H
+1 -3
View File
@@ -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");
-2
View File
@@ -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()
{
+10
View File
@@ -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
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
+14
View File
@@ -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
View File
@@ -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);
}
+9
View File
@@ -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;
+8 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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;
-4
View File
@@ -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
+12 -96
View File
@@ -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()
-12
View File
@@ -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
View File
@@ -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()
{
+10
View File
@@ -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);
}
};
+41
View File
@@ -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
}