v0.2.0
This commit is contained in:
uncor3
2026-01-31 18:03:20 -05:00
committed by GitHub
66 changed files with 1324 additions and 465 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
+19 -7
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)
@@ -216,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)
@@ -282,7 +289,7 @@ target_link_libraries(iDescriptor PRIVATE
PkgConfig::AVCODEC
PkgConfig::AVUTIL
PkgConfig::SWSCALE
airplay
uxplay
ipatool-go
ZUpdater
)
@@ -293,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)
@@ -302,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)
+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
}