Files
iDescriptor/src/mainwindow.cpp
T
2026-04-19 18:53:29 +00:00

524 lines
19 KiB
C++

/*
* 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 "mainwindow.h"
#include "appswidget.h"
#include "devicemanagerwidget.h"
#include "ifusediskunmountbutton.h"
#include "ifusemanager.h"
#include "jailbrokenwidget.h"
#include "releasechangelogdialog.h"
#include "toolboxwidget.h"
#include "welcomewidget.h"
#include <unistd.h>
#include "appcontext.h"
#include "networkdeviceswidget.h"
#include "settingsmanager.h"
#include "statusballoon.h"
#ifdef WIN32
#include "platform/windows/win_common.h"
#endif
#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
void handleCallback(int code, const QString &udid, const QString &info)
{
qDebug() << "Device event: "
<< (code == 1 ? "Connected"
: code == 2 ? "Disconnected"
: code == 3 ? "Pairing Pending"
: code == 4 ? "Pairing Failed"
: "Unknown")
<< ", UDID: " << udid;
AddType addType;
switch (code) {
case 1: { // fully connected
addType = AddType::Regular;
break;
}
case 2: { // disconnected
QMetaObject::invokeMethod(
AppContext::sharedInstance(), "removeDevice", Qt::QueuedConnection,
Q_ARG(iDescriptor::Uniq, iDescriptor::Uniq(udid)));
return;
}
case 3: { // pairing pending
addType = AddType::Pairing;
break;
}
case 4: { // pairing failed
addType = AddType::FailedToPair;
break;
}
}
QMetaObject::invokeMethod(
AppContext::sharedInstance(), "addDevice", Qt::QueuedConnection,
Q_ARG(iDescriptor::Uniq, iDescriptor::Uniq(udid)),
Q_ARG(iDescriptor::IdeviceConnectionType,
static_cast<iDescriptor::IdeviceConnectionType>(
iDescriptor::CONNECTION_USB)),
Q_ARG(AddType, addType), Q_ARG(QString, info));
}
MainWindow *MainWindow::sharedInstance()
{
static MainWindow instance;
return &instance;
}
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
{
setMinimumSize(MIN_MAIN_WINDOW_SIZE);
resize(MIN_MAIN_WINDOW_SIZE);
setContentsMargins(0, 0, 0, 0);
QWidget *centralWidget = new QWidget(this);
setCentralWidget(centralWidget);
auto mainLayout = new QVBoxLayout(centralWidget);
mainLayout->setContentsMargins(0, 0, 0, 0);
auto core = AppContext::sharedInstance()->core;
connect(core, &CXX::Core::device_event, this,
[this](uint32_t code, const QString &udid, const QString &info) {
handleCallback(code, udid, info);
});
connect(core, &CXX::Core::sleepy_time_detected, this,
&MainWindow::handleShowSleepyDeviceWarning);
AppContext::sharedInstance()->core->init();
m_ZTabWidget = new ZTabWidget(this);
#ifdef __APPLE__
setupMacOSWindow(this);
setAttribute(Qt::WA_ContentsMarginsRespectsSafeArea, false);
#endif
mainLayout->addWidget(m_ZTabWidget);
#ifdef WIN32
setupWinWindow(this);
#endif
m_mainStackedWidget = new QStackedWidget();
WelcomeWidget *welcomePage = new WelcomeWidget(this);
m_deviceManager = new DeviceManagerWidget(this);
m_mainStackedWidget->addWidget(welcomePage);
m_mainStackedWidget->addWidget(m_deviceManager);
connect(m_deviceManager, &DeviceManagerWidget::updateNoDevicesConnected,
this, &MainWindow::updateNoDevicesConnected);
m_ZTabWidget->addTab(m_mainStackedWidget, "iDevice");
auto *appsWidgetTab =
m_ZTabWidget->addTab(AppsWidget::sharedInstance(), "Apps");
m_ZTabWidget->addTab(ToolboxWidget::sharedInstance(), "Toolbox");
auto *jailbrokenWidget = new JailbrokenWidget(this);
m_ZTabWidget->addTab(jailbrokenWidget, "Jailbroken");
m_ZTabWidget->finalizeStyles();
connect(
appsWidgetTab, &ZTab::clicked, this,
[this](int index) { AppsWidget::sharedInstance()->init(); },
Qt::SingleShotConnection);
// settings button
ZIconWidget *settingsButton = new ZIconWidget(
QIcon(":/resources/icons/MingcuteSettings7Line.png"), "Settings");
settingsButton->setCursor(Qt::PointingHandCursor);
connect(settingsButton, &ZIconWidget::clicked, this, [this]() {
SettingsManager::sharedInstance()->showSettingsDialog();
});
ZIconWidget *githubButton = new ZIconWidget(
QIcon(":/resources/icons/MdiGithub.png"), "iDescriptor on GitHub");
githubButton->setCursor(Qt::PointingHandCursor);
connect(githubButton, &ZIconWidget::clicked, this,
[]() { QDesktopServices::openUrl(QUrl(REPO_URL)); });
m_connectedDeviceCountLabel = new QLabel("iDescriptor: no devices");
m_connectedDeviceCountLabel->setContentsMargins(0, 0, 0, 0);
m_connectedDeviceCountLabel->setStyleSheet(
"QLabel:hover { background-color : #13131319; }");
QWidget *statusbar = new QWidget();
QHBoxLayout *statusLayout = new QHBoxLayout(statusbar);
statusLayout->setContentsMargins(10, 0, 10, 0);
statusLayout->setSpacing(1);
statusbar->setObjectName("StatusBar");
statusbar->setStyleSheet(
"QWidget#StatusBar { background-color: transparent; }");
statusLayout->addWidget(m_connectedDeviceCountLabel);
StatusBalloon *statusBalloon = StatusBalloon::sharedInstance();
statusLayout->addWidget(statusBalloon->getButton());
ZIconWidget *welcomeMenuSwitch = new ZIconWidget(
QIcon(":/resources/icons/LetsIconsHorizontalDownLeftMainLight.png"),
"Switch to Welcome Menu");
connect(welcomeMenuSwitch, &ZIconWidget::clicked, this,
[this, welcomeMenuSwitch]() {
if (m_mainStackedWidget->currentIndex() != 0) {
welcomeMenuSwitch->setToolTip(
"Switch to Connected Devices");
return showWelcomeTab();
}
welcomeMenuSwitch->setToolTip("Switch to Welcome Menu");
showConnectedDevicesTab();
});
connect(m_ZTabWidget, &ZTabWidget::currentChanged, this,
[welcomeMenuSwitch](int index) {
if (index != 0) {
return welcomeMenuSwitch->hide();
}
welcomeMenuSwitch->show();
});
statusLayout->addWidget(welcomeMenuSwitch);
statusLayout->addStretch(1);
QLabel *appVersionLabel = new QLabel(QString("v%1").arg(APP_VERSION));
appVersionLabel->setContentsMargins(5, 0, 5, 0);
appVersionLabel->setStyleSheet(
"QLabel:hover { background-color : #13131319; }");
statusLayout->addWidget(appVersionLabel);
statusLayout->addWidget(githubButton);
statusLayout->addWidget(settingsButton);
mainLayout->addWidget(statusbar);
#ifdef __linux__
QList<QString> mounted_iFusePaths = iFuseManager::getMountPoints();
for (const QString &path : mounted_iFusePaths) {
auto *p = new iFuseDiskUnmountButton(path);
statusLayout->addWidget(p);
connect(p, &iFuseDiskUnmountButton::clicked, this,
[this, p, path, statusLayout]() {
bool ok = iFuseManager::linuxUnmount(path);
if (!ok) {
QMessageBox::warning(nullptr, "Unmount Failed",
"Failed to unmount iFuse at " +
path + ". Please try again.");
return;
}
statusLayout->removeWidget(p);
p->deleteLater();
});
}
#endif
#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
createMenus();
UpdateProcedure updateProcedure;
bool packageManagerManaged = false;
bool isPortable = false;
bool skipPrerelease = true;
#ifdef WIN32
isPortable = !is_iDescriptorInstalled();
qDebug() << "isPortable=" << isPortable;
#endif
/*
struct UpdateProcedure {
bool openFile;
bool openFileDir;
bool quitApp;
QString boxInformativeText;
QString boxText;
};
*/
switch (ZUpdater::detectPlatform()) {
case Platform::Windows:
updateProcedure = UpdateProcedure{
!isPortable,
isPortable,
!isPortable,
isPortable ? "New portable version downloaded, app location will "
"be shown after this message"
: "The application will now quit to install the update.",
isPortable ? "New portable version downloaded"
: "Do you want to install the downloaded update now?",
};
break;
// todo: adjust for pkg managers
case Platform::MacOS:
updateProcedure = UpdateProcedure{
true,
false,
true,
"The application will now quit and open .dmg file downloaded to "
"\"Downloads\" from there you can drag it to Applications to "
"install.",
"Update downloaded would you like to quit and install the update?",
};
break;
case Platform::Linux:
// currently only on linux (arch aur) is enabled
#ifdef PACKAGE_MANAGER_MANAGED
packageManagerManaged = true;
#endif
updateProcedure = UpdateProcedure{
true,
false,
true,
"AppImages we ship are not updateable. New version is downloaded "
"to "
"\"Downloads\". You can start using the new version by launching "
"it "
"from there. You can delete this AppImage version if you like.",
"Update downloaded would you like to quit and open the new "
"version?",
};
break;
default:
updateProcedure = UpdateProcedure{
false, false, false, "", "",
};
}
m_updater = new ZUpdater("iDescriptor/iDescriptor", APP_VERSION,
"iDescriptor", updateProcedure, isPortable,
packageManagerManaged, skipPrerelease, this);
#if defined(PACKAGE_MANAGER_MANAGED) && defined(__linux__)
m_updater->setPackageManagerManagedMessage(
QString(
"You seem to have installed iDescriptor using a package manager. "
"Please use %1 to update it.")
.arg(PACKAGE_MANAGER_HINT));
#endif
QString lastAppVersion = SettingsManager::sharedInstance()->appVersion();
bool shouldShowReleaseChangelog = lastAppVersion != APP_VERSION;
SettingsManager::sharedInstance()->setAppVersion(APP_VERSION);
if (shouldShowReleaseChangelog) {
connect(
m_updater, &ZUpdater::dataAvailable, this,
[this](const QJsonDocument data, bool isUpdateAvailable) {
if (!isUpdateAvailable) {
ReleaseChangelogDialog dialog(data, this);
dialog.exec();
}
},
Qt::SingleShotConnection);
}
SettingsManager::sharedInstance()->doIfEnabled(
SettingsManager::Setting::AutoCheckUpdates, [this]() {
qDebug() << "Checking for updates...";
m_updater->checkForUpdates();
});
// ═══════════════════════════════════════════════════════════════════════
// Upgrade to wireless when a "WIRED" device is removed
// ═══════════════════════════════════════════════════════════════════════
connect(
AppContext::sharedInstance(), &AppContext::deviceRemoved, this,
[](const QString &udid, const std::string &wifiMacAddress,
bool wasWireless) {
if (wasWireless)
return;
QString wifiMac = QString::fromStdString(wifiMacAddress);
NetworkDevice dev =
NetworkDeviceProvider::sharedInstance()->getNetworkDeviceByMac(
wifiMac);
if (!dev.isValid()) {
qDebug() << "No valid network device found for UDID" << udid
<< "with Wi-Fi MAC" << wifiMac
<< "Not trying to upgrade to wireless connection.";
return;
}
qDebug()
<< "Trying to upgrade device to wireless connection for UDID"
<< udid;
// FIXME: maybe ignore iOS 15 and lower?
AppContext::sharedInstance()->tryToConnectToNetworkDevice(dev);
});
// ═══════════════════════════════════════════════════════════════════════
// Add a wireless device
// ═══════════════════════════════════════════════════════════════════════
connect(
NetworkDeviceProvider::sharedInstance(),
&NetworkDeviceProvider::deviceAdded, this,
[this](const NetworkDevice &device) {
if (!SettingsManager::sharedInstance()
->autoConnectWirelessDevices())
return;
if (auto existingDevice =
AppContext::sharedInstance()->getDeviceByMacAddress(
device.macAddress)) {
if (existingDevice->deviceInfo.isWireless) {
qDebug()
<< "Ignoring wireless device with MAC:"
<< device.macAddress << "as it's already initialized";
} else {
qDebug() << "Prefering wired connection on device with MAC:"
<< device.macAddress;
}
return;
}
AppContext::sharedInstance()->tryToConnectToNetworkDevice(device);
});
}
void MainWindow::createMenus()
{
#ifdef Q_OS_MAC
QMenu *actionsMenu = menuBar()->addMenu("&Actions");
QAction *aboutAct = new QAction("&About iDescriptor", this);
connect(aboutAct, &QAction::triggered, this, [this]() {
QMessageBox::about(this, "iDescriptor",
"A free, open-source, and cross-platform "
"iDevice management tool.");
});
actionsMenu->addAction(aboutAct);
#endif
}
void MainWindow::updateNoDevicesConnected()
{
qDebug() << "No devices connected? "
<< AppContext::sharedInstance()->noDevicesConnected();
if (AppContext::sharedInstance()->noDevicesConnected()) {
m_connectedDeviceCountLabel->setText("iDescriptor: no devices");
return m_mainStackedWidget->setCurrentIndex(0); // Show Welcome page
}
int deviceCount = AppContext::sharedInstance()->getConnectedDeviceCount();
m_connectedDeviceCountLabel->setText(
"iDescriptor: " + QString::number(deviceCount) +
(deviceCount == 1 ? " device" : " devices") + " connected");
m_mainStackedWidget->setCurrentIndex(1); // Show device list page
}
void MainWindow::closeEvent(QCloseEvent *event)
{
if (StatusBalloon::sharedInstance()->hasActiveProcesses()) {
auto reply = QMessageBox::question(
this, tr("Transfers in Progress"),
tr("There are import/export operations in progress.\n"
"Do you really want to quit? This will cancel them."),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (reply == QMessageBox::No) {
event->ignore();
return;
}
IOManagerClient::sharedInstance()->cancelAllJobs();
}
QMainWindow::closeEvent(event);
}
void MainWindow::raiseDeviceTab()
{
m_ZTabWidget->setCurrentIndex(0);
raise();
activateWindow();
}
void MainWindow::showConnectedDevicesTab()
{
m_mainStackedWidget->setCurrentIndex(1);
}
void MainWindow::showWelcomeTab() { m_mainStackedWidget->setCurrentIndex(0); }
MainWindow::~MainWindow()
{
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
irecv_device_event_unsubscribe(context);
#endif
delete m_updater;
// sleep(2);
}
void MainWindow::handleShowSleepyDeviceWarning()
{
static bool widgetAlreadyVisible = false;
/* one minute cooldown to prevent spamming */
static const int kMinIntervalMs = 60 * 1000;
static QDateTime lastShown;
const auto now = QDateTime::currentDateTimeUtc();
if (lastShown.isValid() && lastShown.msecsTo(now) < kMinIntervalMs) {
qDebug() << "Ignoring sleepy device warning dialog.";
return;
}
lastShown = now;
if (SettingsManager::sharedInstance()->isSleepyDeviceWarningDismissed() ||
widgetAlreadyVisible) {
return;
}
widgetAlreadyVisible = true;
DeviceSleepWarningWidget(this).exec();
widgetAlreadyVisible = false;
}