detect recovery devices, cleanup code , use const pointers wherever possible, refactor img thumbnail loading, refactor gallery widget

This commit is contained in:
uncor3
2026-03-02 23:25:06 +00:00
parent df0f0f32a7
commit 35c5985f47
64 changed files with 1490 additions and 1260 deletions
+44 -5
View File
@@ -1,12 +1,13 @@
cmake_minimum_required(VERSION 3.16)
project(iDescriptor VERSION 0.3.0 LANGUAGES CXX)
project(iDescriptor VERSION 0.3.0 LANGUAGES C CXX)
if(WIN32)
set(PKG_CONFIG_EXECUTABLE "C:/msys64/mingw64/bin/pkg-config.exe" CACHE FILEPATH "" FORCE)
endif()
# Feature options
set(PACKAGE_MANAGER_HINT "" CACHE STRING "Name of package manager(s) used to manage this build (e.g. paru, yay, pamac)")
option(ENABLE_RECOVERY_DEVICE_SUPPORT "Enable recovery device support (requires libirecovery)" ON)
set(PACKAGE_MANAGER_HINT "" CACHE STRING "Name of package manager(s) used to manage future updates (e.g. paru, yay, pamac), only used if PACKAGE_MANAGER_MANAGED is ON)")
option(PACKAGE_MANAGER_MANAGED "Build as package manager managed version (auto updates will be handled by the package manager)" OFF)
option(DEPLOY "Deploy the application (WIN32 only)" ON)
@@ -40,7 +41,7 @@ if(WIN32)
set(ENV{PKG_CONFIG_PATH} "${CUSTOM_PKGCONFIG_PATH};$ENV{PKG_CONFIG_PATH}")
elseif(APPLE)
set(CUSTOM_LIB_PATH "/usr/local/lib")
# Remove the problematic include path that's causing conflicts
# TODO: do we need this on macOS?
# set(CUSTOM_INCLUDE_PATH "/usr/local/include")
set(CUSTOM_PKGCONFIG_PATH "/usr/local/lib/pkgconfig")
set(ENV{PKG_CONFIG_PATH} "${CUSTOM_PKGCONFIG_PATH}:$ENV{PKG_CONFIG_PATH}")
@@ -92,7 +93,6 @@ set(IDEVICE_RS_LIB_PATH ${IDEVICE_RS_SOURCE_DIR}/target/debug/libidevice_ffi.a)
if(APPLE)
add_custom_command(
OUTPUT ${IDEVICE_RS_LIB_PATH}
# Ensure rustc is visible in the build environment
COMMAND ${CMAKE_COMMAND} -E env
"PATH=$ENV{HOME}/.cargo/bin:/usr/local/bin:$ENV{PATH}"
"RUSTC=/usr/local/bin/rustc"
@@ -101,6 +101,7 @@ add_custom_command(
COMMENT "Building idevice-rs FFI libraryy"
VERBATIM
)
# We must use +stable-x86_64-pc-windows-gnu on Windows
elseif(WIN32)
add_custom_command(
OUTPUT ${IDEVICE_RS_LIB_PATH}
@@ -109,6 +110,7 @@ add_custom_command(
COMMENT "Building idevice-rs FFI libraryy"
VERBATIM
)
# Linux
else()
add_custom_command(
OUTPUT ${IDEVICE_RS_LIB_PATH}
@@ -166,7 +168,7 @@ find_package(Threads REQUIRED)
target_link_libraries(idevice_cpp PUBLIC idevice_ffi Threads::Threads)
if (UNIX AND NOT APPLE)
pkg_check_modules(UDEV REQUIRED IMPORTED_TARGET udev)
pkg_check_modules(UDEV REQUIRED IMPORTED_TARGET libudev)
target_link_libraries(idevice_cpp PUBLIC PkgConfig::UDEV dl m)
elseif(APPLE)
find_library(COREFOUNDATION_FRAMEWORK CoreFoundation REQUIRED)
@@ -218,6 +220,20 @@ endif()
pkg_check_modules(PUGIXML REQUIRED IMPORTED_TARGET pugixml)
if(ENABLE_RECOVERY_DEVICE_SUPPORT)
find_library(IRECOVERY_LIBRARY
NAMES irecovery-1.0
${CUSTOM_FIND_LIB_ARGS}
)
if(IRECOVERY_LIBRARY)
message(STATUS "Building with recovery device support enabled")
else()
message(WARNING "libirecovery not found. Recovery device support will be disabled. This is to be expected if you are installing from Arch AUR.")
set(ENABLE_RECOVERY_DEVICE_SUPPORT OFF)
endif()
else()
message(STATUS "Recovery device support disabled")
endif()
file(GLOB PROJECT_SOURCES
src/*.h
@@ -229,6 +245,15 @@ src/base/*.h
resources.qrc
)
if (NOT ENABLE_RECOVERY_DEVICE_SUPPORT)
list(REMOVE_ITEM PROJECT_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/src/recoverydeviceinfowidget.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/recoverydeviceinfowidget.h
src/recoverydeviceinfowidget.cpp
src/recoverydeviceinfowidget.h
)
endif()
if(APPLE)
list(APPEND PROJECT_SOURCES
src/platform/macos/macos.mm
@@ -324,6 +349,11 @@ target_link_libraries(iDescriptor PRIVATE
SQLite::SQLite3
)
if(ENABLE_RECOVERY_DEVICE_SUPPORT)
target_link_libraries(iDescriptor PRIVATE ${IRECOVERY_LIBRARY})
target_compile_definitions(iDescriptor PRIVATE ENABLE_RECOVERY_DEVICE_SUPPORT)
endif()
target_include_directories(iDescriptor PRIVATE
# Put idevice-rs includes FIRST
${IDEVICE_CPP_INCLUDE_DIR}
@@ -401,6 +431,15 @@ set_target_properties(iDescriptor PROPERTIES
)
if (UNIX AND NOT APPLE)
# Required on Linux to find libirecovery-1.0.so.5 at runtime
if (ENABLE_RECOVERY_DEVICE_SUPPORT)
set_target_properties(iDescriptor PROPERTIES
# Control library search order - system libs first, then /usr/local/lib
INSTALL_RPATH "/usr/lib/x86_64-linux-gnu:/usr/lib:/usr/local/lib:$ORIGIN"
)
endif()
# Add install rules for the project
# include(GNUInstallDirs)
+27 -72
View File
@@ -10,6 +10,10 @@
"add-ld-path": "."
}
},
"sdk-extensions": [
"org.freedesktop.Sdk.Extension.rust-stable",
"org.freedesktop.Sdk.Extension.llvm20"
],
"cleanup-commands": ["mkdir -p ${FLATPAK_DEST}/lib/ffmpeg"],
"command": "iDescriptor",
"finish-args": [
@@ -19,20 +23,8 @@
"--share=network",
"--device=all",
"--socket=system-bus",
"--filesystem=/run/usbmuxd:rw",
"--filesystem=/run/udev:ro",
"--filesystem=/dev:ro",
"--filesystem=/sys/bus:ro",
"--filesystem=/sys/class:ro",
"--filesystem=/sys/devices:ro",
"--filesystem=/run/usbmuxd",
"--system-talk-name=org.freedesktop.Avahi",
"--talk-name=org.libimobiledevice.usbmuxd",
"--system-talk-name=org.libimobiledevice.usbmuxd",
"--system-talk-name=org.freedesktop.UDisks2",
"--system-talk-name=org.freedesktop.login1",
"--system-talk-name=org.freedesktop.NetworkManager",
"--system-talk-name=org.freedesktop.UPower",
"--system-talk-name=org.freedesktop.systemd1",
"--talk-name=org.freedesktop.Platform.ffmpeg-full",
"--env=QT_MEDIA_BACKEND=ffmpeg",
"--env=AVAHI_COMPAT_NOWARN=1"
@@ -90,7 +82,6 @@
{
"name": "libplist",
"buildsystem": "autotools",
"config-opts": ["--without-cython"],
"sources": [
{
"type": "git",
@@ -98,26 +89,6 @@
}
]
},
{
"name": "libimobiledevice-glue",
"buildsystem": "autotools",
"sources": [
{
"type": "git",
"url": "https://github.com/libimobiledevice/libimobiledevice-glue.git"
}
]
},
{
"name": "libtatsu",
"buildsystem": "autotools",
"sources": [
{
"type": "git",
"url": "https://github.com/libimobiledevice/libtatsu.git"
}
]
},
{
"name": "libusb",
"config-opts": ["--disable-static", "--enable-udev"],
@@ -133,26 +104,6 @@
"install -Dm644 COPYING ${FLATPAK_DEST}/share/licenses/libusb/COPYING"
]
},
{
"name": "libusbmuxd",
"buildsystem": "autotools",
"sources": [
{
"type": "git",
"url": "https://github.com/libimobiledevice/libusbmuxd.git"
}
]
},
{
"name": "libimobiledevice",
"buildsystem": "autotools",
"sources": [
{
"type": "git",
"url": "https://github.com/libimobiledevice/libimobiledevice.git"
}
]
},
{
"name": "libdaemon",
"buildsystem": "autotools",
@@ -204,16 +155,6 @@
}
]
},
{
"name": "libirecovery",
"buildsystem": "autotools",
"sources": [
{
"type": "git",
"url": "https://github.com/libimobiledevice/libirecovery.git"
}
]
},
{
"name": "libde265",
"buildsystem": "autotools",
@@ -271,22 +212,36 @@
},
{
"name": "iDescriptor",
"buildsystem": "cmake-ninja",
"buildsystem": "simple",
"build-options": {
"append-path": "/usr/lib/sdk/rust-stable/bin",
"env": {
"CARGO_HOME": "/run/build/iDescriptor/cargo",
"CARGO_NET_OFFLINE": "true",
"RUST_BACKTRACE": "1",
"CUSTOM_LIB_PATH": "/app/lib",
"CUSTOM_INCLUDE_PATH": "/app/include",
"CUSTOM_PKGCONFIG_PATH": "/app/lib/pkgconfig",
"GO_EXECUTABLE": "/app/sdk/golang/bin/go",
"FLATPAK_BUILD": "ON"
},
"build-args": ["--share=network"]
},
"config-opts": [
"-DCUSTOM_LIB_PATH=/app/lib",
"-DCUSTOM_INCLUDE_PATH=/app/include",
"-DCUSTOM_PKGCONFIG_PATH=/app/lib/pkgconfig",
"-DGO_EXECUTABLE=/app/sdk/golang/bin/go",
"-DFLATPAK_BUILD=ON"
"build-commands": [
"cargo --offline fetch --manifest-path lib/idevice-rs/Cargo.toml --verbose",
"cargo build --offline --release --all-features --manifest-path lib/idevice-rs/Cargo.toml",
"cmake -B build -DGO_EXECUTABLE=$GO_EXECUTABLE .",
"cmake --build build --config Release",
"install -Dm0755 build/iDescriptor ${FLATPAK_DEST}/bin/iDescriptor"
],
"sources": [
{
"type": "dir",
"path": "."
}
},
"cargo-sources.idevice-rs.json",
"cargo-sources.idevice-rs.idevice.json",
"cargo-sources.idevice-rs.cpp.plist_ffi.json"
]
}
]
+1
View File
@@ -37,6 +37,7 @@
<file>resources/icons/MaterialSymbolsFolder.png</file>
<file>resources/icons/QlementineIconsWireless116.png</file>
<file>resources/icons/UimProcess.png</file>
<file>resources/icons/LetsIconsHorizontalDownLeftMainLight.png</file>
<file>qml/MapView.qml</file>
<file>resources/iphone.png</file>
<file>resources/ios-wallpapers/iphone-ios4.png</file>
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

+2 -1
View File
@@ -44,7 +44,8 @@
#include <QTreeWidget>
#include <QVariant>
AfcExplorerWidget::AfcExplorerWidget(iDescriptorDevice *device, bool favEnabled,
AfcExplorerWidget::AfcExplorerWidget(const iDescriptorDevice *device,
bool favEnabled,
AfcClientHandle *afcClient, QString root,
QWidget *parent)
: QWidget(parent), m_device(device), m_favEnabled(favEnabled),
+2 -2
View File
@@ -46,7 +46,7 @@ class AfcExplorerWidget : public QWidget
{
Q_OBJECT
public:
explicit AfcExplorerWidget(iDescriptorDevice *device = nullptr,
explicit AfcExplorerWidget(const iDescriptorDevice *device = nullptr,
bool favEnabled = false,
AfcClientHandle *afcClient = nullptr,
QString root = "/", QWidget *parent = nullptr);
@@ -87,7 +87,7 @@ private:
ZIconWidget *m_homeButton;
ZIconWidget *m_upButton;
ZIconWidget *m_enterButton;
iDescriptorDevice *m_device;
const iDescriptorDevice *m_device;
bool m_favEnabled;
AfcClientHandle *m_afc;
QString m_errorMessage;
+87 -62
View File
@@ -25,6 +25,7 @@
#include "networkdevicemanager.h"
#include <QDebug>
#include <QMessageBox>
#include <QThreadPool>
#include <QTimer>
#include <QUuid>
#include <thread>
@@ -98,11 +99,13 @@ void AppContext::cachePairedDevices()
auto conn = UsbmuxdConnection::default_new(0);
if (conn.is_err()) {
qDebug() << "ERROR: Failed to connect to usbmuxd!";
return;
}
auto devices = conn.unwrap().get_devices();
if (devices.is_err()) {
qDebug() << "ERROR: Failed to get device list!";
return;
}
for (const auto &device : devices.unwrap()) {
@@ -190,6 +193,12 @@ void AppContext::addDevice(iDescriptor::Uniq uniq,
{
emit initStarted(uniq);
if (auto device = getDevice(uniq)) {
emit deviceAlreadyExists(uniq);
return;
}
try {
auto initResult = std::make_shared<iDescriptorInitDeviceResult>();
@@ -237,7 +246,7 @@ void AppContext::addDevice(iDescriptor::Uniq uniq,
watcher->setFuture(future);
connect(
watcher, &QFutureWatcher<void>::finished, this,
[this, uniq, initResult, addType, conn_type, watcher]() {
[this, uniq, initResult, addType, conn_type, watcher]() mutable {
watcher->deleteLater();
qDebug() << "init_idescriptor_device success ?: "
<< initResult->success;
@@ -245,6 +254,9 @@ void AppContext::addDevice(iDescriptor::Uniq uniq,
if (!initResult->success) {
qDebug() << "Failed to initialize device with" << uniq;
emit initFailed(uniq);
// TODO:it could also be password protected, so check for
// that Initialization failed, cleaning up resources.
// PasswordProtected
if (initResult->error && initResult->error->code ==
PairingDialogResponsePending) {
if (addType == AddType::Regular) {
@@ -266,7 +278,8 @@ void AppContext::addDevice(iDescriptor::Uniq uniq,
}
});
// FIXME: free properly and move to a better place
QtConcurrent::run([uniq, this]() {
QThreadPool::globalInstance()->start([uniq,
this]() {
UsbmuxdConnectionHandle *usbmuxd_conn = nullptr;
UsbmuxdAddrHandle *addr_handle = nullptr;
IdeviceProviderHandle *provider = nullptr;
@@ -433,6 +446,16 @@ void AppContext::addDevice(iDescriptor::Uniq uniq,
}
qDebug() << "Device initialized: " << uniq;
/*
We need this because wireless devices get initialized
with Mac addresses. Even though a Mac address is unique, we
are better off using the "UDID" as the unique identifier.
This is only required for wireless devices, Usb devices
already use the UDID but it doesn't hurt to set it for them
as well
*/
uniq.set(initResult->deviceInfo.UniqueDeviceID, false);
iDescriptorDevice *device = new iDescriptorDevice{
.udid = uniq.get().toStdString(),
.conn_type = conn_type,
@@ -444,8 +467,10 @@ void AppContext::addDevice(iDescriptor::Uniq uniq,
.diagRelay = initResult->diagRelay,
.heartbeatThread = initResult->heartbeatThread};
m_devices[device->udid] = device;
if (addType == AddType::Regular) {
qDebug() << "Regular device added: " << uniq;
if (addType == AddType::Wireless ||
addType == AddType::UpgradeToWireless ||
addType == AddType::Regular) {
qDebug() << "Wireless device added: " << uniq;
// SettingsManager::sharedInstance()->doIfEnabled(
// SettingsManager::Setting::AutoRaiseWindow, []() {
// if (MainWindow *mainWindow =
@@ -477,12 +502,6 @@ int AppContext::getConnectedDeviceCount() const
// #endif
}
/*
FIXME:
on macOS, sometimes you get wireless disconnects even though we are not
listening for wireless devices it does not have any to do with us, but
it still happens so be aware of that
*/
void AppContext::removeDevice(QString _udid)
{
const std::string udid = _udid.toStdString();
@@ -513,7 +532,11 @@ void AppContext::removeDevice(QString _udid)
device->deviceInfo.isWireless);
emit deviceChange();
qDebug() << "Waiting to acquire lock for device cleanup: "
<< QString::fromStdString(udid);
std::lock_guard<std::recursive_mutex> lock(device->mutex);
qDebug() << "Acquired lock, cleaning up device: "
<< QString::fromStdString(udid);
// FIXME: implement proper cleanup
if (device->afcClient)
@@ -524,7 +547,7 @@ void AppContext::removeDevice(QString _udid)
if (device->heartbeatThread) {
device->heartbeatThread->requestInterruption();
device->heartbeatThread->wait();
// device->heartbeatThread->wait();
delete device->heartbeatThread;
}
@@ -534,30 +557,29 @@ void AppContext::removeDevice(QString _udid)
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
void AppContext::removeRecoveryDevice(uint64_t ecid)
{
// if (!m_recoveryDevices.contains(ecid)) {
// qDebug() << "Device with ECID " + QString::number(ecid) +
// " not found. Please report this issue.";
// return;
// }
if (!m_recoveryDevices.contains(ecid)) {
qDebug() << "Device with ECID " + QString::number(ecid) +
" not found. Please report this issue.";
return;
}
// qDebug() << "Removing recovery device with ECID:" << ecid;
qDebug() << "Removing recovery device with ECID:" << ecid;
// // Fix use-after-free: get pointer before removing from map
// iDescriptorRecoveryDevice *deviceInfo =
// m_recoveryDevices.value(ecid); m_recoveryDevices.remove(ecid);
iDescriptorRecoveryDevice *deviceInfo = m_recoveryDevices.value(ecid);
m_recoveryDevices.remove(ecid);
// emit recoveryDeviceRemoved(ecid);
emit recoveryDeviceRemoved(ecid);
// TODO: do we need this ?
// emit deviceChange();
// std::lock_guard<std::recursive_mutex> lock(*deviceInfo->mutex);
// delete deviceInfo->mutex;
// delete deviceInfo;
std::lock_guard<std::recursive_mutex> lock(deviceInfo->mutex);
delete deviceInfo;
}
#endif
iDescriptorDevice *AppContext::getDevice(const std::string &udid)
iDescriptorDevice *AppContext::getDevice(const std::string &uniq)
{
return m_devices.value(udid, nullptr);
return m_devices.value(uniq, nullptr);
}
QList<iDescriptorDevice *> AppContext::getAllDevices()
@@ -565,12 +587,12 @@ QList<iDescriptorDevice *> AppContext::getAllDevices()
return m_devices.values();
}
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
// QList<iDescriptorRecoveryDevice *> AppContext::getAllRecoveryDevices()
// {
// // return m_recoveryDevices.values();
// }
// #endif
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
QList<iDescriptorRecoveryDevice *> AppContext::getAllRecoveryDevices()
{
return m_recoveryDevices.values();
}
#endif
// Returns whether there are any devices connected (regular or recovery)
bool AppContext::noDevicesConnected() const
@@ -586,27 +608,35 @@ bool AppContext::noDevicesConnected() const
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
void AppContext::addRecoveryDevice(uint64_t ecid)
{
// iDescriptorInitDeviceResultRecovery res =
// init_idescriptor_recovery_device(ecid);
auto res = std::make_shared<iDescriptorInitDeviceResultRecovery>();
// if (!res.success) {
// qDebug() << "Failed to initialize recovery device with ECID: "
// << QString::number(ecid);
// qDebug() << "Error code: " << res.error;
// return;
// }
QFuture<void> future = QtConcurrent::run(
[this, ecid, res]() { init_idescriptor_recovery_device(ecid, *res); });
QFutureWatcher<void> *watcher = new QFutureWatcher<void>();
watcher->setFuture(future);
connect(watcher, &QFutureWatcher<void>::finished, this,
[this, ecid, res, watcher]() {
watcher->deleteLater();
if (!res->success) {
qDebug()
<< "Failed to initialize recovery device with ECID: "
<< QString::number(ecid);
qDebug() << "Error code: " << res->error;
return;
}
// iDescriptorRecoveryDevice *recoveryDevice = new
// iDescriptorRecoveryDevice(); recoveryDevice->ecid =
// res.deviceInfo.ecid; recoveryDevice->mode = res.mode;
// recoveryDevice->cpid = res.deviceInfo.cpid;
// recoveryDevice->bdid = res.deviceInfo.bdid;
// recoveryDevice->displayName = res.displayName;
// recoveryDevice->mutex = new std::recursive_mutex();
iDescriptorRecoveryDevice *recoveryDevice =
new iDescriptorRecoveryDevice();
recoveryDevice->ecid = res->deviceInfo.ecid;
recoveryDevice->mode = res->mode;
recoveryDevice->cpid = res->deviceInfo.cpid;
recoveryDevice->bdid = res->deviceInfo.bdid;
recoveryDevice->displayName = res->displayName;
// m_recoveryDevices[res.deviceInfo.ecid] = recoveryDevice;
// emit recoveryDeviceAdded(recoveryDevice);
// emit deviceChange();
m_recoveryDevices[res->deviceInfo.ecid] = recoveryDevice;
emit recoveryDeviceAdded(recoveryDevice);
emit deviceChange();
});
}
#endif
@@ -631,21 +661,16 @@ AppContext::~AppContext()
}
}
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
// for (auto recoveryDevice : m_recoveryDevices) {
// emit recoveryDeviceRemoved(recoveryDevice->ecid);
// delete recoveryDevice->mutex;
// delete recoveryDevice;
// }
// #endif
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
for (auto recoveryDevice : m_recoveryDevices) {
emit recoveryDeviceRemoved(recoveryDevice->ecid);
delete recoveryDevice;
}
#endif
}
void AppContext::setCurrentDeviceSelection(const DeviceSelection &selection)
{
qDebug() << "New selection -"
<< " Type:" << selection.type
<< " UDID:" << QString::fromStdString(selection.udid)
<< " ECID:" << selection.ecid << " Section:" << selection.section;
if (m_currentSelection.type == selection.type &&
m_currentSelection.udid == selection.udid &&
m_currentSelection.ecid == selection.ecid &&
@@ -712,4 +737,4 @@ void AppContext::tryToConnectToNetworkDevice(const NetworkDevice &device)
void AppContext::emitNoPairingFileForWirelessDevice(const QString &udid)
{
emit noPairingFileForWirelessDevice(udid);
}
}
+13 -13
View File
@@ -34,14 +34,13 @@ public:
QList<iDescriptorDevice *> getAllDevices();
explicit AppContext(QObject *parent = nullptr);
bool noDevicesConnected() const;
// QMap<WiFiMACAddress, PairingFilePath>
void cachePairingFile(const QString &udid, const QString &pairingFilePath);
const QString getCachedPairingFile(const QString &udid) const;
void tryToConnectToNetworkDevice(const NetworkDevice &device);
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
// QList<iDescriptorRecoveryDevice *> getAllRecoveryDevices();
// #endif
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
QList<iDescriptorRecoveryDevice *> getAllRecoveryDevices();
#endif
~AppContext();
int getConnectedDeviceCount() const;
@@ -52,24 +51,25 @@ public:
private:
QMap<std::string, iDescriptorDevice *> m_devices;
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
// QMap<uint64_t, iDescriptorRecoveryDevice *> m_recoveryDevices;
// #endif
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
QMap<uint64_t, iDescriptorRecoveryDevice *> m_recoveryDevices;
#endif
QStringList m_pendingDevices;
DeviceSelection m_currentSelection = DeviceSelection("");
QMap<QString, QString> m_pairingFileCache;
void cachePairedDevices();
void emitNoPairingFileForWirelessDevice(const QString &udid);
signals:
void deviceAdded(iDescriptorDevice *device);
void deviceAdded(const iDescriptorDevice *device);
void deviceRemoved(const std::string &udid, const std::string &macAddress,
const std::string &ipAddress, bool wasWireless);
void devicePaired(iDescriptorDevice *device);
void devicePaired(const iDescriptorDevice *device);
void devicePasswordProtected(const QString &udid);
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
// void recoveryDeviceAdded(const iDescriptorRecoveryDevice
// *deviceInfo); void recoveryDeviceRemoved(uint64_t ecid);
// #endif
void deviceAlreadyExists(const iDescriptor::Uniq &uniq);
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
void recoveryDeviceAdded(const iDescriptorRecoveryDevice *deviceInfo);
void recoveryDeviceRemoved(uint64_t ecid);
#endif
void devicePairPending(const QString &udid);
void devicePairingExpired(const QString &udid);
// only fired on wireless devices when we have no pairing file for them
-39
View File
@@ -1,39 +0,0 @@
/*
* iDescriptor: A free and open-source idevice management tool.
*
* Copyright (C) 2025 Uncore <https://github.com/uncor3>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "libirecovery.h"
#include <string>
std::string parse_recovery_mode(irecv_mode productType)
{
switch (productType) {
case irecv_mode::IRECV_K_RECOVERY_MODE_1:
case irecv_mode::IRECV_K_RECOVERY_MODE_2:
case irecv_mode::IRECV_K_RECOVERY_MODE_3:
case irecv_mode::IRECV_K_RECOVERY_MODE_4:
return "Recovery Mode";
case irecv_mode::IRECV_K_WTF_MODE:
return "WTF Mode";
case irecv_mode::IRECV_K_DFU_MODE:
case irecv_mode::IRECV_K_PORT_DFU_MODE:
return "DFU Mode";
default:
return "Unknown Mode";
}
}
@@ -47,10 +47,7 @@ QByteArray read_afc_file_to_byte_array(const iDescriptorDevice *device,
ServiceManager::safeAfcFileClose(device, handle); // Close handle
return QByteArray();
}
// Note: afc_file_info_free will be called later if the function returns
// successfully or when returning early after the file size check.
qDebug() << "File size of" << path << "is" << info.size;
size_t fileSize = info.size;
if (fileSize == 0) {
ServiceManager::safeAfcFileClose(device, handle);
@@ -75,10 +72,8 @@ QByteArray read_afc_file_to_byte_array(const iDescriptorDevice *device,
return QByteArray();
}
// Only append and free `chunkData` if `afc_file_read` was successful
buffer.append(reinterpret_cast<const char *>(chunkData), bytesRead);
afc_file_read_data_free(chunkData,
bytesRead); // Free memory owned by Rust FFI
afc_file_read_data_free(chunkData, bytesRead);
ServiceManager::safeAfcFileClose(device, handle);
+55 -56
View File
@@ -21,9 +21,6 @@
#include "../../iDescriptor.h"
// #include "../../servicemanager.h"
#include "../../appcontext.h"
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
#include "libirecovery.h"
#endif
#include "../../heartbeat.h"
#include <QDebug>
@@ -206,6 +203,7 @@ DeviceInfo fullDeviceInfo(const pugi::xml_document &doc,
d.firmwareVersion = safeGet("FirmwareVersion");
d.productVersion = safeGet("ProductVersion");
d.wifiMacAddress = safeGet("WiFiAddress");
d.UniqueDeviceID = safeGet("UniqueDeviceID");
QString q_version = QString::fromStdString(d.productVersion);
QStringList parts = q_version.split('.');
@@ -549,6 +547,9 @@ void init_idescriptor_device(const iDescriptor::Uniq &uniq,
if (val)
plist_print(val);
afc_client_set_timeout(afc_client,
5000); // Set AFC client timeout to 5 seconds
result.provider = provider;
result.success = true;
result.afcClient = afc_client;
@@ -593,64 +594,62 @@ cleanup:
}
}
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
// iDescriptorInitDeviceResultRecovery
// init_idescriptor_recovery_device(uint64_t ecid)
// {
// qDebug() << "Initializing iDescriptor recovery device with ECID: " <<
// ecid; iDescriptorInitDeviceResultRecovery result = {};
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
void init_idescriptor_recovery_device(
uint64_t ecid, iDescriptorInitDeviceResultRecovery &result)
{
qDebug() << "Initializing iDescriptor recovery device with ECID: " << ecid;
result = {};
// irecv_client_t client = nullptr;
// const irecv_device_info *deviceInfo = nullptr;
// irecv_device_t device = nullptr;
// const DeviceDatabaseInfo *info = nullptr;
irecv_client_t client = nullptr;
const irecv_device_info *deviceInfo = nullptr;
irecv_device_t device = nullptr;
const DeviceDatabaseInfo *info = nullptr;
// irecv_error_t ret = irecv_open_with_ecid_and_attempts(
// &client, ecid, RECOVERY_CLIENT_CONNECTION_TRIES);
irecv_error_t ret = irecv_open_with_ecid_and_attempts(
&client, ecid, RECOVERY_CLIENT_CONNECTION_TRIES);
// if (ret != IRECV_E_SUCCESS) {
// qDebug() << "Failed to open recovery client with ECID:" << ecid
// << "Error:" << ret;
// result.error = ret;
// goto cleanup;
// }
if (ret != IRECV_E_SUCCESS) {
qDebug() << "Failed to open recovery client with ECID:" << ecid
<< "Error:" << ret;
result.error = ret;
goto cleanup;
}
// ret = irecv_get_mode(client, (int *)&result.mode);
// if (ret != IRECV_E_SUCCESS) {
// qDebug() << "Failed to get recovery mode. Error:" << ret;
// result.error = ret;
// goto cleanup;
// }
ret = irecv_get_mode(client, (int *)&result.mode);
if (ret != IRECV_E_SUCCESS) {
qDebug() << "Failed to get recovery mode. Error:" << ret;
result.error = ret;
goto cleanup;
}
// deviceInfo = irecv_get_device_info(client);
// if (!deviceInfo) {
// qDebug() << "Failed to get device info from recovery client";
// result.error = IRECV_E_UNKNOWN_ERROR;
// goto cleanup;
// }
deviceInfo = irecv_get_device_info(client);
if (!deviceInfo) {
qDebug() << "Failed to get device info from recovery client";
result.error = IRECV_E_UNKNOWN_ERROR;
goto cleanup;
}
// if (irecv_devices_get_device_by_client(client, &device) ==
// IRECV_E_SUCCESS &&
// device && device->hardware_model) {
// qDebug() << "Recovery device hardware_model: "
// << device->hardware_model;
// info =
// DeviceDatabase::findByHwModel(std::string(device->hardware_model));
// } else {
// qDebug() << "Could not resolve hardware_model from client.";
// }
if (irecv_devices_get_device_by_client(client, &device) ==
IRECV_E_SUCCESS &&
device && device->hardware_model) {
qDebug() << "Recovery device hardware_model: "
<< device->hardware_model;
info =
DeviceDatabase::findByHwModel(std::string(device->hardware_model));
} else {
qDebug() << "Could not resolve hardware_model from client.";
}
// result.displayName =
// info ? (info->displayName ? info->displayName : info->marketingName)
// : "Unknown Device";
// result.deviceInfo = *deviceInfo;
// result.success = true;
result.displayName =
info ? (info->displayName ? info->displayName : info->marketingName)
: "Unknown Device";
result.deviceInfo = *deviceInfo;
result.success = true;
// cleanup:
// if (client) {
// irecv_close(client);
// }
// return result;
// }
// #endif // ENABLE_RECOVERY_DEVICE_SUPPORT
cleanup:
if (client) {
irecv_close(client);
}
}
#endif // ENABLE_RECOVERY_DEVICE_SUPPORT
+20 -6
View File
@@ -29,7 +29,7 @@
#include <QTimer>
#include <QVBoxLayout>
DevDiskImageHelper::DevDiskImageHelper(iDescriptorDevice *device,
DevDiskImageHelper::DevDiskImageHelper(const iDescriptorDevice *device,
QWidget *parent)
: QDialog(parent), m_device(device)
{
@@ -119,7 +119,10 @@ void DevDiskImageHelper::start()
QString::number(deviceMajorVersion) + ".");
}
} else {
finishWithSuccess();
showStatus("Developer disk image is not available for iOS version " +
QString::number(deviceMajorVersion) +
". Please use a device with iOS 6 or above.",
true);
return;
}
}
@@ -224,7 +227,7 @@ void DevDiskImageHelper::onImageDownloadFinished(const QString &version,
paths.second.toStdString().c_str());
if (err == nullptr) {
return finishWithSuccess();
return finishWithSuccess(true);
}
qDebug() << "onImageDownloadFinished:" << err->code
@@ -265,10 +268,21 @@ void DevDiskImageHelper::showStatus(const QString &message, bool isError)
show();
}
void DevDiskImageHelper::finishWithSuccess()
/*
waiting is sometimes required because services
may not become available
as soon as the img is mounted
*/
void DevDiskImageHelper::finishWithSuccess(bool wait)
{
m_loadingIndicator->stop();
accept();
auto handler = [this]() {
m_loadingIndicator->stop();
accept();
};
if (wait) {
return QTimer::singleShot(3000, handler);
}
handler();
}
void DevDiskImageHelper::finishWithError(const QString &errorMessage)
+3 -3
View File
@@ -32,7 +32,7 @@ class DevDiskImageHelper : public QDialog
{
Q_OBJECT
public:
explicit DevDiskImageHelper(iDescriptorDevice *device,
explicit DevDiskImageHelper(const iDescriptorDevice *device,
QWidget *parent = nullptr);
// Start the mounting process
@@ -55,10 +55,10 @@ private:
void showStatus(const QString &message, bool isError = false);
void showMountUI();
void showRetryUI(const QString &errorMessage);
void finishWithSuccess();
void finishWithSuccess(bool wait = false);
void finishWithError(const QString &errorMessage);
iDescriptorDevice *m_device;
const iDescriptorDevice *m_device;
QLabel *m_statusLabel;
QProcessIndicator *m_loadingIndicator;
+3 -3
View File
@@ -322,7 +322,7 @@ bool DevDiskManager::isImageDownloaded(const QString &version,
return QFile::exists(dmgPath) && QFile::exists(sigPath);
}
bool DevDiskManager::downloadCompatibleImage(iDescriptorDevice *device,
bool DevDiskManager::downloadCompatibleImage(const iDescriptorDevice *device,
std::function<void(bool)> callback)
{
QString path = SettingsManager::sharedInstance()->mkDevDiskImgPath();
@@ -403,7 +403,7 @@ bool DevDiskManager::downloadCompatibleImage(iDescriptorDevice *device,
}
// FIXME:DOES NOT CHECK IF THERE IS ALREADY AN IMAGE MOUNTED
bool DevDiskManager::mountCompatibleImage(iDescriptorDevice *device)
bool DevDiskManager::mountCompatibleImage(const iDescriptorDevice *device)
{
QString path = SettingsManager::sharedInstance()->mkDevDiskImgPath();
@@ -493,7 +493,7 @@ bool DevDiskManager::mountCompatibleImage(iDescriptorDevice *device)
}
bool DevDiskManager::mountImage(const QString &version,
iDescriptorDevice *device)
const iDescriptorDevice *device)
{
const QString downloadPath =
SettingsManager::sharedInstance()->devdiskimgpath();
+3 -3
View File
@@ -49,7 +49,7 @@ public:
// Mount operations
bool mountImage(const QString &version, iDescriptorDevice *device);
bool mountImage(const QString &version, const iDescriptorDevice *device);
bool unmountImage();
std::pair<QString, QString> getPathsForVersion(const QString &version);
@@ -58,8 +58,8 @@ public:
const char *mounted_sig, uint64_t mounted_sig_len);
QByteArray getImageListData() const { return m_imageListJsonData; }
bool mountCompatibleImage(iDescriptorDevice *device);
bool downloadCompatibleImage(iDescriptorDevice *device,
bool mountCompatibleImage(const iDescriptorDevice *device);
bool downloadCompatibleImage(const iDescriptorDevice *device,
std::function<void(bool)> callback);
signals:
+2 -1
View File
@@ -26,7 +26,8 @@
#include <QPainterPath>
#include <QVBoxLayout>
DeviceImageWidget::DeviceImageWidget(iDescriptorDevice *device, QWidget *parent)
DeviceImageWidget::DeviceImageWidget(const iDescriptorDevice *device,
QWidget *parent)
: QWidget(parent), m_device(device)
{
QVBoxLayout *layout = new QVBoxLayout(this);
+2 -2
View File
@@ -30,7 +30,7 @@ class DeviceImageWidget : public QWidget
Q_OBJECT
public:
explicit DeviceImageWidget(iDescriptorDevice *device,
explicit DeviceImageWidget(const iDescriptorDevice *device,
QWidget *parent = nullptr);
~DeviceImageWidget();
@@ -47,7 +47,7 @@ private:
QPixmap createCompositeImage() const;
QRect findScreenArea(const QPixmap &mockup) const;
iDescriptorDevice *m_device;
const iDescriptorDevice *m_device;
ResponsiveQLabel *m_imageLabel;
QTimer *m_timeUpdateTimer;
+9 -7
View File
@@ -25,6 +25,7 @@
#include "iDescriptor.h"
#include "infolabel.h"
#include "privateinfolabel.h"
#include "toolboxwidget.h"
#include <QApplication>
#include <QDebug>
#include <QGraphicsDropShadowEffect>
@@ -44,7 +45,8 @@
#include <QVBoxLayout>
#include <QtCore>
DeviceInfoWidget::DeviceInfoWidget(iDescriptorDevice *device, QWidget *parent)
DeviceInfoWidget::DeviceInfoWidget(const iDescriptorDevice *device,
QWidget *parent)
: QWidget(parent), m_device(device)
{
// Main layout with horizontal orientation
@@ -77,22 +79,22 @@ DeviceInfoWidget::DeviceInfoWidget(iDescriptorDevice *device, QWidget *parent)
QIcon(":/resources/icons/IcOutlinePowerSettingsNew.png"), "Shutdown",
1.0, this);
shutdownBtn->setIconSize(QSize(20, 20));
// connect(shutdownBtn, &ZIconWidget::clicked, this,
// [device]() { ToolboxWidget::shutdownDevice(device); });
connect(shutdownBtn, &ZIconWidget::clicked, this,
[device]() { ToolboxWidget::shutdownDevice(device); });
ZIconWidget *restartBtn =
new ZIconWidget(QIcon(":/resources/icons/IcTwotoneRestartAlt.png"),
"Restart", 1.0, this);
restartBtn->setIconSize(QSize(20, 20));
// connect(restartBtn, &ZIconWidget::clicked, this,
// [device]() { ToolboxWidget::restartDevice(device); });
connect(restartBtn, &ZIconWidget::clicked, this,
[device]() { ToolboxWidget::restartDevice(device); });
ZIconWidget *recoveryBtn =
new ZIconWidget(QIcon(":/resources/icons/HugeiconsWrench01.png"),
"Recovery", 1.0, this);
recoveryBtn->setIconSize(QSize(20, 20));
// connect(recoveryBtn, &ZIconWidget::clicked, this,
// [device]() { ToolboxWidget::_enterRecoveryMode(device); });
connect(recoveryBtn, &ZIconWidget::clicked, this,
[device]() { ToolboxWidget::enterRecoveryMode(device); });
actionsLayout->addWidget(shutdownBtn);
actionsLayout->addWidget(restartBtn);
+2 -2
View File
@@ -31,7 +31,7 @@ class DeviceInfoWidget : public QWidget
{
Q_OBJECT
public:
explicit DeviceInfoWidget(iDescriptorDevice *device,
explicit DeviceInfoWidget(const iDescriptorDevice *device,
QWidget *parent = nullptr);
~DeviceInfoWidget(); // added destructor
@@ -39,7 +39,7 @@ private slots:
void onBatteryMoreClicked();
private:
iDescriptorDevice *m_device;
const iDescriptorDevice *m_device;
QTimer *m_updateTimer;
void updateBatteryInfo();
void updateChargingStatusIcon();
+87 -93
View File
@@ -20,10 +20,10 @@
#include "devicemanagerwidget.h"
#include "appcontext.h"
#include "devicemenuwidget.h"
// #include "devicependingwidget.h"
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
// #include "recoverydeviceinfowidget.h"
// #endif
#include "devicependingwidget.h"
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
#include "recoverydeviceinfowidget.h"
#endif
#include "settingsmanager.h"
#include <QDebug>
@@ -33,7 +33,7 @@ DeviceManagerWidget::DeviceManagerWidget(QWidget *parent)
setupUI();
connect(AppContext::sharedInstance(), &AppContext::deviceAdded, this,
[this](iDescriptorDevice *device) {
[this](const iDescriptorDevice *device) {
addDevice(device);
// Apply settings-based behavior for switching to new device
@@ -70,7 +70,7 @@ DeviceManagerWidget::DeviceManagerWidget(QWidget *parent)
});
connect(AppContext::sharedInstance(), &AppContext::devicePaired, this,
[this](iDescriptorDevice *device) {
[this](const iDescriptorDevice *device) {
addPairedDevice(device);
// SettingsManager::sharedInstance()->doIfEnabled(
// SettingsManager::Setting::SwitchToNewDevice,
@@ -82,22 +82,19 @@ DeviceManagerWidget::DeviceManagerWidget(QWidget *parent)
emit updateNoDevicesConnected();
});
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
// connect(AppContext::sharedInstance(),
// &AppContext::recoveryDeviceAdded,
// this, [this](const iDescriptorRecoveryDevice
// *recoveryDeviceInfo) {
// addRecoveryDevice(recoveryDeviceInfo);
// emit updateNoDevicesConnected();
// });
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
connect(AppContext::sharedInstance(), &AppContext::recoveryDeviceAdded,
this, [this](const iDescriptorRecoveryDevice *recoveryDeviceInfo) {
addRecoveryDevice(recoveryDeviceInfo);
emit updateNoDevicesConnected();
});
// connect(AppContext::sharedInstance(),
// &AppContext::recoveryDeviceRemoved,
// this, [this](uint64_t ecid) {
// removeRecoveryDevice(ecid);
// emit updateNoDevicesConnected();
// });
// #endif
connect(AppContext::sharedInstance(), &AppContext::recoveryDeviceRemoved,
this, [this](uint64_t ecid) {
removeRecoveryDevice(ecid);
emit updateNoDevicesConnected();
});
#endif
connect(AppContext::sharedInstance(), &AppContext::devicePairingExpired,
this, [this](const QString &udid) {
@@ -130,7 +127,7 @@ void DeviceManagerWidget::setupUI()
&DeviceManagerWidget::onDeviceSelectionChanged);
}
void DeviceManagerWidget::addDevice(iDescriptorDevice *device)
void DeviceManagerWidget::addDevice(const iDescriptorDevice *device)
{
if (m_deviceWidgets.contains(device->udid)) {
qWarning() << "Device already exists:"
@@ -150,52 +147,51 @@ void DeviceManagerWidget::addDevice(iDescriptorDevice *device)
device->deviceInfo.isWireless)};
}
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
// void DeviceManagerWidget::addRecoveryDevice(
// const iDescriptorRecoveryDevice *device)
// {
// try {
// // Create device info widget
// RecoveryDeviceInfoWidget *recoveryDeviceInfoWidget =
// new RecoveryDeviceInfoWidget(device);
// m_recoveryDeviceWidgets.insert(
// device->ecid,
// std::pair{recoveryDeviceInfoWidget,
// m_sidebar->addRecoveryDevice(device->ecid)});
// m_stackedWidget->addWidget(recoveryDeviceInfoWidget);
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
void DeviceManagerWidget::addRecoveryDevice(
const iDescriptorRecoveryDevice *device)
{
try {
// Create device info widget
RecoveryDeviceInfoWidget *recoveryDeviceInfoWidget =
new RecoveryDeviceInfoWidget(device);
m_recoveryDeviceWidgets.insert(
device->ecid,
std::pair{recoveryDeviceInfoWidget,
m_sidebar->addRecoveryDevice(device->ecid)});
m_stackedWidget->addWidget(recoveryDeviceInfoWidget);
// } catch (...) {
// qDebug() << "Error initializing recovery device";
// }
// }
} catch (...) {
qDebug() << "Error initializing recovery device";
}
}
// void DeviceManagerWidget::removeRecoveryDevice(uint64_t ecid)
// {
// qDebug() << "Removing recovery device with ECID:" << ecid;
// if (!m_recoveryDeviceWidgets.contains(ecid)) {
// qDebug() << "Recovery device with ECID" + QString::number(ecid) +
// " not found. Please report this issue.";
// return;
// }
void DeviceManagerWidget::removeRecoveryDevice(uint64_t ecid)
{
qDebug() << "Removing recovery device with ECID:" << ecid;
if (!m_recoveryDeviceWidgets.contains(ecid)) {
qDebug() << "Recovery device with ECID" + QString::number(ecid) +
" not found. Please report this issue.";
return;
}
// RecoveryDeviceInfoWidget *deviceWidget =
// m_recoveryDeviceWidgets[ecid].first;
// RecoveryDeviceSidebarItem *sidebarItem =
// m_recoveryDeviceWidgets[ecid].second;
RecoveryDeviceInfoWidget *deviceWidget =
m_recoveryDeviceWidgets[ecid].first;
RecoveryDeviceSidebarItem *sidebarItem =
m_recoveryDeviceWidgets[ecid].second;
// if (deviceWidget != nullptr && sidebarItem != nullptr) {
// qDebug() << "Recovery device exists removing:" <<
// QString::number(ecid);
if (deviceWidget != nullptr && sidebarItem != nullptr) {
qDebug() << "Recovery device exists removing:" << QString::number(ecid);
// m_recoveryDeviceWidgets.remove(ecid);
// m_stackedWidget->removeWidget(deviceWidget);
// m_sidebar->removeRecoveryDevice(ecid);
// deviceWidget->deleteLater();
m_recoveryDeviceWidgets.remove(ecid);
m_stackedWidget->removeWidget(deviceWidget);
m_sidebar->removeRecoveryDevice(ecid);
deviceWidget->deleteLater();
// emit updateNoDevicesConnected();
// }
// }
// #endif
emit updateNoDevicesConnected();
}
}
#endif
void DeviceManagerWidget::addPendingDevice(const QString &uniq, bool locked)
{
@@ -222,27 +218,26 @@ void DeviceManagerWidget::addPendingDevice(const QString &uniq, bool locked)
void DeviceManagerWidget::removePendingDevice(const QString &udid)
{
// qDebug() << "Removing pending device:" << udid;
// if (!m_pendingDeviceWidgets.contains(udid.toStdString())) {
// qDebug() << "Pending device not found:" << udid;
// return;
// }
// std::string udidStr = udid.toStdString();
// DevicePendingWidget *deviceWidget =
// m_pendingDeviceWidgets[udidStr].first; DevicePendingSidebarItem
// *sidebarItem =
// m_pendingDeviceWidgets[udidStr].second;
qDebug() << "Removing pending device:" << udid;
if (!m_pendingDeviceWidgets.contains(udid.toStdString())) {
qDebug() << "Pending device not found:" << udid;
return;
}
std::string udidStr = udid.toStdString();
DevicePendingWidget *deviceWidget = m_pendingDeviceWidgets[udidStr].first;
DevicePendingSidebarItem *sidebarItem =
m_pendingDeviceWidgets[udidStr].second;
// if (deviceWidget != nullptr && sidebarItem != nullptr) {
// qDebug() << "Pending device exists removing:" << udid;
// m_pendingDeviceWidgets.remove(udidStr);
// m_stackedWidget->removeWidget(deviceWidget);
// m_sidebar->removePendingDevice(udidStr);
// deviceWidget->deleteLater();
// }
if (deviceWidget != nullptr && sidebarItem != nullptr) {
qDebug() << "Pending device exists removing:" << udid;
m_pendingDeviceWidgets.remove(udidStr);
m_stackedWidget->removeWidget(deviceWidget);
m_sidebar->removePendingDevice(udidStr);
deviceWidget->deleteLater();
}
}
void DeviceManagerWidget::addPairedDevice(iDescriptorDevice *device)
void DeviceManagerWidget::addPairedDevice(const iDescriptorDevice *device)
{
qDebug() << "Device paired:" << QString::fromStdString(device->udid);
@@ -336,19 +331,18 @@ void DeviceManagerWidget::onDeviceSelectionChanged(
}
break;
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
// case DeviceSelection::Recovery:
// if (m_recoveryDeviceWidgets.contains(selection.ecid)) {
// QWidget *tabWidget =
// m_recoveryDeviceWidgets[selection.ecid].first; if
// (tabWidget) {
// m_stackedWidget->setCurrentWidget(tabWidget);
// // Clear current device since we're viewing recovery
// device m_currentDeviceUuid = "";
// }
// }
// break;
// #endif
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
case DeviceSelection::Recovery:
if (m_recoveryDeviceWidgets.contains(selection.ecid)) {
QWidget *tabWidget = m_recoveryDeviceWidgets[selection.ecid].first;
if (tabWidget) {
m_stackedWidget->setCurrentWidget(tabWidget);
// Clear current device since we're viewing recovery device
m_currentDeviceUuid = "";
}
}
break;
#endif
case DeviceSelection::Pending:
if (m_pendingDeviceWidgets.contains(selection.udid)) {
+14 -15
View File
@@ -24,9 +24,9 @@
#include "devicependingwidget.h"
#include "devicesidebarwidget.h"
#include "iDescriptor.h"
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
// #include "recoverydeviceinfowidget.h"
// #endif
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
#include "recoverydeviceinfowidget.h"
#endif
#include <QHBoxLayout>
#include <QMap>
#include <QStackedWidget>
@@ -51,14 +51,14 @@ private slots:
private:
void setupUI();
void addDevice(iDescriptorDevice *device);
void addDevice(const iDescriptorDevice *device);
void removeDevice(const std::string &uuid);
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
// void addRecoveryDevice(const iDescriptorRecoveryDevice *device);
// void removeRecoveryDevice(uint64_t ecid);
// #endif
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
void addRecoveryDevice(const iDescriptorRecoveryDevice *device);
void removeRecoveryDevice(uint64_t ecid);
#endif
void addPendingDevice(const QString &udid, bool locked);
void addPairedDevice(iDescriptorDevice *device);
void addPairedDevice(const iDescriptorDevice *device);
void removePendingDevice(const QString &udid);
QHBoxLayout *m_mainLayout;
@@ -72,12 +72,11 @@ private:
std::pair<DevicePendingWidget *, DevicePendingSidebarItem *>>
m_pendingDeviceWidgets; // Map to store devices by UDID
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
// QMap<uint64_t,
// std::pair<RecoveryDeviceInfoWidget *, RecoveryDeviceSidebarItem
// *>>
// m_recoveryDeviceWidgets; // Map to store recovery devices by ECID
// #endif
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
QMap<uint64_t,
std::pair<RecoveryDeviceInfoWidget *, RecoveryDeviceSidebarItem *>>
m_recoveryDeviceWidgets; // Map to store recovery devices by ECID
#endif
std::string m_currentDeviceUuid;
};
+2 -1
View File
@@ -31,7 +31,8 @@
#include <QStackedWidget>
#include <QVBoxLayout>
DeviceMenuWidget::DeviceMenuWidget(iDescriptorDevice *device, QWidget *parent)
DeviceMenuWidget::DeviceMenuWidget(const iDescriptorDevice *device,
QWidget *parent)
: QWidget{parent}, m_device(device)
{
QVBoxLayout *mainLayout = new QVBoxLayout(this);
+3 -3
View File
@@ -31,15 +31,15 @@ class DeviceMenuWidget : public QWidget
{
Q_OBJECT
public:
explicit DeviceMenuWidget(iDescriptorDevice *device,
explicit DeviceMenuWidget(const iDescriptorDevice *device,
QWidget *parent = nullptr);
void switchToTab(const QString &tabName);
void init();
~DeviceMenuWidget();
private:
QStackedWidget *stackedWidget; // Pointer to the stacked widget
iDescriptorDevice *m_device; // Pointer to the iDescriptor device
QStackedWidget *stackedWidget;
const iDescriptorDevice *m_device;
DeviceInfoWidget *m_deviceInfoWidget;
InstalledAppsWidget *m_installedAppsWidget;
GalleryWidget *m_galleryWidget;
+4 -3
View File
@@ -33,7 +33,8 @@ extern "C" {
using namespace iDescriptor;
DiskUsageWidget::DiskUsageWidget(iDescriptorDevice *device, QWidget *parent)
DiskUsageWidget::DiskUsageWidget(const iDescriptorDevice *device,
QWidget *parent)
: QWidget(parent), m_device(device), m_state(Loading), m_totalCapacity(0),
m_systemUsage(0), m_appsUsage(0), m_mediaUsage(0), m_othersUsage(0),
m_freeSpace(0)
@@ -501,11 +502,11 @@ void DiskUsageWidget::fetchData()
/*
on older devices if Photos.sqlite is high in size and the device is
connected wirelessly it takes ~5 minutes to read the entire file maybe
skip on wireless connections on old devices (iPhone 6s in this case)?
skip on wireless connections on old devices than iPhone10,1 (iPhone 8)
*/
if (m_device->deviceInfo.is_iPhone && m_device->deviceInfo.isWireless &&
!iDescriptor::Utils::isProductTypeNewer(
m_device->deviceInfo.rawProductType, "iPhone8,4")) {
m_device->deviceInfo.rawProductType, "iPhone10,1")) {
qDebug() << "Skipping gallery usage calculation on older "
"wireless device.";
result["galleryUsage"] = QVariant::fromValue(uint64_t(0));
+2 -2
View File
@@ -35,7 +35,7 @@ class DiskUsageWidget : public QWidget
{
Q_OBJECT
public:
explicit DiskUsageWidget(iDescriptorDevice *device,
explicit DiskUsageWidget(const iDescriptorDevice *device,
QWidget *parent = nullptr);
private:
@@ -45,7 +45,7 @@ private:
enum State { Loading, Ready, Error };
iDescriptorDevice *m_device;
const iDescriptorDevice *m_device;
State m_state;
QString m_errorMessage;
+1 -1
View File
@@ -52,7 +52,7 @@ ExportManager::~ExportManager()
m_activeJobs.clear();
}
QUuid ExportManager::startExport(iDescriptorDevice *device,
QUuid ExportManager::startExport(const iDescriptorDevice *device,
const QList<ExportItem> &items,
const QString &destinationPath,
std::optional<AfcClientHandle *> altAfc)
+2 -1
View File
@@ -48,7 +48,8 @@ public:
ExportManager(const ExportManager &) = delete;
ExportManager &operator=(const ExportManager &) = delete;
QUuid startExport(iDescriptorDevice *device, const QList<ExportItem> &items,
QUuid startExport(const iDescriptorDevice *device,
const QList<ExportItem> &items,
const QString &destinationPath,
std::optional<AfcClientHandle *> altAfc = std::nullopt);
+1 -1
View File
@@ -42,7 +42,7 @@
#include <QTreeWidget>
#include <QVariant>
FileExplorerWidget::FileExplorerWidget(iDescriptorDevice *device,
FileExplorerWidget::FileExplorerWidget(const iDescriptorDevice *device,
QWidget *parent)
: QWidget(parent), m_device(device)
{
+2 -2
View File
@@ -39,7 +39,7 @@ class FileExplorerWidget : public QWidget
{
Q_OBJECT
public:
explicit FileExplorerWidget(iDescriptorDevice *device,
explicit FileExplorerWidget(const iDescriptorDevice *device,
QWidget *parent = nullptr);
private slots:
@@ -50,7 +50,7 @@ private:
QStackedWidget *m_stackedWidget;
AfcClientHandle *currentAfcClient;
QTreeWidget *m_sidebarTree;
iDescriptorDevice *m_device;
const iDescriptorDevice *m_device;
// Tree items
QTreeWidgetItem *m_defaultAfcItem;
+14 -13
View File
@@ -50,7 +50,7 @@
https://github.com/ScottKjr3347/iOS_Local_PL_Photos.sqlite_Queries
*/
GalleryWidget::GalleryWidget(iDescriptorDevice *device, QWidget *parent)
GalleryWidget::GalleryWidget(const iDescriptorDevice *device, QWidget *parent)
: QWidget{parent}, m_device(device), m_model(nullptr),
m_albumSelectionWidget(nullptr), m_albumListView(nullptr),
m_photoGalleryWidget(nullptr), m_listView(nullptr), m_backButton(nullptr)
@@ -479,6 +479,18 @@ void GalleryWidget::onAlbumSelected(const QString &albumPath)
m_model = new PhotoModel(m_device, getCurrentFilterType(), this);
m_listView->setModel(m_model);
connect(m_model, &PhotoModel::albumPathSet, this, [this]() {
// Switch to photo gallery view once album is loaded
m_loadingWidget->switchToWidget(m_photoGalleryWidget);
// Enable controls and show back button
setControlsEnabled(true);
m_backButton->show();
});
connect(m_model, &PhotoModel::timedOut, this, [this]() {
m_loadingWidget->showError("Timed out loading album");
});
// Update export button states based on selection
connect(m_listView->selectionModel(),
&QItemSelectionModel::selectionChanged, this, [this]() {
@@ -488,25 +500,14 @@ void GalleryWidget::onAlbumSelected(const QString &albumPath)
});
}
// connect(m_model, &PhotoModel::thumbnailNeedsToBeLoaded, m_model,
// &PhotoModel::requestThumbnail, Qt::QueuedConnection);
// Set album path and load photos
m_model->setAlbumPath(albumPath);
// Switch to photo gallery view
m_loadingWidget->switchToWidget(m_photoGalleryWidget);
// Enable controls and show back button
setControlsEnabled(true);
m_backButton->show();
m_loadingWidget->showLoading();
}
void GalleryWidget::onBackToAlbums()
{
if (m_model) {
// disconnect(m_model, &PhotoModel::thumbnailNeedsToBeLoaded, m_model,
// &PhotoModel::requestThumbnail);
}
// Switch back to album selection view
m_loadingWidget->switchToWidget(m_albumSelectionWidget);
+2 -2
View File
@@ -44,7 +44,7 @@ class GalleryWidget : public QWidget
Q_OBJECT
public:
explicit GalleryWidget(iDescriptorDevice *device,
explicit GalleryWidget(const iDescriptorDevice *device,
QWidget *parent = nullptr);
void load();
~GalleryWidget();
@@ -70,7 +70,7 @@ private:
void onPhotoContextMenu(const QPoint &pos);
PhotoModel::FilterType getCurrentFilterType() const;
iDescriptorDevice *m_device;
const iDescriptorDevice *m_device;
bool m_loaded = false;
QString m_currentAlbumPath;
+2
View File
@@ -57,6 +57,8 @@
#define COLOR_HYPERLINK QColor("#FF7FFFD4")
#endif
#define THUMBNAIL_SIZE QSize(128, 128)
inline QString mergeStyles(QWidget *widget, const QString &newStyles)
{
if (!widget) {
+25 -1
View File
@@ -59,8 +59,19 @@ public:
bool isMac() const { return m_isMac; };
bool isUdid() const { return !m_isMac; }
void set(const QString &uniq, bool isMac = false)
{
m_uniq = uniq;
m_isMac = isMac;
};
void set(const std::string &uniq, bool isMac = false)
{
m_uniq = QString::fromStdString(uniq);
m_isMac = isMac;
};
const QString &get() const { return m_uniq; }
operator QString() const { return m_uniq; }
operator std::string() const { return m_uniq.toStdString(); }
private:
QString m_uniq;
@@ -139,8 +150,21 @@ public:
static bool isVideoFile(const QString &fileName)
{
/* known iPhone video file extensions */
/* known iPhone video file extensions (AVI and MKV is not common but it
* may be some from some app)*/
return fileName.endsWith(".MOV", Qt::CaseInsensitive) ||
fileName.endsWith(".MP4", Qt::CaseInsensitive) ||
fileName.endsWith(".M4V", Qt::CaseInsensitive) ||
fileName.endsWith(".AVI", Qt::CaseInsensitive) ||
fileName.endsWith(".MKV", Qt::CaseInsensitive);
}
static bool isGalleryFile(const QString &fileName)
{
return fileName.endsWith(".JPG", Qt::CaseInsensitive) ||
fileName.endsWith(".PNG", Qt::CaseInsensitive) ||
fileName.endsWith(".HEIC", Qt::CaseInsensitive) ||
fileName.endsWith(".MOV", Qt::CaseInsensitive) ||
fileName.endsWith(".MP4", Qt::CaseInsensitive) ||
fileName.endsWith(".M4V", Qt::CaseInsensitive);
}
+31 -39
View File
@@ -36,6 +36,7 @@
#include <idevice++/heartbeat.hpp>
#include <idevice++/installation_proxy.hpp>
#include <idevice++/lockdown.hpp>
#include <idevice++/lockdown_location_simulation.hpp>
#include <idevice++/provider.hpp>
#include <idevice++/readwrite.hpp>
#include <idevice++/rsd.hpp>
@@ -68,11 +69,16 @@
((((maj) & 0xFF) << 16) | (((min) & 0xFF) << 8) | ((patch) & 0xFF))
#include "devicemonitor.h"
#include "iDescriptor-utils.h"
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
#include <libirecovery.h>
#endif
#define DeviceLockedMountErrorCode -21
#define NotFoundErrorCode -14
#define ServiceNotFoundErrorCode -15
#define PairingDialogResponsePending -28
#define InvalidServiceErrorCode -59
#define TimeoutErrorCode -71
#define DISK_IMAGE_TYPE_DEVELOPER "Developer"
@@ -214,6 +220,8 @@ struct DeviceInfo {
bool isWireless = false;
// empty on USB devices
std::string ipAddress;
/* same as udid on iDescriptorDevice */
std::string UniqueDeviceID;
};
struct iDescriptorDevice {
@@ -242,34 +250,35 @@ struct iDescriptorInitDeviceResult {
std::shared_ptr<DiagnosticsRelay> diagRelay;
QThread *heartbeatThread;
};
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
// struct iDescriptorRecoveryDevice {
// uint64_t ecid;
// irecv_mode mode;
// uint32_t cpid;
// uint32_t bdid;
// std::string displayName;
// std::recursive_mutex *mutex;
// };
// #endif
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
struct iDescriptorRecoveryDevice {
uint64_t ecid;
irecv_mode mode;
uint32_t cpid;
uint32_t bdid;
std::string displayName;
std::recursive_mutex mutex;
};
struct iDescriptorInitDeviceResultRecovery {
irecv_client_t client = nullptr;
irecv_device_info deviceInfo;
irecv_error_t error;
bool success = false;
irecv_mode mode = IRECV_K_RECOVERY_MODE_1;
const char *displayName = nullptr;
};
std::string parse_recovery_mode(irecv_mode productType);
void init_idescriptor_recovery_device(uint64_t ecid,
iDescriptorInitDeviceResultRecovery &res);
#endif
struct TakeScreenshotResult {
bool success = false;
QImage img;
};
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
// struct iDescriptorInitDeviceResultRecovery {
// irecv_client_t client = nullptr;
// irecv_device_info deviceInfo;
// irecv_error_t error;
// bool success = false;
// irecv_mode mode = IRECV_K_RECOVERY_MODE_1;
// const char *displayName = nullptr;
// };
// #endif
void warn(const QString &message, const QString &title = "Warning",
QWidget *parent = nullptr);
@@ -402,15 +411,8 @@ public:
}
};
// afc_error_t safe_afc_read_directory(afc_client_t afcClient, idevice_t device,
// const char *path, char ***dirs);
std::string parse_product_type(const std::string &productType);
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
// std::string parse_recovery_mode(irecv_mode productType);
// #endif
struct MediaEntry {
std::string name;
bool isDir;
@@ -440,14 +442,6 @@ void init_idescriptor_device(const iDescriptor::Uniq &uniq,
iDescriptorInitDeviceResult &result,
const WirelessInitArgs &wirelessArgs = {"", ""});
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
// iDescriptorInitDeviceResultRecovery
// init_idescriptor_recovery_device(uint64_t ecid);
// #endif
// bool set_location(idevice_t device, char *lat, char *lon);
// bool shutdown(idevice_t device);
IdeviceFfiError *mount_dev_image(const iDescriptorDevice *device,
const char *image_file,
const char *signature_file);
@@ -467,8 +461,6 @@ MountedImageInfo _get_mounted_image(const iDescriptorDevice *device);
void mounted_image_info_free(MountedImageInfo &info);
// bool restart(std::string udid);
enum class ImageCompatibility {
Compatible, // Exact match or known compatible version
MaybeCompatible, // Major version matches but minor doesn't
+165 -64
View File
@@ -15,6 +15,7 @@ extern "C" {
ImageLoader::ImageLoader(QObject *parent) : QObject(parent)
{
// TODO: maybe finetune to hardware ?
m_pool.setMaxThreadCount(10);
// 350 MB cache for thumbnails
m_cache.setMaxCost(350 * 1024 * 1024);
@@ -22,51 +23,144 @@ ImageLoader::ImageLoader(QObject *parent) : QObject(parent)
bool ImageLoader::isLoading(const QString &path)
{
return m_pending.contains(path);
QMutexLocker locker(&m_mutex);
return m_pendingTasks.contains(path);
}
void ImageLoader::requestThumbnail(const iDescriptorDevice *device,
const QString &path, int priority,
unsigned int row)
const QString &path, unsigned int row)
{
if (auto *cached = m_cache.object(path)) {
emit thumbnailReady(path, *cached, row);
return;
}
if (m_pending.contains(path))
return;
{
QMutexLocker locker(&m_mutex);
if (m_pendingTasks.contains(path))
return;
}
m_pending.insert(path);
auto *task = new ImageTask(device, path, row);
// FIXME: qsize
auto *task = new ImageTask(device, path, QSize(128, 128), row);
{
QMutexLocker locker(&m_mutex);
m_pendingTasks[path] = task;
}
connect(task, &ImageTask::finished, this, &ImageLoader::onTaskFinished,
Qt::QueuedConnection);
// Use row as priority
m_pool.start(task, row);
}
/*
this method should not load from cache
because cached images are already scaled down
we need the original image
*/
void ImageLoader::requestImageWithCallback(
const iDescriptorDevice *device, const QString &path, int priority,
std::function<void(const QPixmap &)> callback)
{
/*
FIXME: priority is passed as row
nothing dangerous but a bit hacky, should be handled better
*/ //scale=false
auto *task = new ImageTask(device, path, priority, false);
/*
TODO: should we do this ?
this function is meant for the media preview dialog,
which only loads a image at a time
and not really related to the thumbnails in the photomodel
*/
// m_pendingTasks[path] = task;
connect(
task, &ImageTask::finished, this,
[this, path, callback](const QString &, const QPixmap &pixmap,
unsigned int row) { callback(pixmap); },
Qt::QueuedConnection);
m_pool.start(task, priority);
}
void ImageLoader::cancelThumbnail(const QString &path)
{
m_pending.remove(path);
qDebug() << "Attempting to cancel thumbnail loading for" << path;
QMutexLocker locker(&m_mutex);
if (!m_pendingTasks.contains(path)) {
return;
}
ImageTask *task = m_pendingTasks.value(path);
if (task && m_pool.tryTake(task)) {
qDebug() << "Cancelled thumbnail loading for" << path;
m_pendingTasks.remove(path);
delete task;
} else {
m_pendingTasks.remove(path);
}
}
void ImageLoader::clear()
{
m_pending.clear();
qDebug() << "Clearing ImageLoader cache and pending tasks";
m_pool.clear();
{
QMutexLocker locker(&m_mutex);
for (auto it = m_pendingTasks.begin(); it != m_pendingTasks.end();) {
ImageTask *task = it.value();
if (task && m_pool.tryTake(task)) {
qDebug() << "Cancelled pending task";
delete task;
}
it = m_pendingTasks.erase(it);
}
}
/*
TODO: This could make the UI unresponsive but
maybe a good approch to handle
async cancellation properly(wireless)
Wait for any running tasks to complete
*/
// m_pool.waitForDone();
{
QMutexLocker locker(&m_mutex);
m_pendingTasks.clear();
}
m_cache.clear();
}
void ImageLoader::onTaskFinished(const QString &path, const QPixmap &pixmap,
unsigned int row)
{
// Stale?
if (!m_pending.contains(path))
return;
ImageTask *task = nullptr;
m_pending.remove(path);
{
QMutexLocker locker(&m_mutex);
if (!m_pendingTasks.contains(path)) {
return;
}
task = m_pendingTasks.take(path);
}
if (task) {
delete task;
}
// Cache
m_cache.insert(path, new QPixmap(pixmap));
@@ -74,18 +168,56 @@ void ImageLoader::onTaskFinished(const QString &path, const QPixmap &pixmap,
emit thumbnailReady(path, pixmap, row);
}
// Static function that runs in worker thread
QPixmap ImageLoader::loadThumbnailFromDevice(const iDescriptorDevice *device,
const QString &filePath,
const QSize &size)
// almost a copy of loadThumbnailFromDevice but without any scaling logic
QPixmap ImageLoader::loadImage(const iDescriptorDevice *device,
const QString &filePath)
{
// Load from device using ServiceManager
QByteArray imageData = ServiceManager::safeReadAfcFileToByteArray(
device, filePath.toUtf8().constData());
if (imageData.isEmpty()) {
qDebug() << "Could not read from device:" << filePath;
return {}; // Return empty pixmap on error
return {};
}
if (filePath.endsWith(".HEIC", Qt::CaseInsensitive)) {
QPixmap img = load_heic(imageData);
return img.isNull() ? QPixmap() : img;
}
QBuffer buffer(&imageData);
buffer.open(QIODevice::ReadOnly);
QImageReader reader(&buffer);
if (reader.canRead()) {
QImage image = reader.read();
if (!image.isNull()) {
return QPixmap::fromImage(image);
}
qDebug() << "QImageReader failed to decode" << filePath
<< "Error:" << reader.errorString();
}
// Fallback for formats QImageReader might struggle with
QPixmap pixmap;
if (pixmap.loadFromData(imageData)) {
return pixmap;
}
qDebug() << "Could not decode image data for:" << filePath;
return {};
}
QPixmap ImageLoader::loadThumbnailFromDevice(const iDescriptorDevice *device,
const QString &filePath,
const QSize &size)
{
QByteArray imageData = ServiceManager::safeReadAfcFileToByteArray(
device, filePath.toUtf8().constData());
if (imageData.isEmpty()) {
qDebug() << "Could not read from device:" << filePath;
return {};
}
if (filePath.endsWith(".HEIC", Qt::CaseInsensitive)) {
@@ -95,14 +227,11 @@ QPixmap ImageLoader::loadThumbnailFromDevice(const iDescriptorDevice *device,
Qt::SmoothTransformation);
}
// Use QImageReader for efficient, low-memory scaled loading
QBuffer buffer(&imageData);
buffer.open(QIODevice::ReadOnly);
QImageReader reader(&buffer);
if (reader.canRead()) {
// This is the key optimization: it decodes a smaller image directly,
// saving a massive amount of memory.
reader.setScaledSize(size);
QImage image = reader.read();
if (!image.isNull()) {
@@ -146,10 +275,8 @@ ImageLoader::generateVideoThumbnailFFmpeg(const iDescriptorDevice *device,
// Get file size
AfcFileInfo info = {};
IdeviceFfiError
*err_info = // Use distinct variable name for the error from GetFileInfo
ServiceManager::safeAfcGetFileInfo(
device, filePath.toUtf8().constData(), &info);
IdeviceFfiError *err_info = ServiceManager::safeAfcGetFileInfo(
device, filePath.toUtf8().constData(), &info);
uint64_t fileSize = 0;
if (err_info) {
@@ -157,12 +284,11 @@ ImageLoader::generateVideoThumbnailFFmpeg(const iDescriptorDevice *device,
<< "Error:" << err_info->message;
idevice_error_free(err_info);
ServiceManager::safeAfcFileClose(device, fileHandle);
afc_file_info_free(&info); // Free internal strings of info
return {};
}
fileSize = info.size;
afc_file_info_free(&info); // Free internal strings of info after use
afc_file_info_free(&info);
if (fileSize == 0) {
ServiceManager::safeAfcFileClose(device, fileHandle);
@@ -189,7 +315,7 @@ ImageLoader::generateVideoThumbnailFFmpeg(const iDescriptorDevice *device,
StreamContext *streamCtx =
new StreamContext{device, fileHandle, fileSize, 0};
// Custom read function that reads from device on-demand
// Custom read function
auto readPacket = [](void *opaque, uint8_t *buf, int bufSize) -> int {
StreamContext *ctx = static_cast<StreamContext *>(opaque);
@@ -201,10 +327,8 @@ ImageLoader::generateVideoThumbnailFFmpeg(const iDescriptorDevice *device,
std::min(static_cast<uint32_t>(bufSize),
static_cast<uint32_t>(ctx->fileSize - ctx->currentPos));
size_t bytesRead = 0;
uint8_t *read_data_ptr =
nullptr; // Pointer to store the data allocated by safeAfcFileRead
uint8_t *read_data_ptr = nullptr;
// Call safeAfcFileRead to get the data into a newly allocated buffer
IdeviceFfiError *err = ServiceManager::safeAfcFileRead(
ctx->device, ctx->fileHandle, &read_data_ptr, toRead, &bytesRead);
@@ -238,9 +362,7 @@ ImageLoader::generateVideoThumbnailFFmpeg(const iDescriptorDevice *device,
// `buf`
if (read_data_ptr) {
memcpy(buf, read_data_ptr, bytesRead);
afc_file_read_data_free(
read_data_ptr,
bytesRead); // Free the memory allocated by safeAfcFileRead
afc_file_read_data_free(read_data_ptr, bytesRead);
} else {
qWarning() << "AFC readPacket: read_data_ptr was null but "
"bytesRead > 0. This is unexpected.";
@@ -285,7 +407,7 @@ ImageLoader::generateVideoThumbnailFFmpeg(const iDescriptorDevice *device,
if (err) {
qDebug() << "AFC seek error:" << err->message
<< "code:" << err->code;
// idevice_error_free(err);
idevice_error_free(err);
return -1;
}
@@ -444,6 +566,12 @@ ImageLoader::generateVideoThumbnailFFmpeg(const iDescriptorDevice *device,
QImage imgCopy = img.copy();
// Scale to requested size
/*
TODO: scaling might become optional
if we ever needed the raw frame,
might need to abstract the main logic to get the frame
and handle scaling separately
*/
thumbnail = QPixmap::fromImage(
imgCopy.scaled(requestedSize, Qt::KeepAspectRatio,
Qt::SmoothTransformation));
@@ -472,30 +600,3 @@ ImageLoader::generateVideoThumbnailFFmpeg(const iDescriptorDevice *device,
return thumbnail;
}
// todo make sure this is also a task used in mediapreviewdialog.cpp
QPixmap ImageLoader::loadImage(const iDescriptorDevice *device,
const QString &filePath)
{
QByteArray imageData = ServiceManager::safeReadAfcFileToByteArray(
device, filePath.toUtf8().constData());
if (imageData.isEmpty()) {
qDebug() << "Could not read from device:" << filePath;
return QPixmap(); // Return empty pixmap on error
}
if (filePath.endsWith(".HEIC")) {
qDebug() << "Loading HEIC image from data for:" << filePath;
QPixmap img = load_heic(imageData);
return img.isNull() ? QPixmap() : img;
}
QPixmap original;
if (!original.loadFromData(imageData)) {
qDebug() << "Could not decode image data for:" << filePath;
return QPixmap();
}
return original;
}
+11 -5
View File
@@ -3,19 +3,20 @@
#include "iDescriptor.h"
#include <QCache>
#include <QHash>
#include <QImage>
#include <QMutex>
#include <QObject>
#include <QSet>
#include <QString>
#include <QThreadPool>
class ImageTask;
class ImageLoader : public QObject
{
Q_OBJECT
public:
enum LoadPriority { Low = 0, Medium = 50, High = 100 };
Q_ENUM(LoadPriority)
explicit ImageLoader(QObject *parent = nullptr);
static ImageLoader &sharedInstance()
{
@@ -23,7 +24,11 @@ public:
return instance;
}
void requestThumbnail(const iDescriptorDevice *device, const QString &path,
int priority, unsigned int row = 0);
unsigned int row = 0);
void
requestImageWithCallback(const iDescriptorDevice *device,
const QString &path, int priority,
std::function<void(const QPixmap &)> callback);
void cancelThumbnail(const QString &path);
bool isLoading(const QString &path);
void clear();
@@ -46,7 +51,8 @@ private slots:
private:
QThreadPool m_pool;
QSet<QString> m_pending;
QHash<QString, ImageTask *> m_pendingTasks;
QMutex m_mutex;
};
#endif // IMAGELOADER_H
+15 -9
View File
@@ -1,6 +1,7 @@
#ifndef IMAGETASK_H
#define IMAGETASK_H
#include "iDescriptor-ui.h"
#include "iDescriptor.h"
#include <QImage>
#include <QObject>
@@ -15,11 +16,10 @@ class ImageTask : public QObject, public QRunnable
Q_OBJECT
public:
ImageTask(const iDescriptorDevice *device, const QString &path,
const QSize &thumbnailSize, unsigned int row)
: m_device(device), m_path(path), m_thumbnailSize(thumbnailSize),
m_row(row)
unsigned int row, bool scale = true)
: m_device(device), m_path(path), m_isThumbnail(scale), m_row(row)
{
setAutoDelete(true);
setAutoDelete(false);
}
signals:
@@ -32,20 +32,26 @@ protected:
if (isVideo) {
QPixmap thumbnail = ImageLoader::generateVideoThumbnailFFmpeg(
m_device, m_path, m_thumbnailSize);
m_device, m_path, THUMBNAIL_SIZE);
emit finished(m_path, thumbnail, m_row);
} else {
QPixmap image = ImageLoader::loadThumbnailFromDevice(
m_device, m_path, m_thumbnailSize);
emit finished(m_path, image, m_row);
if (m_isThumbnail) {
QPixmap image = ImageLoader::loadThumbnailFromDevice(
m_device, m_path, THUMBNAIL_SIZE);
emit finished(m_path, image, m_row);
} else {
qDebug() << "Loading full image for:" << m_path;
QPixmap image = ImageLoader::loadImage(m_device, m_path);
emit finished(m_path, image, m_row);
}
}
}
private:
const iDescriptorDevice *m_device;
QString m_path;
QSize m_thumbnailSize;
bool m_isThumbnail;
unsigned int m_row;
};
+67 -48
View File
@@ -46,8 +46,7 @@ AppTabWidget::AppTabWidget(const QString &appName, const QString &bundleId,
setMinimumWidth(100);
setCursor(Qt::PointingHandCursor);
setupUI();
m_iconLabel->setPixmap(icon);
setupUI(icon);
}
void AppTabWidget::setSelected(bool selected)
@@ -56,21 +55,25 @@ void AppTabWidget::setSelected(bool selected)
updateStyles();
}
void AppTabWidget::setupUI()
void AppTabWidget::setupUI(const QPixmap &icon)
{
QHBoxLayout *mainLayout = new QHBoxLayout(this);
mainLayout->setContentsMargins(10, 8, 10, 8);
mainLayout->setSpacing(10);
// m_defaultBg = this->palette().color(QPalette::Window);
// Icon label
m_iconLabel = new QLabel();
m_iconLabel->setFixedSize(32, 32);
m_iconLabel->setScaledContents(true);
QPixmap placeholderIcon = QApplication::style()
->standardIcon(QStyle::SP_ComputerIcon)
.pixmap(32, 32);
m_iconLabel->setPixmap(placeholderIcon);
if (!icon.isNull()) {
m_iconLabel->setPixmap(icon);
} else {
QPixmap placeholderIcon = QApplication::style()
->standardIcon(QStyle::SP_ComputerIcon)
.pixmap(32, 32);
m_iconLabel->setPixmap(placeholderIcon);
}
mainLayout->addWidget(m_iconLabel);
// Text container
@@ -133,7 +136,7 @@ void AppTabWidget::updateStyles()
}
}
InstalledAppsWidget::InstalledAppsWidget(iDescriptorDevice *device,
InstalledAppsWidget::InstalledAppsWidget(const iDescriptorDevice *device,
QWidget *parent)
: QWidget(parent), m_device(device)
{
@@ -149,7 +152,14 @@ InstalledAppsWidget::InstalledAppsWidget(iDescriptorDevice *device,
fetchInstalledApps();
}
InstalledAppsWidget::~InstalledAppsWidget() { cleanupHouseArrestClients(); }
InstalledAppsWidget::~InstalledAppsWidget()
{
cleanupHouseArrestClients();
if (m_springboardClient) {
springboard_services_free(m_springboardClient);
m_springboardClient = nullptr;
}
}
void InstalledAppsWidget::setupUI()
{
@@ -269,6 +279,17 @@ void InstalledAppsWidget::fetchInstalledApps()
QFuture<QVariantMap> future = QtConcurrent::run([this]() -> QVariantMap {
QVariantMap result;
QVariantList apps;
// fetch icon from springboard service
IdeviceFfiError *err = nullptr;
if (!m_springboardClient) {
err = springboard_services_connect(m_device->provider,
&m_springboardClient);
if (err) {
qDebug() << "Error connecting to SpringBoard services:"
<< QString::fromUtf8(err->message);
idevice_error_free(err);
}
}
try {
InstallationProxyClientHandle *installationProxyClientHandle =
@@ -370,7 +391,36 @@ void InstalledAppsWidget::fetchInstalledApps()
appData["type"] = appType;
if (!appData["bundleId"].toString().isEmpty()) {
QString bundleId = appData["bundleId"].toString();
if (m_springboardClient && !bundleId.isEmpty()) {
void *out_result;
size_t out_result_len;
// FIXME: free out_result
// there is no springboard_services_free_data or
// similar, create an issue
err = springboard_services_get_icon(
m_springboardClient,
bundleId.toUtf8().constData(), &out_result,
&out_result_len);
if (err != nullptr) {
qWarning() << "Error getting icon for"
<< appData.value("bundleId") << ":"
<< QString::fromUtf8(err->message);
idevice_error_free(err);
} else {
QByteArray byteArray(
reinterpret_cast<const char *>(out_result),
static_cast<int>(out_result_len));
QImage image;
image.loadFromData(byteArray);
QPixmap pixmap = QPixmap::fromImage(image);
appData["icon"] = pixmap;
}
}
if (!bundleId.isEmpty()) {
apps.append(appData);
}
}
@@ -424,22 +474,6 @@ void InstalledAppsWidget::onAppsDataReady()
m_appTabs.clear();
m_selectedTab = nullptr;
// fetch icon from springboard service
SpringBoardServicesClientHandle *springboardClient = nullptr;
IdeviceFfiError *err = nullptr;
err = springboard_services_connect(m_device->provider, &springboardClient);
if (err != nullptr) {
qDebug() << "Error connecting to SpringBoard services:"
<< QString::fromUtf8(err->message);
} else {
/*
FIXME:springboard_services_connect takes time
MOVE EVERYTHING INTO QTCONCURRENT SO IT DOESN'T BLOCK UI
*/
qDebug() << "Successfully connected to SpringBoard services.";
}
// Create tabs for each app
for (const QVariant &appVariant : apps) {
QVariantMap appData = appVariant.toMap();
@@ -447,6 +481,8 @@ void InstalledAppsWidget::onAppsDataReady()
QString bundleId = appData.value("bundleId").toString();
QString version = appData.value("version").toString();
QString appType = appData.value("type").toString();
QPixmap icon = appData.value("icon").value<QPixmap>();
bool fileSharingEnabled =
appData.value("fileSharingEnabled", false).toBool();
@@ -465,26 +501,7 @@ void InstalledAppsWidget::onAppsDataReady()
tabName += " (System)";
}
if (springboardClient) {
void *out_result;
size_t out_result_len;
err = springboard_services_get_icon(springboardClient,
bundleId.toUtf8().constData(),
&out_result, &out_result_len);
if (err != nullptr) {
qWarning() << "Error getting icon for" << bundleId << ":"
<< QString::fromUtf8(err->message);
createAppTab(tabName, bundleId, version);
} else {
QByteArray byteArray(reinterpret_cast<const char *>(out_result),
static_cast<int>(out_result_len));
QImage image;
image.loadFromData(byteArray);
QPixmap pixmap = QPixmap::fromImage(image);
createAppTab(tabName, bundleId, version, pixmap);
}
}
createAppTab(tabName, bundleId, version, icon);
// Select first tab if available
if (!m_appTabs.isEmpty()) {
@@ -716,7 +733,8 @@ void InstalledAppsWidget::cleanupHouseArrestClients()
}
if (m_houseArrestClient) {
house_arrest_client_free(m_houseArrestClient);
// FIXME: crash
// house_arrest_client_free(m_houseArrestClient);
m_houseArrestClient = nullptr;
}
}
@@ -743,6 +761,7 @@ void InstalledAppsWidget::createLeftPanel()
searchLayout->addWidget(m_searchEdit);
// File sharing filter checkbox
// FIXME: crash when toggled
m_fileSharingCheckBox = new QCheckBox("Show Only File Sharing Enabled");
m_fileSharingCheckBox->setChecked(true);
m_fileSharingCheckBox->setStyleSheet("QCheckBox { font-size: 10px; }");
+4 -4
View File
@@ -43,7 +43,6 @@
#include <QVBoxLayout>
#include <QWidget>
// Custom App Tab Widget
class AppTabWidget : public QGroupBox
{
Q_OBJECT
@@ -75,7 +74,7 @@ protected:
};
private:
void setupUI();
void setupUI(const QPixmap &icon);
QString m_appName;
QString m_bundleId;
@@ -93,7 +92,7 @@ class InstalledAppsWidget : public QWidget
Q_OBJECT
public:
explicit InstalledAppsWidget(iDescriptorDevice *device,
explicit InstalledAppsWidget(const iDescriptorDevice *device,
QWidget *parent = nullptr);
~InstalledAppsWidget();
@@ -120,7 +119,7 @@ private:
void loadAppContainer(const QString &bundleId);
void cleanupHouseArrestClients();
iDescriptorDevice *m_device;
const iDescriptorDevice *m_device;
QHBoxLayout *m_mainLayout;
QStackedWidget *m_stackedWidget;
QWidget *m_loadingWidget;
@@ -144,6 +143,7 @@ private:
// App data storage
QList<AppTabWidget *> m_appTabs;
AppTabWidget *m_selectedTab = nullptr;
SpringBoardServicesClientHandle *m_springboardClient = nullptr;
};
#endif // INSTALLEDAPPSWIDGET_H
+49 -18
View File
@@ -36,11 +36,9 @@
#include <unistd.h>
#include "appcontext.h"
#include "settingsmanager.h"
// #include "devicemonitor.h"
// #include "Toast.h"
#include "networkdevicemanager.h"
#include "networkdeviceswidget.h"
#include "settingsmanager.h"
#include "statusballoon.h"
#include <QApplication>
#include <QDesktopServices>
@@ -51,7 +49,29 @@
#include "platform/windows/win_common.h"
#endif
using namespace IdeviceFFI;
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
void handleCallbackRecovery(const irecv_device_event_t *event, void *userData)
{
switch (event->type) {
case IRECV_DEVICE_ADD:
qDebug() << "Recovery device added: ";
QMetaObject::invokeMethod(AppContext::sharedInstance(),
"addRecoveryDevice", Qt::QueuedConnection,
Q_ARG(uint64_t, event->device_info->ecid));
break;
case IRECV_DEVICE_REMOVE:
qDebug() << "Recovery device removed: ";
QMetaObject::invokeMethod(AppContext::sharedInstance(),
"removeRecoveryDevice", Qt::QueuedConnection,
Q_ARG(uint64_t, event->device_info->ecid));
break;
default:
printf("Unhandled recovery event: %d\n", event->type);
}
}
irecv_device_event_context_t context;
#endif
MainWindow *MainWindow::sharedInstance()
{
@@ -134,6 +154,20 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
StatusBalloon *statusBalloon = StatusBalloon::sharedInstance();
statusLayout->addWidget(statusBalloon->getButton());
ZIconWidget *welcomeMenu = new ZIconWidget(
QIcon(":/resources/icons/LetsIconsHorizontalDownLeftMainLight.png"),
"Switch to Welcome Menu");
connect(welcomeMenu, &ZIconWidget::clicked, this, [this, welcomeMenu]() {
if (m_mainStackedWidget->currentIndex() != 0) {
welcomeMenu->setToolTip("Switch to Connected Devices");
return m_mainStackedWidget->setCurrentIndex(0);
}
welcomeMenu->setToolTip("Switch to Welcome Menu");
m_mainStackedWidget->setCurrentIndex(1);
});
statusLayout->addWidget(welcomeMenu);
statusLayout->addStretch(1);
statusLayout->setContentsMargins(0, 0, 0, 0);
@@ -174,19 +208,17 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
}
#endif
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
// irecv_error_t res_recovery =
// irecv_device_event_subscribe(&context, handleCallbackRecovery,
// nullptr);
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
irecv_error_t res_recovery =
irecv_device_event_subscribe(&context, handleCallbackRecovery, nullptr);
// if (res_recovery != IRECV_E_SUCCESS) {
// qDebug() << "ERROR: Unable to subscribe to recovery device
// events. "
// "Error code:"
// << res_recovery;
// }
// qDebug() << "Subscribed to recovery device events successfully.";
// #endif
if (res_recovery != IRECV_E_SUCCESS) {
qDebug() << "ERROR: Unable to subscribe to recovery device events. "
"Error code:"
<< res_recovery;
}
qDebug() << "Subscribed to recovery device events successfully.";
#endif
// idevice_error_t res = idevice_event_subscribe(handleCallback,
// nullptr); if (res != IDEVICE_E_SUCCESS) {
@@ -195,7 +227,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
// << res;
// }
// qDebug() << "Subscribed to device events successfully.";
// createMenus();
createMenus();
// UpdateProcedure updateProcedure;
// bool packageManagerManaged = false;
@@ -285,7 +317,6 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
// m_updater->checkForUpdates();
// });
// Usage in main thread:
m_deviceMonitor = new DeviceMonitorThread(this);
connect(
m_deviceMonitor, &DeviceMonitorThread::deviceEvent, this,
+54 -102
View File
@@ -47,20 +47,12 @@
#include <QtConcurrent/QtConcurrent>
#include <QtGlobal>
MediaPreviewDialog::MediaPreviewDialog(iDescriptorDevice *device,
MediaPreviewDialog::MediaPreviewDialog(const iDescriptorDevice *device,
AfcClientHandle *afcClient,
const QString &filePath, QWidget *parent)
: QDialog(parent), m_device(device), m_filePath(filePath),
m_isVideo(isVideoFile(filePath)), m_mainLayout(nullptr),
m_controlsLayout(nullptr), m_imageView(nullptr), m_imageScene(nullptr),
m_pixmapItem(nullptr), m_videoWidget(nullptr), m_mediaPlayer(nullptr),
m_videoControlsLayout(nullptr), m_playPauseBtn(nullptr),
m_stopBtn(nullptr), m_repeatBtn(nullptr), m_timelineSlider(nullptr),
m_timeLabel(nullptr), m_volumeSlider(nullptr), m_volumeLabel(nullptr),
m_progressTimer(nullptr), m_loadingLabel(nullptr), m_statusLabel(nullptr),
m_zoomInBtn(nullptr), m_zoomOutBtn(nullptr), m_zoomResetBtn(nullptr),
m_fitToWindowBtn(nullptr), m_zoomFactor(1.0), m_isRepeatEnabled(true),
m_isDraggingTimeline(false), m_videoDuration(0), m_afcClient(afcClient)
m_isVideo(iDescriptor::Utils::isVideoFile(filePath)),
m_afcClient(afcClient)
{
setWindowTitle(QFileInfo(filePath).fileName() + " - iDescriptor");
@@ -97,28 +89,14 @@ void MediaPreviewDialog::setupUI()
m_mainLayout->setContentsMargins(0, 0, 0, 0);
m_mainLayout->setSpacing(0);
// Loading label
m_loadingLabel = new QLabel("Loading...", this);
m_loadingLabel->setAlignment(Qt::AlignCenter);
m_loadingLabel->setStyleSheet(
"QLabel { font-size: 16px; color: #666; padding: 20px; }");
m_mainLayout->addWidget(m_loadingLabel);
m_loadingWidget = new ZLoadingWidget();
m_mainLayout->addWidget(m_loadingWidget);
if (m_isVideo) {
setupVideoView();
} else {
setupImageView();
}
// Status bar
// more margin because of border radius on macOS
m_statusLabel = new QLabel(this);
#ifdef Q_OS_MAC
m_statusLabel->setStyleSheet("QLabel { margin-left: 15px; }");
#else
m_statusLabel->setStyleSheet("QLabel { margin-left: 5px; }");
#endif
m_mainLayout->addWidget(m_statusLabel);
}
void MediaPreviewDialog::setupImageView()
@@ -129,7 +107,7 @@ void MediaPreviewDialog::setupImageView()
m_imageView->setDragMode(QGraphicsView::ScrollHandDrag);
m_imageView->setRenderHint(QPainter::Antialiasing);
m_imageView->setVisible(false);
m_mainLayout->addWidget(m_imageView);
m_loadingWidget->setupContentWidget(m_imageView);
// Controls layout
m_controlsLayout = new QHBoxLayout();
@@ -166,7 +144,7 @@ void MediaPreviewDialog::setupVideoView()
m_videoWidget->setVisible(false);
m_videoWidget->setSizePolicy(QSizePolicy::Expanding,
QSizePolicy::Expanding);
m_mainLayout->addWidget(m_videoWidget, 1); // Give it stretch factor 1
m_loadingWidget->setupContentWidget(m_videoWidget);
// Media player
m_mediaPlayer = new QMediaPlayer(this);
@@ -189,13 +167,10 @@ void MediaPreviewDialog::setupVideoView()
&MediaPreviewDialog::onMediaPlayerStateChanged);
connect(m_mediaPlayer, &QMediaPlayer::errorOccurred, this,
[this](QMediaPlayer::Error error, const QString &errorString) {
qDebug() << "MediaPlayer Error:" << error << errorString;
m_statusLabel->setText("Error: " + errorString);
m_loadingLabel->setText("Error: " + errorString);
m_loadingLabel->show();
m_videoWidget->hide();
m_loadingWidget->showError("Error playing video: " +
errorString);
});
// Setup progress timer for smooth updates
m_progressTimer = new QTimer(this);
connect(m_progressTimer, &QTimer::timeout, this,
&MediaPreviewDialog::updateVideoProgress);
@@ -211,22 +186,17 @@ void MediaPreviewDialog::loadMedia()
void MediaPreviewDialog::loadImage()
{
auto future = QtConcurrent::run(
[this]() { return ImageLoader::loadImage(m_device, m_filePath); });
auto *watcher = new QFutureWatcher<QPixmap>(this);
connect(watcher, &QFutureWatcher<QPixmap>::finished, this,
[this, watcher]() {
QPixmap pixmap = watcher->result();
if (!pixmap.isNull()) {
m_originalPixmap = pixmap;
onImageLoaded();
} else {
onImageLoadFailed();
}
watcher->deleteLater();
});
watcher->setFuture(future);
auto callback = [this](const QPixmap &pixmap) {
if (!pixmap.isNull()) {
onImageLoaded(pixmap);
} else {
onImageLoadFailed();
}
};
// 99999 is so that it gets the highest priority in the queue
unsigned int priority = 99999;
ImageLoader::sharedInstance().requestImageWithCallback(m_device, m_filePath,
priority, callback);
}
void MediaPreviewDialog::loadVideo()
@@ -238,20 +208,22 @@ void MediaPreviewDialog::loadVideo()
m_device, m_afcClient, m_filePath);
qDebug() << "Streaming video from URL:" << streamUrl;
if (streamUrl.isEmpty()) {
m_statusLabel->setText("Failed to start video stream");
// TODO: connect to retry signal to attempt restarting the stream
m_loadingWidget->showError("Failed to start video stream");
return;
}
m_mediaPlayer->setSource(streamUrl);
m_mediaPlayer->play();
m_loadingLabel->hide();
m_statusLabel->setText(
QString("Playing: %1").arg(QFileInfo(m_filePath).fileName()));
m_loadingWidget->stop();
// m_statusLabel->setText(
// QString("Playing: %1").arg(QFileInfo(m_filePath).fileName()));
}
void MediaPreviewDialog::onImageLoaded()
void MediaPreviewDialog::onImageLoaded(const QPixmap &pixmap)
{
m_loadingLabel->hide();
m_originalPixmap = pixmap;
m_imageView->setVisible(true);
// Add pixmap to scene
@@ -259,19 +231,22 @@ void MediaPreviewDialog::onImageLoaded()
m_imageScene->setSceneRect(m_originalPixmap.rect());
// Fit to window initially
fitToWindow();
// TODO:why QTimer::singleShot is required here ?
// fitToWindow();
QTimer::singleShot(0, this, &MediaPreviewDialog::fitToWindow);
m_loadingWidget->stop();
// Update status
m_statusLabel->setText(QString("Image: %1 (%2x%3)")
.arg(QFileInfo(m_filePath).fileName())
.arg(m_originalPixmap.width())
.arg(m_originalPixmap.height()));
// m_statusLabel->setText(QString("Image: %1 (%2x%3)")
// .arg(QFileInfo(m_filePath).fileName())
// .arg(m_originalPixmap.width())
// .arg(m_originalPixmap.height()));
}
void MediaPreviewDialog::onImageLoadFailed()
{
m_loadingLabel->setText("Failed to load image");
m_statusLabel->setText("Error loading image");
// TODO: connect to retry signal to attempt reloading the image
m_loadingWidget->showError("Failed to load image");
// m_statusLabel->setText("Error loading image");
}
void MediaPreviewDialog::wheelEvent(QWheelEvent *event)
@@ -425,35 +400,17 @@ void MediaPreviewDialog::zoom(double factor)
updateZoomStatus();
}
// void MediaPreviewDialog::updateZoomStatus()
// {
// if (!m_isVideo && !m_originalPixmap.isNull()) {
// m_statusLabel->setText(QString("Image: %1 (%2x%3) - Zoom: %4%")
// .arg(QFileInfo(m_filePath).fileName())
// .arg(m_originalPixmap.width())
// .arg(m_originalPixmap.height())
// .arg(qRound(m_zoomFactor * 100)));
// }
// }
void MediaPreviewDialog::updateZoomStatus()
{
if (!m_isVideo && !m_originalPixmap.isNull()) {
m_statusLabel->setText(QString("Image: %1 (%2x%3) - Zoom: %4%")
.arg(QFileInfo(m_filePath).fileName())
.arg(m_originalPixmap.width())
.arg(m_originalPixmap.height())
.arg(qRound(m_zoomFactor * 100)));
// m_statusLabel->setText(QString("Image: %1 (%2x%3) - Zoom: %4%")
// .arg(QFileInfo(m_filePath).fileName())
// .arg(m_originalPixmap.width())
// .arg(m_originalPixmap.height())
// .arg(qRound(m_zoomFactor * 100)));
}
}
bool MediaPreviewDialog::isVideoFile(const QString &filePath) const
{
const QString lower = filePath.toLower();
return lower.endsWith(".mov") || lower.endsWith(".mp4") ||
lower.endsWith(".avi") || lower.endsWith(".m4v");
}
void MediaPreviewDialog::setupVideoControls()
{
// Create video controls layout
@@ -525,7 +482,7 @@ void MediaPreviewDialog::setupVideoControls()
m_videoControlsLayout->addWidget(m_playPauseBtn);
m_videoControlsLayout->addWidget(m_stopBtn);
m_videoControlsLayout->addWidget(m_repeatBtn);
m_videoControlsLayout->addWidget(m_timelineSlider, 1); // Stretch factor 1
m_videoControlsLayout->addWidget(m_timelineSlider, 1);
m_videoControlsLayout->addWidget(m_timeLabel);
m_videoControlsLayout->addWidget(m_volumeLabel);
m_videoControlsLayout->addWidget(m_volumeSlider);
@@ -563,8 +520,6 @@ void MediaPreviewDialog::onRepeatToggled(bool enabled)
m_repeatBtn->setStyleSheet(
enabled ? "QPushButton { background-color: #4CAF50; color: white; }"
: "");
qDebug() << "Repeat mode:" << (enabled ? "ON" : "OFF");
}
void MediaPreviewDialog::onTimelinePressed()
@@ -665,9 +620,9 @@ void MediaPreviewDialog::onMediaPlayerDurationChanged(qint64 duration)
if (duration > 0) {
QString durationStr;
formatTime(duration, durationStr);
m_statusLabel->setText(QString("Video: %1 - Duration: %2")
.arg(QFileInfo(m_filePath).fileName())
.arg(durationStr));
// m_statusLabel->setText(QString("Video: %1 - Duration: %2")
// .arg(QFileInfo(m_filePath).fileName())
// .arg(durationStr));
}
}
@@ -742,10 +697,11 @@ void MediaPreviewDialog::onVolumeChanged(int value)
}
}
#ifdef __APPLE__
bool MediaPreviewDialog::event(QEvent *event)
{
// FIXME: lets implement this on all dialogs
// catch platform Close (Cmd+W on macOS)
// TODO: implement this on all dialogs
// catch platform close (Cmd+W on macOS)
if (event->type() == QEvent::ShortcutOverride) {
if (auto *ke = dynamic_cast<QKeyEvent *>(event)) {
const Qt::KeyboardModifiers mods = ke->modifiers();
@@ -755,12 +711,8 @@ bool MediaPreviewDialog::event(QEvent *event)
close();
return true;
}
if (ke->key() == Qt::Key_Escape) {
ke->accept();
close();
return true;
}
}
}
return QDialog::event(event);
}
}
#endif
+12 -23
View File
@@ -22,6 +22,7 @@
#include "iDescriptor-ui.h"
#include "iDescriptor.h"
#include "zloadingwidget.h"
#include <QCoreApplication>
#include <QDialog>
#include <QGraphicsPixmapItem>
@@ -37,21 +38,12 @@
#include <QVideoWidget>
#include <QtGlobal>
/**
* @brief A dialog for previewing images and videos from iOS devices
*
* Features:
* - Image viewing with zoom and pan using QGraphicsView
* - Video streaming with timeline scrubbing support
* - Asynchronous loading from device
* - Proper memory management
*/
class MediaPreviewDialog : public QDialog
{
Q_OBJECT
public:
explicit MediaPreviewDialog(iDescriptorDevice *device,
explicit MediaPreviewDialog(const iDescriptorDevice *device,
AfcClientHandle *afcClient,
const QString &filePath,
QWidget *parent = nullptr);
@@ -61,10 +53,11 @@ protected:
void wheelEvent(QWheelEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
bool event(QEvent *event) override; // handle ShortcutOverride
#ifdef __APPLE__
bool event(QEvent *event) override;
#endif
private slots:
void onImageLoaded();
void onImageLoaded(const QPixmap &pixmap);
void onImageLoadFailed();
void zoomIn();
void zoomOut();
@@ -96,10 +89,9 @@ private:
void updateZoomStatus();
void updateVideoTimeDisplay();
void formatTime(qint64 milliseconds, QString &timeString);
bool isVideoFile(const QString &filePath) const;
// Core data
iDescriptorDevice *m_device;
const iDescriptorDevice *m_device;
QString m_filePath;
bool m_isVideo;
@@ -127,10 +119,6 @@ private:
QLabel *m_volumeLabel;
QTimer *m_progressTimer;
// Common components
QLabel *m_loadingLabel;
QLabel *m_statusLabel;
// Control buttons
QPushButton *m_zoomInBtn;
QPushButton *m_zoomOutBtn;
@@ -138,15 +126,16 @@ private:
QPushButton *m_fitToWindowBtn;
// State
double m_zoomFactor;
double m_zoomFactor = 1.0;
QPixmap m_originalPixmap;
// Video state
bool m_isRepeatEnabled;
bool m_isDraggingTimeline;
qint64 m_videoDuration;
bool m_isRepeatEnabled = true;
bool m_isDraggingTimeline = false;
qint64 m_videoDuration = 0;
AfcClientHandle *m_afcClient;
ZLoadingWidget *m_loadingWidget;
};
#endif // MEDIAPREVIEWDIALOG_H
+22 -29
View File
@@ -30,7 +30,7 @@
#include <QTimer>
#include <memory>
MediaStreamer::MediaStreamer(iDescriptorDevice *device,
MediaStreamer::MediaStreamer(const iDescriptorDevice *device,
AfcClientHandle *afcClient,
const QString &filePath, QObject *parent)
: QTcpServer(parent), m_device(device), m_afcClient(afcClient),
@@ -63,10 +63,10 @@ QUrl MediaStreamer::getUrl() const
if (!isListening()) {
return QUrl();
}
// todo pass folder/filename
return QUrl(QString("http://127.0.0.1:%1/%2")
.arg(serverPort())
.arg(QFileInfo(m_filePath).fileName()));
.arg(QUrl::toPercentEncoding(m_filePath)));
}
bool MediaStreamer::isListening() const { return QTcpServer::isListening(); }
@@ -235,7 +235,6 @@ void MediaStreamer::handleRequest(QTcpSocket *socket,
response += "\r\n";
socket->write(response);
// Remove blocking call - let Qt handle when bytes are actually written
// For HEAD requests, don't send body
if (request.method == "HEAD") {
@@ -259,14 +258,12 @@ void MediaStreamer::sendErrorResponse(QTcpSocket *socket, int statusCode,
.toUtf8();
socket->write(response);
// Remove blocking call
socket->disconnectFromHost();
}
void MediaStreamer::streamFileRange(QTcpSocket *socket, qint64 startByte,
qint64 endByte)
{
// Create a new streaming context for this request
StreamingContext *context = new StreamingContext();
context->socket = socket;
context->device = m_device;
@@ -276,12 +273,10 @@ void MediaStreamer::streamFileRange(QTcpSocket *socket, qint64 startByte,
context->bytesRemaining = endByte - startByte + 1;
context->afcHandle = nullptr;
qDebug() << "m_filepath" << m_filePath;
const QByteArray pathBytes = m_filePath.toUtf8();
IdeviceFfiError *err_open = // Use distinct variable name
ServiceManager::safeAfcFileOpen(m_device, pathBytes.constData(),
AfcRdOnly, &context->afcHandle);
IdeviceFfiError *err_open = ServiceManager::safeAfcFileOpen(
m_device, pathBytes.constData(), AfcRdOnly, &context->afcHandle);
if (err_open || context->afcHandle == 0) {
qWarning() << "Failed to open file on device:" << m_filePath;
@@ -290,15 +285,16 @@ void MediaStreamer::streamFileRange(QTcpSocket *socket, qint64 startByte,
return;
}
// Seek to start position if needed
/* TODO: can it be optimized by doing
SEEK_END 2 /* Seek from end of file. ???
*/
if (startByte > 0) {
// FIXME: m_afcClient must be passed as alt afc
IdeviceFfiError *seek_err = ServiceManager::safeAfcFileSeek(
m_device, context->afcHandle, startByte, SEEK_SET);
if (seek_err) {
qWarning() << "Failed to seek in file:" << m_filePath;
// FIXME: m_afcClient must be passed as alt afc
IdeviceFfiError *err =
ServiceManager::safeAfcFileClose(m_device, context->afcHandle);
if (err) {
@@ -310,9 +306,6 @@ void MediaStreamer::streamFileRange(QTcpSocket *socket, qint64 startByte,
}
}
qDebug() << "Starting non-blocking stream for range" << startByte << "-"
<< endByte << "(" << context->bytesRemaining << "bytes)";
// Store context as socket property for cleanup
socket->setProperty("streamingContext",
QVariant::fromValue(static_cast<void *>(context)));
@@ -321,15 +314,16 @@ void MediaStreamer::streamFileRange(QTcpSocket *socket, qint64 startByte,
connect(socket, &QTcpSocket::bytesWritten, this,
[this, context](qint64 bytes) {
Q_UNUSED(bytes)
// Check if context is still valid
QTcpSocket *senderSocket = qobject_cast<QTcpSocket *>(sender());
if (!senderSocket ||
senderSocket->property("streamingContext").isNull()) {
return; // Context already cleaned up
return;
}
// Continue streaming when socket buffer has space
if (context->socket->bytesToWrite() <
32768) { // Keep buffer below 32KB
if (context->socket->bytesToWrite() < 50000) {
streamNextChunk(context);
}
});
@@ -339,7 +333,7 @@ void MediaStreamer::streamFileRange(QTcpSocket *socket, qint64 startByte,
QTcpSocket *senderSocket = qobject_cast<QTcpSocket *>(sender());
if (!senderSocket ||
senderSocket->property("streamingContext").isNull()) {
return; // Already cleaned up
return;
}
cleanupStreamingContext(context);
});
@@ -421,7 +415,7 @@ void MediaStreamer::streamNextChunk(StreamingContext *context)
return;
}
const int CHUNK_SIZE = 256 * 1024;
const int CHUNK_SIZE = 64 * 1024;
const uint32_t bytesToRead = static_cast<uint32_t>(
qMin(static_cast<qint64>(CHUNK_SIZE), context->bytesRemaining));
@@ -441,9 +435,10 @@ void MediaStreamer::streamNextChunk(StreamingContext *context)
if (bytesRead == 0 && bytesToRead > 0) {
qWarning() << "AFC read returned 0 bytes but expected to read:"
<< bytesToRead;
if (chunkData) {
afc_file_read_data_free(chunkData, 0);
}
// FIXME: in such situation, freeing shouldn't be safe ?
// if (chunkData) {
// afc_file_read_data_free(chunkData, 0);
// }
cleanupStreamingContext(context);
return;
}
@@ -475,7 +470,7 @@ void MediaStreamer::streamNextChunk(StreamingContext *context)
return;
}
if (context->socket->bytesToWrite() >= 1024 * 1024) {
if (context->socket->bytesToWrite() >= 50000) {
// Wait for bytesWritten signal
return;
} else {
@@ -505,9 +500,7 @@ void MediaStreamer::cleanupStreamingContext(StreamingContext *context)
}
if (context->afcHandle != 0) {
// FIXME: m_afcClient must be passed as alt afc
ServiceManager::safeAfcFileClose(context->device, context->afcHandle
/*m_afcClient*/);
ServiceManager::safeAfcFileClose(context->device, context->afcHandle);
context->afcHandle = 0;
}
@@ -517,7 +510,7 @@ void MediaStreamer::cleanupStreamingContext(StreamingContext *context)
disconnect(context->socket, &QTcpSocket::disconnected, this, nullptr);
context->socket->disconnectFromHost();
context->socket = nullptr; // Prevent further access
context->socket = nullptr;
}
qDebug() << "Streaming context cleaned up for"
+3 -24
View File
@@ -30,38 +30,17 @@ QT_BEGIN_NAMESPACE
class QTcpSocket;
QT_END_NAMESPACE
/**
* @brief A lightweight HTTP server for streaming media files from iOS devices
*
* This class implements an HTTP server that supports:
* - Basic HTTP GET requests
* - HTTP Range requests for video scrubbing
* - Streaming from AFC (Apple File Conduit) without loading entire file into
* memory
* - Thread-safe operation
*
* The server automatically shuts down when the client disconnects.
*/
class MediaStreamer : public QTcpServer
{
Q_OBJECT
public:
explicit MediaStreamer(iDescriptorDevice *device,
explicit MediaStreamer(const iDescriptorDevice *device,
AfcClientHandle *afcClient, const QString &filePath,
QObject *parent = nullptr);
~MediaStreamer();
/**
* @brief Get the URL that clients should use to connect to this server
* @return URL in format http://127.0.0.1:port
*/
QUrl getUrl() const;
/**
* @brief Check if the server started successfully
* @return true if server is listening, false otherwise
*/
bool isListening() const;
protected:
@@ -83,7 +62,7 @@ private:
struct StreamingContext {
QTcpSocket *socket;
iDescriptorDevice *device;
const iDescriptorDevice *device;
QString filePath;
qint64 startByte;
qint64 endByte;
@@ -102,7 +81,7 @@ private:
QString getMimeType() const;
// Core data
iDescriptorDevice *m_device;
const iDescriptorDevice *m_device;
QString m_filePath;
// File info cache
+2 -2
View File
@@ -30,7 +30,7 @@ MediaStreamerManager *MediaStreamerManager::sharedInstance()
return &instance;
}
QUrl MediaStreamerManager::getStreamUrl(iDescriptorDevice *device,
QUrl MediaStreamerManager::getStreamUrl(const iDescriptorDevice *device,
AfcClientHandle *afcClient,
const QString &filePath)
{
@@ -65,7 +65,7 @@ QUrl MediaStreamerManager::getStreamUrl(iDescriptorDevice *device,
return QUrl();
}
// Store the streamer info
// FIXME: device pointer can become dangling if device is disconnected
StreamerInfo info;
info.streamer = streamer;
info.device = device;
+3 -27
View File
@@ -27,40 +27,16 @@
#include <QObject>
#include <QUrl>
/**
* @brief Singleton manager for MediaStreamer instances
*
* This class manages MediaStreamer instances to avoid creating multiple
* streamers for the same file. It automatically cleans up unused streamers
* and provides thread-safe access.
*/
class MediaStreamerManager
{
public:
/**
* @brief Get the singleton instance
* @return The MediaStreamerManager instance
*/
static MediaStreamerManager *sharedInstance();
/**
* @brief Get or create a streamer for the specified file
* @param device The iOS device
* @param filePath The file path on the device
* @return URL to stream the file, or empty URL if failed
*/
QUrl getStreamUrl(iDescriptorDevice *device, AfcClientHandle *afcClient,
const QString &filePath);
QUrl getStreamUrl(const iDescriptorDevice *device,
AfcClientHandle *afcClient, const QString &filePath);
/**
* @brief Release a streamer for the specified file
* @param filePath The file path to release
*/
void releaseStreamer(const QString &filePath);
/**
* @brief Clean up all inactive streamers
*/
void cleanup();
private:
@@ -69,7 +45,7 @@ private:
private:
struct StreamerInfo {
MediaStreamer *streamer;
iDescriptorDevice *device;
const iDescriptorDevice *device;
int refCount;
};
+48 -6
View File
@@ -130,6 +130,17 @@ void NetworkDeviceCard::noPairingFile()
});
}
void NetworkDeviceCard::connected()
{
m_connectButton->setText("Connected");
m_connectButton->setEnabled(false);
QTimer::singleShot(10000, this, [this]() {
m_connectButton->setText("Connect");
m_connectButton->setEnabled(true);
});
}
void NetworkDeviceCard::initStarted()
{
m_connectButton->setText("Connecting...");
@@ -168,6 +179,10 @@ NetworkDevicesToConnectWidget::NetworkDevicesToConnectWidget(QWidget *parent)
&NetworkDevicesToConnectWidget::onDeviceInitFailed);
connect(AppContext::sharedInstance(), &AppContext::initStarted, this,
&NetworkDevicesToConnectWidget::onDeviceInitStarted);
connect(AppContext::sharedInstance(), &AppContext::deviceAdded, this,
&NetworkDevicesToConnectWidget::onDeviceAdded);
connect(AppContext::sharedInstance(), &AppContext::deviceAlreadyExists,
this, &NetworkDevicesToConnectWidget::onDeviceAlreadyExists);
}
NetworkDevicesToConnectWidget::~NetworkDevicesToConnectWidget()
@@ -313,20 +328,47 @@ void NetworkDevicesToConnectWidget::onNoPairingFileForWirelessDevice(
}
// udid or mac address
void NetworkDevicesToConnectWidget::onDeviceInitFailed(const QString &udid)
void NetworkDevicesToConnectWidget::onDeviceInitFailed(const QString &uniq)
{
NetworkDeviceCard *deviceCard = m_deviceCards[udid];
NetworkDeviceCard *deviceCard = m_deviceCards[uniq];
if (deviceCard) {
qDebug() << "Calling failed() on device card for" << udid;
qDebug() << "Calling failed() on device card for" << uniq;
deviceCard->failed();
}
}
void NetworkDevicesToConnectWidget::onDeviceInitStarted(const QString &udid)
void NetworkDevicesToConnectWidget::onDeviceInitStarted(const QString &uniq)
{
NetworkDeviceCard *deviceCard = m_deviceCards[udid];
NetworkDeviceCard *deviceCard = m_deviceCards[uniq];
if (deviceCard) {
qDebug() << "Calling initStarted() on device card for" << udid;
qDebug() << "Calling initStarted() on device card for" << uniq;
deviceCard->initStarted();
}
}
void NetworkDevicesToConnectWidget::onDeviceAdded(
const iDescriptorDevice *device)
{
NetworkDeviceCard *deviceCard = m_deviceCards[QString::fromStdString(
device->deviceInfo.wifiMacAddress)];
if (deviceCard) {
qDebug() << "Calling connected() on device card for"
<< QString::fromStdString(device->deviceInfo.wifiMacAddress);
deviceCard->connected();
return;
}
qDebug() << "No device card found for"
<< QString::fromStdString(device->deviceInfo.wifiMacAddress);
}
void NetworkDevicesToConnectWidget::onDeviceAlreadyExists(
const iDescriptor::Uniq &uniq)
{
NetworkDeviceCard *deviceCard = m_deviceCards[QString(uniq.get())];
if (deviceCard) {
qDebug() << "Calling connected() on device card for" << uniq.get();
deviceCard->connected();
return;
}
qDebug() << "No device card found for" << uniq.get();
}
+3
View File
@@ -49,6 +49,7 @@ public:
void failed();
void noPairingFile();
void initStarted();
void connected();
};
class NetworkDevicesToConnectWidget : public QWidget
@@ -65,6 +66,8 @@ private slots:
void onNoPairingFileForWirelessDevice(const QString &macAddress);
void onDeviceInitFailed(const QString &udid);
void onDeviceInitStarted(const QString &udid);
void onDeviceAdded(const iDescriptorDevice *device);
void onDeviceAlreadyExists(const iDescriptor::Uniq &uniq);
private:
void setupUI();
+5 -4
View File
@@ -184,7 +184,7 @@ void OpenSSHTerminalWidget::clearDeviceButtons()
}
}
void OpenSSHTerminalWidget::addWiredDevice(iDescriptorDevice *device)
void OpenSSHTerminalWidget::addWiredDevice(const iDescriptorDevice *device)
{
QString deviceName = QString::fromStdString(device->deviceInfo.deviceName);
QString udid = QString::fromStdString(device->udid);
@@ -192,8 +192,9 @@ void OpenSSHTerminalWidget::addWiredDevice(iDescriptorDevice *device)
QRadioButton *radioButton = new QRadioButton(displayText);
radioButton->setProperty("deviceType", "wired");
radioButton->setProperty("devicePointer",
QVariant::fromValue(static_cast<void *>(device)));
radioButton->setProperty(
"devicePointer",
QVariant::fromValue(static_cast<const void *>(device)));
radioButton->setProperty("udid", udid);
m_deviceButtonGroup->addButton(radioButton);
@@ -214,7 +215,7 @@ void OpenSSHTerminalWidget::addWirelessDevice(const NetworkDevice &device)
m_wirelessDevicesLayout->addWidget(radioButton);
}
void OpenSSHTerminalWidget::onWiredDeviceAdded(iDescriptorDevice *device)
void OpenSSHTerminalWidget::onWiredDeviceAdded(const iDescriptorDevice *device)
{
addWiredDevice(device);
}
+3 -4
View File
@@ -47,7 +47,7 @@ public:
~OpenSSHTerminalWidget();
private slots:
void onOpenSSHTerminal();
void onWiredDeviceAdded(iDescriptorDevice *device);
void onWiredDeviceAdded(const iDescriptorDevice *device);
void onWiredDeviceRemoved(const std::string &udid);
void onWirelessDeviceAdded(const NetworkDevice &device);
void onWirelessDeviceRemoved(const QString &deviceName);
@@ -57,7 +57,7 @@ private:
void setupDeviceSelectionUI(QVBoxLayout *layout);
void updateDeviceList();
void clearDeviceButtons();
void addWiredDevice(iDescriptorDevice *device);
void addWiredDevice(const iDescriptorDevice *device);
void addWirelessDevice(const NetworkDevice &device);
void resetSelection();
@@ -82,8 +82,7 @@ private:
iDescriptorDevice *m_selectedWiredDevice = nullptr;
NetworkDevice m_selectedNetworkDevice;
// Legacy device pointer (kept for compatibility)
iDescriptorDevice *m_device = nullptr;
const iDescriptorDevice *m_device = nullptr;
// SSH components
ssh_session m_sshSession;
+3 -15
View File
@@ -31,11 +31,8 @@
#include <QUrl>
#include <qrencode.h>
PhotoImportDialog::PhotoImportDialog(const QStringList &files,
bool hasDirectories, QWidget *parent)
: QDialog(parent), selectedFiles(files),
containsDirectories(hasDirectories), m_httpServer(nullptr),
m_mediaPlayer(nullptr)
PhotoImportDialog::PhotoImportDialog(const QStringList &files, QWidget *parent)
: QDialog(parent), selectedFiles(files)
{
setupUI();
setModal(true);
@@ -59,16 +56,6 @@ void PhotoImportDialog::setupUI()
{
QVBoxLayout *mainLayout = new QVBoxLayout(this);
// Warning label for directories
if (containsDirectories) {
warningLabel =
new QLabel("⚠️ Warning: Selected items contain directories. All "
"gallery-compatible files will be included.",
this);
warningLabel->setWordWrap(true);
mainLayout->addWidget(warningLabel);
}
// File list
QLabel *listLabel = new QLabel(
QString("Files to be served (%1 items):").arg(selectedFiles.size()),
@@ -230,6 +217,7 @@ void PhotoImportDialog::onServerStarted()
void PhotoImportDialog::onDownloadProgress(const QString &fileName,
int bytesDownloaded, int totalBytes)
{
// TODO: bring in a progress bar each item
m_progressLabel->setText(QString("Downloaded: %1 (%2 KB)")
.arg(fileName)
.arg(bytesDownloaded / 1024));
+3 -5
View File
@@ -25,20 +25,20 @@
#include <QHBoxLayout>
#include <QLabel>
#include <QListWidget>
#include <QMediaPlayer>
#include <QProgressBar>
#include <QPushButton>
#include <QStackedWidget>
#include <QStringList>
#include <QVBoxLayout>
#include <QStackedWidget>
#include <QVideoWidget>
#include <QMediaPlayer>
class PhotoImportDialog : public QDialog
{
Q_OBJECT
public:
explicit PhotoImportDialog(const QStringList &files, bool hasDirectories,
explicit PhotoImportDialog(const QStringList &files,
QWidget *parent = nullptr);
~PhotoImportDialog();
@@ -52,10 +52,8 @@ private slots:
private:
QStringList selectedFiles;
bool containsDirectories;
QListWidget *fileList;
QLabel *warningLabel;
QLabel *qrCodeLabel;
QStackedWidget *m_instructionStack;
QLabel *m_instructionLabel;
+67 -48
View File
@@ -34,40 +34,27 @@
#include <QTimer>
#include <QVideoFrame>
#include <QVideoSink>
#include <QtConcurrent/QtConcurrent>
PhotoModel::PhotoModel(iDescriptorDevice *device, FilterType filterType,
PhotoModel::PhotoModel(const iDescriptorDevice *device, FilterType filterType,
QObject *parent)
: QAbstractListModel(parent), m_device(device), m_thumbnailSize(120, 120),
m_sortOrder(NewestFirst), m_filterType(filterType)
: QAbstractListModel(parent), m_device(device), m_sortOrder(NewestFirst),
m_filterType(filterType)
{
connect(&ImageLoader::sharedInstance(), &ImageLoader::thumbnailReady, this,
&PhotoModel::onThumbnailReady);
}
void PhotoModel::clear()
{
blockSignals(true);
// // Clean up any active watchers
// for (auto *watcher : m_activeLoaders.values()) {
// if (watcher) {
// watcher->disconnect();
// watcher->cancel();
// // watcher->waitForFinished();
// watcher->deleteLater();
// }
// }
// m_activeLoaders.clear();
// m_loadingPaths.clear();
// m_thumbnailCache.clear();
QMutexLocker locker(&m_mutex);
disconnect(&ImageLoader::sharedInstance(), &ImageLoader::thumbnailReady,
this, &PhotoModel::onThumbnailReady);
beginResetModel();
m_photos.clear();
m_allPhotos.clear();
endResetModel();
blockSignals(false);
qDebug() << "Cleared PhotoModel data";
ImageLoader::sharedInstance().clear();
}
PhotoModel::~PhotoModel()
@@ -112,8 +99,7 @@ QVariant PhotoModel::data(const QModelIndex &index, int role) const
}
}
imgloader.requestThumbnail(m_device, info.filePath, index.row(),
index.row());
imgloader.requestThumbnail(m_device, info.filePath, index.row());
if (iDescriptor::Utils::isVideoFile(info.fileName)) {
return QIcon(":/resources/icons/video-x-generic.png");
@@ -134,19 +120,33 @@ QVariant PhotoModel::data(const QModelIndex &index, int role) const
void PhotoModel::onThumbnailReady(const QString &path, const QPixmap &pixmap,
unsigned int row)
{
if (m_photos[row].filePath == path) {
QModelIndex idx = createIndex(row, 0);
emit dataChanged(idx, idx, {Qt::DecorationRole});
// check bounds
if (row < m_photos.size()) {
const PhotoInfo &photo = m_photos.at(row);
if (photo.filePath == path) {
QModelIndex idx = createIndex(row, 0);
emit dataChanged(idx, idx, {Qt::DecorationRole});
}
} else {
// FIXME: happens when we filter down to videos only
qDebug() << "Out of bounds in PhotoModel::onThumbnailReady";
}
}
void PhotoModel::populatePhotoPaths()
bool isTimeoutError(IdeviceFfiError *err)
{
// TODO:beginResetModel called on PhotoModel(0x600002d12a40) without calling
// endResetModel first
return err && err->code == TimeoutErrorCode;
}
bool PhotoModel::populatePhotoPaths()
{
// FIXME:DEADLOCK?
// QMutexLocker locker(&m_mutex);
connect(&ImageLoader::sharedInstance(), &ImageLoader::thumbnailReady, this,
&PhotoModel::onThumbnailReady);
if (m_albumPath.isEmpty()) {
qDebug() << "No album path set, skipping population";
return;
return false;
}
m_allPhotos.clear();
@@ -160,9 +160,13 @@ void PhotoModel::populatePhotoPaths()
ServiceManager::safeAfcGetFileInfo(m_device, albumPathCStr, &albumInfo);
if (err) {
qDebug() << "Album path does not exist or cannot be accessed:"
<< m_albumPath << "Error:" << err->message;
<< m_albumPath << "Error:" << err->message
<< "Code:" << err->code;
if (isTimeoutError(err)) {
emit timedOut();
}
idevice_error_free(err);
return;
return false;
}
// FIXME: should we continue if albumInfo is null?
if (albumInfo.size) {
@@ -182,27 +186,23 @@ void PhotoModel::populatePhotoPaths()
if (err) {
qDebug() << "Failed to read photo directory:" << photoDir
<< "Error:" << err->message;
if (isTimeoutError(err)) {
emit timedOut();
}
idevice_error_free(err);
return;
return false;
}
if (files) {
for (int i = 0; files[i]; i++) {
QString fileName = QString::fromUtf8(files[i]);
if (fileName.endsWith(".JPG", Qt::CaseInsensitive) ||
fileName.endsWith(".PNG", Qt::CaseInsensitive) ||
fileName.endsWith(".HEIC", Qt::CaseInsensitive) ||
fileName.endsWith(".MOV", Qt::CaseInsensitive) ||
fileName.endsWith(".MP4", Qt::CaseInsensitive) ||
fileName.endsWith(".M4V", Qt::CaseInsensitive)) {
if (iDescriptor::Utils::isGalleryFile(fileName)) {
PhotoInfo info;
info.filePath = m_albumPath + "/" + fileName;
info.fileName = fileName;
info.thumbnailRequested = false;
info.fileType = determineFileType(fileName);
info.dateTime = extractDateTimeFromFile(info.filePath);
m_allPhotos.append(info);
}
}
@@ -214,6 +214,7 @@ void PhotoModel::populatePhotoPaths()
qDebug() << "Loaded" << m_allPhotos.size() << "media files from device";
qDebug() << "After filtering:" << m_photos.size() << "items shown";
return true;
}
// Sorting and filtering methods
@@ -235,6 +236,7 @@ void PhotoModel::setFilterType(FilterType filter)
void PhotoModel::applyFilterAndSort()
{
QMutexLocker locker(&m_mutex);
beginResetModel();
// Filter photos
@@ -330,15 +332,15 @@ QStringList PhotoModel::getFilteredFilePaths() const
// Helper methods
QDateTime PhotoModel::extractDateTimeFromFile(const QString &filePath) const
{
AfcFileInfo *info = nullptr;
AfcFileInfo info = {};
IdeviceFfiError *err = ServiceManager::safeAfcGetFileInfo(
m_device, filePath.toUtf8().constData(), info);
if (!err && info) {
uint64_t creation_seconds = info->creation;
m_device, filePath.toUtf8().constData(), &info);
if (!err && info.creation) {
uint64_t creation_seconds = info.creation;
QDateTime dateTime =
QDateTime::fromSecsSinceEpoch(creation_seconds, Qt::UTC);
// afc_file_info_free(info);
afc_file_info_free(&info);
if (dateTime.isValid()) {
return dateTime;
}
@@ -362,7 +364,24 @@ void PhotoModel::setAlbumPath(const QString &albumPath)
clear();
m_albumPath = albumPath;
populatePhotoPaths();
QFutureWatcher<bool> *futureWatcher = new QFutureWatcher<bool>(this);
QFuture<bool> future =
QtConcurrent::run([this]() { return populatePhotoPaths(); });
futureWatcher->setFuture(future);
connect(futureWatcher, &QFutureWatcher<bool>::finished, this,
[this, futureWatcher]() {
futureWatcher->deleteLater();
bool success = futureWatcher->result();
if (success) {
qDebug() << "Finished populating photo paths for album:"
<< m_albumPath;
emit albumPathSet();
} else {
// qDebug() << "Failed to populate photo paths for album:"
// << m_albumPath;
// emit albumPathFailed();
}
});
}
// TODO:REMOVE
void PhotoModel::refreshPhotos() { populatePhotoPaths(); }
+12 -7
View File
@@ -26,10 +26,12 @@
#include <QCryptographicHash>
#include <QDateTime>
#include <QFutureWatcher>
#include <QMutex>
#include <QMutexLocker>
#include <QPixmap>
#include <QSemaphore>
#include <QSize>
#include <QStandardPaths>
#include <QtConcurrent/QtConcurrent>
struct PhotoInfo {
QString filePath;
@@ -50,7 +52,7 @@ public:
enum FilterType { All, ImagesOnly, VideosOnly };
explicit PhotoModel(iDescriptorDevice *device, FilterType filterType,
explicit PhotoModel(const iDescriptorDevice *device, FilterType filterType,
QObject *parent = nullptr);
~PhotoModel();
@@ -83,20 +85,19 @@ public:
private:
// Data members
iDescriptorDevice *m_device;
const iDescriptorDevice *m_device;
QString m_albumPath;
QList<PhotoInfo> m_allPhotos; // All photos from device
QList<PhotoInfo> m_photos; // Currently filtered/sorted photos
// Thumbnail management
QSize m_thumbnailSize;
// Sorting and filtering
SortOrder m_sortOrder;
FilterType m_filterType;
QMutex m_mutex;
// Helper methods
void populatePhotoPaths();
bool populatePhotoPaths();
void applyFilterAndSort();
void sortPhotos(QList<PhotoInfo> &photos) const;
bool matchesFilter(const PhotoInfo &info) const;
@@ -107,6 +108,10 @@ private:
private slots:
void onThumbnailReady(const QString &path, const QPixmap &pixmap,
unsigned int row);
signals:
void albumPathSet();
void timedOut();
};
#endif // PHOTOMODEL_H
+130 -116
View File
@@ -28,143 +28,157 @@
#include <QPushButton>
#include <QVBoxLayout>
RecoveryDeviceInfoWidget::RecoveryDeviceInfoWidget(const void *info,
QWidget *parent)
std::string parse_recovery_mode(irecv_mode productType)
{
switch (productType) {
case irecv_mode::IRECV_K_RECOVERY_MODE_1:
case irecv_mode::IRECV_K_RECOVERY_MODE_2:
case irecv_mode::IRECV_K_RECOVERY_MODE_3:
case irecv_mode::IRECV_K_RECOVERY_MODE_4:
return "Recovery Mode";
case irecv_mode::IRECV_K_WTF_MODE:
return "WTF Mode";
case irecv_mode::IRECV_K_DFU_MODE:
case irecv_mode::IRECV_K_PORT_DFU_MODE:
return "DFU Mode";
default:
return "Unknown Mode";
}
}
RecoveryDeviceInfoWidget::RecoveryDeviceInfoWidget(
const iDescriptorRecoveryDevice *info, QWidget *parent)
: QWidget{parent}
{
// ecid = info->ecid; // Assuming ecid is unique for each device
ecid = info->ecid; // Assuming ecid is unique for each device
// QVBoxLayout *mainLayout = new QVBoxLayout(this);
// mainLayout->setContentsMargins(20, 20, 20, 20);
// mainLayout->setSpacing(16);
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(20, 20, 20, 20);
mainLayout->setSpacing(16);
// // Device Information Group
// QGroupBox *deviceInfoGroup = new QGroupBox("Device Information");
// QVBoxLayout *infoLayout = new QVBoxLayout(deviceInfoGroup);
// infoLayout->setSpacing(8);
// infoLayout->setContentsMargins(16, 16, 16, 16);
// Device Information Group
QGroupBox *deviceInfoGroup = new QGroupBox("Device Information");
QVBoxLayout *infoLayout = new QVBoxLayout(deviceInfoGroup);
infoLayout->setSpacing(8);
infoLayout->setContentsMargins(16, 16, 16, 16);
// // Device name with larger font
// QLabel *deviceNameLabel =
// new QLabel(QString::fromStdString(info->displayName));
// QFont nameFont = deviceNameLabel->font();
// nameFont.setPointSize(nameFont.pointSize() + 2);
// nameFont.setWeight(QFont::DemiBold);
// deviceNameLabel->setFont(nameFont);
// infoLayout->addWidget(deviceNameLabel);
// Device name with larger font
QLabel *deviceNameLabel =
new QLabel(QString::fromStdString(info->displayName));
QFont nameFont = deviceNameLabel->font();
nameFont.setPointSize(nameFont.pointSize() + 2);
nameFont.setWeight(QFont::DemiBold);
deviceNameLabel->setFont(nameFont);
infoLayout->addWidget(deviceNameLabel);
// // Add spacing
// infoLayout->addSpacing(8);
// Add spacing
infoLayout->addSpacing(8);
// // Mode info
// QString modeText =
// QString::fromStdString(parse_recovery_mode(info->mode)); QLabel
// *modeLabel = new QLabel("Mode: " + modeText);
// infoLayout->addWidget(modeLabel);
// Mode info
QString modeText = QString::fromStdString(parse_recovery_mode(info->mode));
QLabel *modeLabel = new QLabel("Mode: " + modeText);
infoLayout->addWidget(modeLabel);
// // ECID info
// QLabel *ecidLabel = new QLabel("ECID: " + QString::number(info->ecid));
// infoLayout->addWidget(ecidLabel);
// ECID info
QLabel *ecidLabel = new QLabel("ECID: " + QString::number(info->ecid));
infoLayout->addWidget(ecidLabel);
// // CPID info
// QLabel *cpidLabel = new QLabel("CPID: " + QString::number(info->cpid));
// infoLayout->addWidget(cpidLabel);
// CPID info
QLabel *cpidLabel = new QLabel("CPID: " + QString::number(info->cpid));
infoLayout->addWidget(cpidLabel);
// mainLayout->addWidget(deviceInfoGroup);
mainLayout->addWidget(deviceInfoGroup);
// // Actions Group
// QGroupBox *actionsGroup = new QGroupBox("Actions");
// QVBoxLayout *actionsLayout = new QVBoxLayout(actionsGroup);
// actionsLayout->setSpacing(12);
// actionsLayout->setContentsMargins(16, 16, 16, 16);
// Actions Group
QGroupBox *actionsGroup = new QGroupBox("Actions");
QVBoxLayout *actionsLayout = new QVBoxLayout(actionsGroup);
actionsLayout->setSpacing(12);
actionsLayout->setContentsMargins(16, 16, 16, 16);
// // Info label
// QLabel *infoLabel = new QLabel(
// "Exit recovery mode and restart the device into normal mode.");
// infoLabel->setWordWrap(true);
// QFont infoFont = infoLabel->font();
// infoFont.setPointSize(infoFont.pointSize() - 1);
// infoLabel->setFont(infoFont);
// QPalette infoPalette = infoLabel->palette();
// infoPalette.setColor(QPalette::WindowText,
// infoPalette.color(QPalette::WindowText).lighter(120));
// infoLabel->setPalette(infoPalette);
// actionsLayout->addWidget(infoLabel);
// Info label
QLabel *infoLabel = new QLabel(
"Exit recovery mode and restart the device into normal mode.");
infoLabel->setWordWrap(true);
QFont infoFont = infoLabel->font();
infoFont.setPointSize(infoFont.pointSize() - 1);
infoLabel->setFont(infoFont);
QPalette infoPalette = infoLabel->palette();
infoPalette.setColor(QPalette::WindowText,
infoPalette.color(QPalette::WindowText).lighter(120));
infoLabel->setPalette(infoPalette);
actionsLayout->addWidget(infoLabel);
// // Button layout
// QHBoxLayout *buttonLayout = new QHBoxLayout();
// buttonLayout->addStretch();
// Button layout
QHBoxLayout *buttonLayout = new QHBoxLayout();
buttonLayout->addStretch();
// QPushButton *exitRecoveryMode = new QPushButton("Exit Recovery Mode");
// exitRecoveryMode->setMinimumWidth(160);
// exitRecoveryMode->setMinimumHeight(32);
QPushButton *exitRecoveryMode = new QPushButton("Exit Recovery Mode");
exitRecoveryMode->setMinimumWidth(160);
exitRecoveryMode->setMinimumHeight(32);
// connect(exitRecoveryMode, &QPushButton::clicked, this, [this, info]() {
// irecv_client_t client = NULL;
// irecv_error_t ierr = irecv_open_with_ecid_and_attempts(
// &client, info->ecid, RECOVERY_CLIENT_CONNECTION_TRIES);
// irecv_error_t error = IRECV_E_SUCCESS;
// if (ierr != IRECV_E_SUCCESS) {
// QMessageBox::critical(
// this, "Connection Error",
// QString("Failed to open device with ECID %1:\n%2")
// .arg(info->ecid)
// .arg(irecv_strerror(ierr)));
// return;
// }
connect(exitRecoveryMode, &QPushButton::clicked, this, [this, info]() {
irecv_client_t client = NULL;
irecv_error_t ierr = irecv_open_with_ecid_and_attempts(
&client, info->ecid, RECOVERY_CLIENT_CONNECTION_TRIES);
irecv_error_t error = IRECV_E_SUCCESS;
if (ierr != IRECV_E_SUCCESS) {
QMessageBox::critical(
this, "Connection Error",
QString("Failed to open device with ECID %1:\n%2")
.arg(info->ecid)
.arg(irecv_strerror(ierr)));
return;
}
// if (client == NULL) {
// QMessageBox::critical(this, "Error",
// "Client is NULL after successful open");
// return;
// }
if (client == NULL) {
QMessageBox::critical(this, "Error",
"Client is NULL after successful open");
return;
}
// error = irecv_setenv(client, "auto-boot", "true");
// if (error != IRECV_E_SUCCESS) {
// QMessageBox::critical(
// this, "Error",
// QString("Failed to set environment variable
// 'auto-boot':\n%1")
// .arg(irecv_strerror(error)));
// irecv_close(client);
// return;
// }
error = irecv_setenv(client, "auto-boot", "true");
if (error != IRECV_E_SUCCESS) {
QMessageBox::critical(
this, "Error",
QString("Failed to set environment variable 'auto-boot':\n%1")
.arg(irecv_strerror(error)));
irecv_close(client);
return;
}
// error = irecv_saveenv(client);
// if (error != IRECV_E_SUCCESS) {
// QMessageBox::critical(
// this, "Error",
// QString("Failed to save environment variables:\n%1")
// .arg(irecv_strerror(error)));
// irecv_close(client);
// return;
// }
error = irecv_saveenv(client);
if (error != IRECV_E_SUCCESS) {
QMessageBox::critical(
this, "Error",
QString("Failed to save environment variables:\n%1")
.arg(irecv_strerror(error)));
irecv_close(client);
return;
}
// error = irecv_reboot(client);
// if (error != IRECV_E_SUCCESS) {
// QMessageBox::critical(this, "Error",
// QString("Failed to send reboot
// command:\n%1")
// .arg(irecv_strerror(error)));
// irecv_close(client);
// return;
// }
error = irecv_reboot(client);
if (error != IRECV_E_SUCCESS) {
QMessageBox::critical(this, "Error",
QString("Failed to send reboot command:\n%1")
.arg(irecv_strerror(error)));
irecv_close(client);
return;
}
// irecv_close(client);
irecv_close(client);
// auto *msgBox =
// new QMessageBox(QMessageBox::Information, "Success",
// "Device is exiting recovery mode and
// restarting...", QMessageBox::Ok,
// QApplication::activeWindow());
// msgBox->setAttribute(Qt::WA_DeleteOnClose);
// msgBox->open();
// });
auto *msgBox = new QMessageBox(
QMessageBox::Information, "Success",
"Device is exiting recovery mode and restarting... ",
QMessageBox::Ok, QApplication::activeWindow());
msgBox->setAttribute(Qt::WA_DeleteOnClose);
msgBox->open();
});
// buttonLayout->addWidget(exitRecoveryMode);
// buttonLayout->addStretch();
// actionsLayout->addLayout(buttonLayout);
buttonLayout->addWidget(exitRecoveryMode);
buttonLayout->addStretch();
actionsLayout->addLayout(buttonLayout);
// mainLayout->addWidget(actionsGroup);
// mainLayout->addStretch();
mainLayout->addWidget(actionsGroup);
mainLayout->addStretch();
}
+1 -1
View File
@@ -27,7 +27,7 @@ class RecoveryDeviceInfoWidget : public QWidget
Q_OBJECT
public:
explicit RecoveryDeviceInfoWidget(const void *info,
explicit RecoveryDeviceInfoWidget(const iDescriptorRecoveryDevice *info,
QWidget *parent = nullptr);
uint64_t ecid; // Assuming ecid is unique for each device
signals:
+46 -40
View File
@@ -223,96 +223,102 @@ IdeviceFfiError *ServiceManager::exportFileToPath(
std::atomic<bool> *cancelRequested)
{
qDebug()
<< "[serviceManager::exportFileToPath] Exporting file from device path:"
<< "[ServiceManager::exportFileToPath] Exporting file from device path:"
<< device_path << "to local path:" << local_path;
// FIXME : use execute afc op
return executeOperation<IdeviceFfiError *>(
device,
[device, device_path, local_path, progressCallback,
cancelRequested]() -> IdeviceFfiError * {
AfcFileHandle *afcHandle = nullptr;
qDebug() << "Opening file on device:" << device_path;
IdeviceFfiError *err_open = safeAfcFileOpen(
device, device_path, AfcFopenMode::AfcRdOnly, &afcHandle);
IdeviceFfiError *err =
afc_file_open(device->afcClient, device_path,
AfcFopenMode::AfcRdOnly, &afcHandle);
if (err_open != nullptr) {
if (err != nullptr) {
qDebug() << "Failed to open file on device:" << device_path
<< "Error Code:" << err_open->code
<< "Message:" << err_open->message;
return err_open;
<< "Error Code:" << err->code
<< "Message:" << err->message;
return err;
}
qDebug() << "File opened on device successfully";
FILE *out = fopen(local_path, "wb");
if (!out) {
qDebug() << "Failed to open local file:" << local_path;
IdeviceFfiError *err_close =
safeAfcFileClose(device, afcHandle);
IdeviceFfiError *err_close = afc_file_close(afcHandle);
if (err_close != nullptr) {
// idevice_error_free(err_close);
idevice_error_free(err_close);
}
return new IdeviceFfiError{1, "FAILED_TO_OPEN_LOCAL_FILE"};
return new IdeviceFfiError{1, "Failed to open local file"};
}
qDebug() << "Local file opened successfully";
const size_t CHUNK_SIZE = 256 * 1024; // 256KB chunks
// 256KB chunks
const size_t CHUNK_SIZE = 256 * 1024;
uint8_t *chunkData = nullptr;
size_t bytesRead = 0;
qint64 totalBytesRead = 0;
// Get file size for progress
AfcFileInfo fileInfo;
IdeviceFfiError *info_err =
safeAfcGetFileInfo(device, device_path, &fileInfo);
err = afc_get_file_info(device->afcClient, device_path, &fileInfo);
qint64 totalFileSize = 0;
if (info_err == nullptr) {
totalFileSize = fileInfo.size;
// afc_file_info_free(&fileInfo);
} else {
// idevice_error_free(info_err);
}
if (err != nullptr)
return err;
totalFileSize = fileInfo.size;
afc_file_info_free(&fileInfo);
IdeviceFfiError *read_err = nullptr;
// Read file in chunks
while (true) {
// Check for cancellation
if (cancelRequested && cancelRequested->load()) {
fclose(out);
safeAfcFileClose(device, afcHandle);
return new IdeviceFfiError{1, "OPERATION_CANCELLED"};
err = afc_file_close(afcHandle);
if (err != nullptr) {
idevice_error_free(err);
}
// FIXME: we need error codes
return new IdeviceFfiError{1, "Transfer cancelled"};
}
read_err = safeAfcFileRead(device, afcHandle, &chunkData,
CHUNK_SIZE, &bytesRead);
err = afc_file_read(afcHandle, &chunkData, CHUNK_SIZE,
&bytesRead);
if (read_err != nullptr) {
qDebug() << "Error reading file:" << read_err->message;
if (err != nullptr) {
qDebug() << "Error reading file:" << err->message;
fclose(out);
safeAfcFileClose(device, afcHandle);
return read_err;
IdeviceFfiError *err_close = afc_file_close(afcHandle);
if (err_close != nullptr) {
idevice_error_free(err_close);
}
return err;
}
if (bytesRead == 0) {
// End of file reached
// End of file
break;
}
// Write chunk to local file
// Write chunk
size_t written = fwrite(chunkData, 1, bytesRead, out);
// Free the memory allocated by afc_file_read
afc_file_read_data_free(chunkData, bytesRead);
chunkData = nullptr;
if (written != bytesRead) {
qDebug() << "Failed to write all bytes to local file";
fclose(out);
safeAfcFileClose(device, afcHandle);
return new IdeviceFfiError{1, "WRITE_ERROR"};
IdeviceFfiError *err_close = afc_file_close(afcHandle);
if (err_close != nullptr) {
idevice_error_free(err_close);
}
return new IdeviceFfiError{1,
"Failed to write to local file"};
}
totalBytesRead += bytesRead;
// Report progress
if (progressCallback) {
progressCallback(totalBytesRead, totalFileSize);
}
@@ -320,13 +326,13 @@ IdeviceFfiError *ServiceManager::exportFileToPath(
fclose(out);
IdeviceFfiError *err_close = safeAfcFileClose(device, afcHandle);
IdeviceFfiError *err_close = afc_file_close(afcHandle);
if (err_close != nullptr) {
qDebug() << "Failed to close AFC file:" << err_close->message;
return err_close;
}
return nullptr; // Success
return nullptr;
});
}
+14 -27
View File
@@ -456,16 +456,7 @@ void ToolboxWidget::onToolboxClicked(iDescriptorTool tool, bool requiresDevice)
liveScreen->show();
} break;
case iDescriptorTool::RecoveryMode: {
// Handle entering recovery mode
// bool success = _enterRecoveryMode(device);
// 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 =
@@ -547,8 +538,6 @@ void ToolboxWidget::onToolboxClicked(iDescriptorTool tool, bool requiresDevice)
#endif
case iDescriptorTool::CableInfoWidget: {
CableInfoWidget *cableInfoWidget = new CableInfoWidget(device);
cableInfoWidget->setAttribute(Qt::WA_DeleteOnClose);
cableInfoWidget->resize(600, 400);
cableInfoWidget->show();
} break;
case iDescriptorTool::NetworkDevices: {
@@ -569,7 +558,7 @@ void ToolboxWidget::onToolboxClicked(iDescriptorTool tool, bool requiresDevice)
}
}
void ToolboxWidget::restartDevice(iDescriptorDevice *device)
void ToolboxWidget::restartDevice(const iDescriptorDevice *device)
{
QMessageBox msgBox;
msgBox.setWindowTitle("Restart Device");
@@ -598,7 +587,7 @@ void ToolboxWidget::restartDevice(iDescriptorDevice *device)
}
}
void ToolboxWidget::shutdownDevice(iDescriptorDevice *device)
void ToolboxWidget::shutdownDevice(const iDescriptorDevice *device)
{
QMessageBox msgBox;
@@ -629,7 +618,7 @@ void ToolboxWidget::shutdownDevice(iDescriptorDevice *device)
}
}
void ToolboxWidget::_enterRecoveryMode(iDescriptorDevice *device)
void ToolboxWidget::enterRecoveryMode(const iDescriptorDevice *device)
{
QMessageBox msgBox;
msgBox.setWindowTitle("Enter Recovery Mode");
@@ -645,18 +634,16 @@ void ToolboxWidget::_enterRecoveryMode(iDescriptorDevice *device)
return;
}
// auto res = device->diagRelay->enterRecovery();
// if (res.is_err()) {
// QMessageBox::warning(
// nullptr, "Enter Recovery Mode Failed",
// "Failed to enter recovery mode: " +
// QString::fromStdString(res.unwrap_err().message));
// } else {
// QMessageBox::information(nullptr, "Enter Recovery Mode Initiated",
// "Device will enter recovery mode once
// unplugged.");
// qDebug() << "Entering recovery mode";
// }
IdeviceFfiError *error = lockdownd_enter_recovery(device->lockdown);
if (error != nullptr) {
QMessageBox::warning(nullptr, "Enter Recovery Mode Failed",
"Failed to enter recovery mode: " +
QString::fromStdString(error->message));
idevice_error_free(error);
} else {
QMessageBox::information(nullptr, "Enter Recovery Mode Initiated",
"Device will enter recovery mode.");
}
}
void ToolboxWidget::restartAirPlayWidget()
+3 -3
View File
@@ -44,9 +44,9 @@ class ToolboxWidget : public QWidget
Q_OBJECT
public:
explicit ToolboxWidget(QWidget *parent = nullptr);
static void restartDevice(iDescriptorDevice *device);
static void shutdownDevice(iDescriptorDevice *device);
static void _enterRecoveryMode(iDescriptorDevice *device);
static void restartDevice(const iDescriptorDevice *device);
static void shutdownDevice(const iDescriptorDevice *device);
static void enterRecoveryMode(const iDescriptorDevice *device);
static ToolboxWidget *sharedInstance();
void restartAirPlayWidget();
private slots:
+207 -145
View File
@@ -152,8 +152,6 @@ VirtualLocation::VirtualLocation(iDescriptorDevice *device, QWidget *parent)
this->deleteLater();
}
});
// DevDiskManager::sharedInstance()->downloadCompatibleImage(m_device);
}
void VirtualLocation::onQuickWidgetStatusChanged(QQuickWidget::Status status)
@@ -170,8 +168,7 @@ void VirtualLocation::onQuickWidgetStatusChanged(QQuickWidget::Status status)
void VirtualLocation::onInputChanged()
{
// Update map when input changes (with slight delay to avoid too frequent
// updates)
// Update map when input changes
m_updateTimer.setSingleShot(true);
m_updateTimer.setInterval(500); // 500ms delay
@@ -301,186 +298,251 @@ void VirtualLocation::onApplyClicked()
m_applyButton->setEnabled(true);
return;
}
// DevDiskImageHelper *devDiskImageHelper =
// new DevDiskImageHelper(m_device, this);
// connect(devDiskImageHelper, &DevDiskImageHelper::mountingCompleted, this,
// [this, latitude, longitude, devDiskImageHelper](bool success) {
// if (devDiskImageHelper) {
// devDiskImageHelper->deleteLater();
// }
// if (!success) {
// return;
// }
// // Apply location
// emit locationChanged(latitude, longitude);
// updateMapFromInputs();
// // Visual feedback
// m_applyButton->setText("Applied!");
// bool locationSuccess = set_location(
// m_device->device,
// const_cast<char *>(
// m_latitudeEdit->text().toStdString().c_str()),
// const_cast<char *>(
// m_longitudeEdit->text().toStdString().c_str()));
// if (!locationSuccess) {
// QMessageBox::warning(this, "Error",
// "Failed to set location on device");
// } else {
// SettingsManager::sharedInstance()->saveRecentLocation(
// latitude, longitude);
// QMessageBox::information(this, "Success",
// "Location applied
// successfully!");
// }
// });
// connect(
// devDiskImageHelper, &DevDiskImageHelper::destroyed, this,
// [this]() {
// QTimer::singleShot(1000, this, [this]() {
// m_applyButton->setText("Apply Location");
// m_applyButton->setEnabled(true);
// });
// },
// Qt::SingleShotConnection);
// devDiskImageHelper->start();
int major = m_device->deviceInfo.parsedDeviceVersion.major;
if (major < 17) {
QMessageBox::warning(this, "TODO", "TODO");
LocationSimulationServiceHandle *locationSim = nullptr;
IdeviceFfiError *err = nullptr;
err = lockdown_location_simulation_connect(m_device->provider,
&locationSim);
if (err != NULL) {
qDebug() << "Failed to connect to location simulation service:"
<< err->code << err->message;
if (err->code != InvalidServiceErrorCode) {
QMessageBox::warning(
this, "Error",
QString(
"Failed to connect to location simulation service: %1")
.arg(err->message));
idevice_error_free(err);
m_applyButton->setEnabled(true);
return;
}
idevice_error_free(err);
DevDiskImageHelper *devDiskImageHelper =
new DevDiskImageHelper(m_device, this);
connect(
devDiskImageHelper, &DevDiskImageHelper::mountingCompleted,
this,
[this, latitude, longitude, devDiskImageHelper](bool success) {
if (devDiskImageHelper) {
devDiskImageHelper->deleteLater();
}
if (!success) {
// mounter will show its own error message, just
// re-enable the button here
m_applyButton->setEnabled(true);
return;
}
IdeviceFfiError *err = nullptr;
LocationSimulationServiceHandle *locationSim = nullptr;
err = lockdown_location_simulation_connect(
m_device->provider, &locationSim);
if (err != NULL) {
qDebug() << "Failed to connect to location simulation "
"service after mounting disk image:"
<< err->code << err->message;
QMessageBox::warning(
this, "Error",
QString("Failed to connect to location simulation "
"service after mounting disk image: %1")
.arg(err->message));
idevice_error_free(err);
m_applyButton->setEnabled(true);
return;
}
err = lockdown_location_simulation_set(
locationSim,
m_latitudeEdit->text().toStdString().c_str(),
m_longitudeEdit->text().toStdString().c_str());
if (err != NULL) {
qDebug()
<< "Failed to set location simulation:" << err->code
<< err->message;
QMessageBox::warning(
this, "Error",
QString("Failed to set location simulation: %1")
.arg(err->message));
idevice_error_free(err);
}
lockdown_location_simulation_free(locationSim);
QMessageBox::information(this, "Success",
"Location applied successfully!");
SettingsManager::sharedInstance()->saveRecentLocation(
latitude, longitude);
m_applyButton->setEnabled(true);
return;
});
connect(
devDiskImageHelper, &DevDiskImageHelper::destroyed, this,
[this]() {
QTimer::singleShot(1000, this, [this]() {
m_applyButton->setText("Apply Location");
m_applyButton->setEnabled(true);
});
},
Qt::SingleShotConnection);
return devDiskImageHelper->start();
}
err = lockdown_location_simulation_set(
locationSim, m_latitudeEdit->text().toStdString().c_str(),
m_longitudeEdit->text().toStdString().c_str());
if (err != NULL) {
qDebug() << "Failed to set location simulation:" << err->code
<< err->message;
QMessageBox::warning(
this, "Error",
QString("Failed to set location simulation: %1")
.arg(err->message));
idevice_error_free(err);
}
lockdown_location_simulation_free(locationSim);
QMessageBox::information(this, "Success",
"Location applied successfully!");
m_applyButton->setEnabled(true);
return;
}
/* iOS 17 and above */
IdeviceFfiError *err = nullptr;
// Connect to CoreDeviceProxy
IdeviceFfiError *err_reveal = nullptr;
CoreDeviceProxyHandle *core_device = NULL;
uint16_t rsd_port;
AdapterHandle *adapter = NULL;
ReadWriteOpaque *stream = NULL;
RsdHandshakeHandle *handshake = NULL;
RemoteServerHandle *remote_server = NULL;
LocationSimulationHandle *location_sim = NULL;
err = core_device_proxy_connect(m_device->provider, &core_device);
if (err != NULL) {
fprintf(stderr, "Failed to connect to CoreDeviceProxy: [%d] %s",
err->code, err->message);
idevice_error_free(err);
qDebug() << "Failed to connect to CoreDeviceProxy:" << err->code
<< err->message;
goto cleanup;
}
// Get server RSD port
uint16_t rsd_port;
err = core_device_proxy_get_server_rsd_port(core_device, &rsd_port);
if (err != NULL) {
fprintf(stderr, "Failed to get server RSD port: [%d] %s", err->code,
err->message);
idevice_error_free(err);
core_device_proxy_free(core_device);
qDebug() << "Failed to get server RSD port:" << err->code
<< err->message;
goto cleanup;
}
// Create TCP adapter and connect to RSD port
AdapterHandle *adapter = NULL;
err = core_device_proxy_create_tcp_adapter(core_device, &adapter);
if (err != NULL) {
fprintf(stderr, "Failed to create TCP adapter: [%d] %s", err->code,
err->message);
idevice_error_free(err);
qDebug() << "Failed to create TCP adapter:" << err->code
<< err->message;
goto cleanup;
}
// Connect to RSD port
ReadWriteOpaque *stream = NULL;
err = adapter_connect(adapter, rsd_port, &stream);
if (err != NULL) {
fprintf(stderr, "Failed to connect to RSD port: [%d] %s", err->code,
err->message);
idevice_error_free(err);
adapter_free(adapter);
qDebug() << "Failed to connect to RSD port:" << err->code
<< err->message;
goto cleanup;
}
RsdHandshakeHandle *handshake = NULL;
err = rsd_handshake_new(stream, &handshake);
if (err != NULL) {
fprintf(stderr, "Failed to perform RSD handshake: [%d] %s", err->code,
err->message);
idevice_error_free(err);
// adapter_close(stream);
idevice_stream_free(stream);
adapter_free(adapter);
qDebug() << "Failed to perform RSD handshake:" << err->code
<< err->message;
goto cleanup;
}
// Create RemoteServerClient
RemoteServerHandle *remote_server = NULL;
err = remote_server_connect_rsd(adapter, handshake, &remote_server);
if (err != NULL) {
// needs dev mode
fprintf(stderr, "Failed to create remote server: [%d] %s", err->code,
err->message);
if (err->code == ServiceNotFoundErrorCode) {
auto res = QMessageBox::question(
this, "Enable Developer Mode?",
"Location Simulation service is not found. Enable Developer "
"Mode on the device?",
QMessageBox::Yes | QMessageBox::No);
if (res == QMessageBox::Yes) {
IdeviceFfiError *devmodeErr =
ServiceManager::enableDevMode(m_device);
if (devmodeErr != NULL) {
QMessageBox::warning(
this, "Error",
QString("Failed to enable Developer Mode:\n%1")
.arg(devmodeErr->message));
idevice_error_free(devmodeErr);
} else {
QMessageBox::information(
this, "Success",
"Developer Mode enabled successfully. Please try "
"applying the location again.");
}
err_reveal =
ServiceManager::revealDeveloperModeOptionInUI(m_device);
if (err_reveal != NULL) {
qDebug() << "Failed to reveal developer mode option in UI:"
<< err_reveal->code << err_reveal->message;
QMessageBox::warning(
this, "Error",
"Developer Mode Not Enabled and failed to "
"reveal developer mode option in UI:\n" +
QString::fromStdString(err_reveal->message));
goto cleanup;
}
idevice_error_free(err);
adapter_free(adapter);
rsd_handshake_free(handshake);
}
// Create LocationSimulationClient
LocationSimulationHandle *location_sim = NULL;
err = location_simulation_new(remote_server, &location_sim);
if (err != NULL) {
fprintf(stderr,
"Failed to create location simulation client: [%d] %s",
err->code, err->message);
idevice_error_free(err);
remote_server_free(remote_server);
}
// Set location
err = location_simulation_set(location_sim, latitude, longitude);
if (err != NULL) {
fprintf(stderr, "Failed to set location: [%d] %s", err->code,
err->message);
idevice_error_free(err);
// TODO: create a widget showing instructions on how to enable dev
// mode,
QMessageBox::information(this, "Developer Mode Not Enabled",
"Please enable Developer "
"Mode on the device to use this "
"feature.");
goto cleanup;
} else {
printf("Successfully set location to %.6f, %.6f\n", latitude,
longitude);
qDebug() << "Failed to connect to remote server:" << err->code
<< err->message;
QMessageBox::warning(this, "Connection Failed",
"Failed to connect to device's remote "
"service:\n" +
QString::fromStdString(err->message));
goto cleanup;
}
}
// // FIXME: create issue for c bindings
// IdeviceFfiError *err =
// location_simulation_set(m_device->locationSimulation,
// latitude, longitude);
// if (err != nullptr) {
// QMessageBox::warning(this, "Error",
// "Failed to set location on device:\n" +
// QString::fromStdString(err->message));
// // idevice_ffi_error_free(err);
// } else {
// // SettingsManager::sharedInstance()->saveRecentLocation(
// // latitude, longitude);
// QMessageBox::information(this, "Success",
// "Location applied successfully!");
err = location_simulation_new(remote_server, &location_sim);
if (err != NULL) {
qDebug() << "Failed to create location simulation client:" << err->code
<< err->message;
QMessageBox::warning(this, "Error",
"Failed to create location simulation client:\n" +
QString::fromStdString(err->message));
goto cleanup;
}
err = location_simulation_set(location_sim, latitude, longitude);
if (err != NULL) {
qDebug() << "Failed to set location:" << err->code << err->message;
QMessageBox::warning(this, "Error",
"Failed to set location on device:\n" +
QString::fromStdString(err->message));
} else {
qDebug() << "Successfully set location to" << latitude << longitude;
QMessageBox::information(this, "Success",
"Location applied successfully!");
SettingsManager::sharedInstance()->saveRecentLocation(latitude,
longitude);
}
cleanup:
if (location_sim) {
location_simulation_free(location_sim);
}
if (remote_server) {
remote_server_free(remote_server);
}
if (handshake) {
rsd_handshake_free(handshake);
}
if (adapter) {
adapter_close(adapter);
adapter_free(adapter);
}
// FIXME: seg faults
// if (stream) {
// idevice_stream_free(stream);
// }
if (core_device) {
core_device_proxy_free(core_device);
}
if (err) {
idevice_error_free(err);
}
if (err_reveal) {
idevice_error_free(err_reveal);
}
m_applyButton->setEnabled(true);
}
void VirtualLocation::loadRecentLocations(QVBoxLayout *layout)
+1 -2
View File
@@ -87,8 +87,7 @@ void WelcomeWidget::setupUI()
[]() { QDesktopServices::openUrl(QUrl(REPO_URL)); });
QPalette githubPalette = m_githubLabel->palette();
githubPalette.setColor(QPalette::WindowText,
COLOR_HYPERLINK); // Apple blue
githubPalette.setColor(QPalette::WindowText, COLOR_HYPERLINK);
m_githubLabel->setPalette(githubPalette);
m_mainLayout->addWidget(m_githubLabel, 0, Qt::AlignCenter);
+1 -1
View File
@@ -190,7 +190,7 @@ void WirelessGalleryImportWidget::onImportPhotos()
return;
}
PhotoImportDialog dialog(m_selectedFiles, false, this);
PhotoImportDialog dialog(m_selectedFiles, this);
dialog.exec();
}
+33 -20
View File
@@ -22,7 +22,13 @@ ZLoadingWidget::ZLoadingWidget(bool start, QWidget *parent)
loadingLayout->addWidget(m_loadingIndicator);
loadingLayout->addStretch();
addWidget(loadingWidget); // Loading widget at index 0
m_errorWidget = new ZLoadingErrorWidget(this);
connect(static_cast<ZLoadingErrorWidget *>(m_errorWidget),
&ZLoadingErrorWidget::retryClicked, this,
[this]() { emit retryClicked(); });
addWidget(loadingWidget);
addWidget(m_errorWidget);
}
void ZLoadingWidget::setupContentWidget(QWidget *contentWidget)
@@ -41,32 +47,24 @@ void ZLoadingWidget::setupContentWidget(QLayout *contentLayout)
void ZLoadingWidget::setupErrorWidget(QWidget *errorWidget)
{
if (m_errorWidget) {
m_errorWidget->deleteLater();
}
m_errorWidget = errorWidget;
addWidget(m_errorWidget);
}
void ZLoadingWidget::setupErrorWidget(QLayout *errorLayout)
{
if (m_errorWidget) {
m_errorWidget->deleteLater();
}
m_errorWidget = new QWidget();
m_errorWidget->setLayout(errorLayout);
addWidget(m_errorWidget);
}
void ZLoadingWidget::setupErrorWidget(const QString &errorMessage)
{
m_errorWidget = new QWidget();
QVBoxLayout *errorLayout = new QVBoxLayout(m_errorWidget);
errorLayout->setAlignment(Qt::AlignCenter);
QLabel *errorLabel = new QLabel(errorMessage);
errorLabel->setAlignment(Qt::AlignCenter);
errorLabel->setStyleSheet("QLabel { color: red; }");
errorLayout->addWidget(errorLabel);
addWidget(m_errorWidget);
}
void ZLoadingWidget::setupAditionalWidget(QWidget *customWidget)
{
addWidget(customWidget);
@@ -76,6 +74,9 @@ void ZLoadingWidget::switchToWidget(QWidget *widget)
{
int index = indexOf(widget);
if (index != -1) {
if (m_loadingIndicator) {
m_loadingIndicator->stop();
}
setCurrentIndex(index);
}
}
@@ -85,9 +86,8 @@ void ZLoadingWidget::stop(bool showContent)
if (m_loadingIndicator) {
m_loadingIndicator->stop();
}
if (showContent) {
// FIXME: dont use hardcoded index
setCurrentIndex(1);
if (showContent && m_contentWidget) {
switchToWidget(m_contentWidget);
}
}
@@ -99,13 +99,26 @@ void ZLoadingWidget::showError()
}
}
void ZLoadingWidget::showError(const QString &errorMessage)
{
m_loadingIndicator->stop();
if (m_errorWidget) {
// FIXME: can be handled better
// maybe subclass ZLoadingWidget for custom error widget?
if (auto errorWidget =
qobject_cast<ZLoadingErrorWidget *>(m_errorWidget)) {
errorWidget->setText(errorMessage);
}
setCurrentWidget(m_errorWidget);
}
}
void ZLoadingWidget::showLoading()
{
if (m_loadingIndicator) {
m_loadingIndicator->start();
}
// FIXME: dont use hardcoded index
setCurrentIndex(0);
setCurrentWidget(m_loadingIndicator->parentWidget());
}
ZLoadingWidget::~ZLoadingWidget()
+35 -1
View File
@@ -1,8 +1,41 @@
#ifndef ZLOADINGWIDGET_H
#define ZLOADINGWIDGET_H
#include <QLabel>
#include <QPushButton>
#include <QStackedWidget>
#include <QWidget>
#include <QHBoxLayout>
class ZLoadingErrorWidget : public QWidget
{
Q_OBJECT
public:
ZLoadingErrorWidget(QWidget *parent = nullptr)
{
QHBoxLayout *layout = new QHBoxLayout(this);
layout->addStretch();
m_errorLabel = new QLabel("An error occurred.", this);
m_errorLabel->setAlignment(Qt::AlignCenter);
m_errorLabel->setStyleSheet("QLabel { color: red; }");
layout->addWidget(m_errorLabel);
m_retryButton = new QPushButton("Retry", this);
m_retryButton->setMaximumWidth(m_retryButton->sizeHint().width());
layout->addWidget(m_retryButton);
layout->addStretch();
connect(m_retryButton, &QPushButton::clicked, this,
[this]() { emit retryClicked(); });
}
void setText(const QString &text) { m_errorLabel->setText(text); };
private:
QLabel *m_errorLabel;
QPushButton *m_retryButton;
signals:
void retryClicked();
};
class ZLoadingWidget : public QStackedWidget
{
@@ -15,17 +48,18 @@ public:
void setupContentWidget(QWidget *contentWidget);
void setupContentWidget(QLayout *contentLayout);
void setupErrorWidget(QWidget *errorWidget);
void setupErrorWidget(const QString &errorMessage);
void setupErrorWidget(QLayout *errorLayout);
void setupAditionalWidget(QWidget *customWidget);
void switchToWidget(QWidget *widget);
void showError();
void showError(const QString &errorMessage);
private:
class QProcessIndicator *m_loadingIndicator = nullptr;
QWidget *m_contentWidget = nullptr;
QWidget *m_errorWidget = nullptr;
signals:
void retryClicked();
};
#endif // ZLOADINGWIDGET_H