diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75d3068..067eba0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,17 @@ name: iDescriptor CI (Linux) on: workflow_dispatch: + push: + paths: + - "**.cpp" + - "**.h" + - "**.hpp" + - "**.cxx" + - "**.cc" + - "CMakeLists.txt" + - "**.cmake" + - ".github/workflows/ci.yml" + env: QT_VERSION: "6.7.2" GO_VERSION: "1.23.0" diff --git a/.gitmodules b/.gitmodules index 1e3396f..a5f05f3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,9 +4,9 @@ [submodule "lib/ipatool-go"] path = lib/ipatool-go url = https://github.com/uncor3/libipatool-go.git -[submodule "lib/zupdater"] - path = lib/zupdater - url = https://github.com/uncor3/ZUpdater [submodule "lib/win-ifuse"] path = lib/win-ifuse url = https://github.com/uncor3/win-ifuse.git +[submodule "lib/zupdater"] + path = lib/zupdater + url = https://github.com/libZQT/ZUpdater diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 0b6e02f..64890af 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -12,7 +12,8 @@ "/usr/include/qt6/**", "/usr/include/qt6/QtBluetooth/**", "${workspaceFolder}/lib/zupdater/src", - "/usr/include/qt6/QtConcurrent" + "/usr/include/qt6/QtConcurrent", + "/usr/include/qt6" ], "defines": [], "compilerPath": "/usr/bin/gcc", diff --git a/.vscode/settings.json b/.vscode/settings.json index 4ec39cc..912d071 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -202,6 +202,8 @@ "qimage": "cpp", "qabstractbutton": "cpp", "qtnetwork": "cpp", - "qtcore": "cpp" + "qtcore": "cpp", + "qbluetoothdevicediscoveryagent": "cpp", + "qbluetoothuuid": "cpp" } } diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d2d993..327ce56 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(iDescriptor VERSION 0.1.0 LANGUAGES CXX) +project(iDescriptor VERSION 0.1.2 LANGUAGES CXX) # Feature options option(ENABLE_RECOVERY_DEVICE_SUPPORT "Enable recovery device support (requires libirecovery)" ON) @@ -14,8 +14,8 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) if (APPLE) - # Target at least macOS 14.0 - set(CMAKE_OSX_DEPLOYMENT_TARGET "14.0") + # Target at least macOS 13.0 + set(CMAKE_OSX_DEPLOYMENT_TARGET "13.0") endif() # Platform-specific paths for libraries built from source @@ -55,7 +55,7 @@ endforeach() list(APPEND _qt_pkg_dirs ${CUSTOM_PKGCONFIG_PATH}) find_package(PkgConfig REQUIRED) -find_package(Qt6 REQUIRED COMPONENTS Widgets Multimedia MultimediaWidgets Network QuickControls2 SerialPort Positioning Location QuickWidgets) +find_package(Qt6 REQUIRED COMPONENTS Widgets Multimedia MultimediaWidgets Network QuickControls2 SerialPort Positioning Location QuickWidgets) # Add QTermWidget # Prefer CMake-native qtermwidget6, fallback to pkg-config if needed diff --git a/lib/zupdater b/lib/zupdater index 3821cf8..61aea85 160000 --- a/lib/zupdater +++ b/lib/zupdater @@ -1 +1 @@ -Subproject commit 3821cf82c850b2e90bdc78a926b7e533b96466f7 +Subproject commit 61aea855c82a67a3536ec00aca7acd583bcbfc83 diff --git a/src/detailwindow.cpp b/src/detailwindow.cpp deleted file mode 100644 index 0f34620..0000000 --- a/src/detailwindow.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * 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 . - */ - -#include "detailwindow.h" -#include -#include -#include - -DetailWindow::DetailWindow(const QString &title, int userId, QWidget *parent) - : QMainWindow(parent), m_title(title), m_userId(userId) -{ - - setWindowTitle(m_title); - - QLabel *label = new QLabel(QString("User ID: %1").arg(m_userId)); - QWidget *central = new QWidget; - QVBoxLayout *layout = new QVBoxLayout(central); - layout->addWidget(label); - setCentralWidget(central); -} diff --git a/src/detailwindow.h b/src/detailwindow.h deleted file mode 100644 index 2bee3b9..0000000 --- a/src/detailwindow.h +++ /dev/null @@ -1,38 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * 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 . - */ - -#ifndef DETAILWINDOW_H -#define DETAILWINDOW_H - -#include - -class DetailWindow : public QMainWindow -{ - Q_OBJECT -public: - explicit DetailWindow(const QString &title, int userId, - QWidget *parent = nullptr); - -signals: -private: - QString m_title; - int m_userId; -}; - -#endif // DETAILWINDOW_H diff --git a/src/deviceinfowidget.cpp b/src/deviceinfowidget.cpp index 7f02ea0..2ef7417 100644 --- a/src/deviceinfowidget.cpp +++ b/src/deviceinfowidget.cpp @@ -76,20 +76,19 @@ DeviceInfoWidget::DeviceInfoWidget(iDescriptorDevice *device, QWidget *parent) ZIconWidget *shutdownBtn = new ZIconWidget( QIcon(":/resources/icons/IcOutlinePowerSettingsNew.png"), "Shutdown", - this); - shutdownBtn->setIconSize(QSize(20, 20)); + 1.0, this); connect(shutdownBtn, &ZIconWidget::clicked, this, [device]() { ToolboxWidget::shutdownDevice(device); }); - ZIconWidget *restartBtn = new ZIconWidget( - QIcon(":/resources/icons/IcTwotoneRestartAlt.png"), "Restart", this); - restartBtn->setIconSize(QSize(20, 20)); + ZIconWidget *restartBtn = + new ZIconWidget(QIcon(":/resources/icons/IcTwotoneRestartAlt.png"), + "Restart", 1.0, this); connect(restartBtn, &ZIconWidget::clicked, this, [device]() { ToolboxWidget::restartDevice(device); }); - ZIconWidget *recoveryBtn = new ZIconWidget( - QIcon(":/resources/icons/HugeiconsWrench01.png"), "Recovery", this); - recoveryBtn->setIconSize(QSize(20, 20)); + ZIconWidget *recoveryBtn = + new ZIconWidget(QIcon(":/resources/icons/HugeiconsWrench01.png"), + "Recovery", 1.0, this); connect(recoveryBtn, &ZIconWidget::clicked, this, [device]() { ToolboxWidget::_enterRecoveryMode(device); }); @@ -144,10 +143,12 @@ DeviceInfoWidget::DeviceInfoWidget(iDescriptorDevice *device, QWidget *parent) m_chargingStatusLabel = new QLabel(device->deviceInfo.batteryInfo.isCharging ? "Charging" : "Not Charging"); + m_chargingStatusLabel->setStyleSheet( device->deviceInfo.batteryInfo.isCharging ? QString("color: %1;").arg(COLOR_GREEN.name()) - : "color: white;"); + : QString("color: %1;") + .arg(qApp->palette().color(QPalette::WindowText).name())); // Create the layout without a parent widget QHBoxLayout *chargingLayout = new QHBoxLayout(); @@ -155,9 +156,10 @@ DeviceInfoWidget::DeviceInfoWidget(iDescriptorDevice *device, QWidget *parent) chargingLayout->setSpacing(5); // Create icon label - m_lightningIconLabel = new ZIconLabel( - QIcon(":/resources/icons/MdiLightningBolt.png"), " Charging", this); - m_lightningIconLabel->setFixedSize(QSize(20, 20)); + m_lightningIconLabel = + new ZIconLabel(QIcon(":/resources/icons/MdiLightningBolt.png"), + " Charging", 1.0, this); + m_batteryWidget = new BatteryWidget( qBound(1, device->deviceInfo.batteryInfo.currentBatteryLevel, 100), device->deviceInfo.batteryInfo.isCharging, this); diff --git a/src/exportmanager.cpp b/src/exportmanager.cpp index c37ff01..9f6713f 100644 --- a/src/exportmanager.cpp +++ b/src/exportmanager.cpp @@ -208,23 +208,65 @@ ExportResult ExportManager::exportSingleItem(iDescriptorDevice *device, QString outputPath = QDir(destinationDir).filePath(item.suggestedFileName); outputPath = generateUniqueOutputPath(outputPath); result.outputFilePath = outputPath; - + QDateTime modificationTime; + QDateTime birthTime; // Get file size first - char **info = nullptr; - afc_error_t infoResult = ServiceManager::safeAfcGetFileInfo( - device, item.sourcePathOnDevice.toUtf8().constData(), &info, altAfc); + // Example { + // "st_size": 64523, + // "st_blocks": 128, + // "st_nlink": 1, + // "st_ifmt": "S_IFREG", + // "st_mtime": 1754987735634348907, + // "st_birthtime": 1754987735633715011 + // } - qint64 totalFileSize = 0; - if (infoResult == AFC_E_SUCCESS && info) { - for (int i = 0; info[i]; i += 2) { - if (strcmp(info[i], "st_size") == 0) { - totalFileSize = QString::fromUtf8(info[i + 1]).toLongLong(); - break; - } - } - afc_dictionary_free(info); + plist_t info = nullptr; + afc_error_t infoResult = ServiceManager::safeAfcGetFileInfoPlist( + device, item.sourcePathOnDevice.toUtf8().constData(), &info, altAfc); + quint64 totalFileSize = 0; + if (infoResult != AFC_E_SUCCESS || !info) { + qDebug() << "File info retrieval failed for" << item.sourcePathOnDevice; + return result; } + PlistNavigator fileInfo = PlistNavigator(info); + + bool valid = fileInfo["st_size"].valid(); + if (!valid) { + qDebug() << "File size info not valid for" << item.sourcePathOnDevice; + if (info) + plist_free(info); + return result; + } + + totalFileSize = fileInfo["st_size"].getUInt(); + + valid = fileInfo["st_mtime"].valid(); + if (!valid) { + qDebug() << "File modification time info not valid for" + << item.sourcePathOnDevice; + if (info) + plist_free(info); + return result; + } + + uint64_t modTimeNs = fileInfo["st_mtime"].getUInt(); + // The timestamp from the device is in nanoseconds, convert to seconds + modificationTime = QDateTime::fromSecsSinceEpoch(modTimeNs / 1000000000); + + valid = fileInfo["st_birthtime"].valid(); + if (!valid) { + qDebug() << "File birth time info not valid for" + << item.sourcePathOnDevice; + if (info) + plist_free(info); + return result; + } + uint64_t birthTimeNs = fileInfo["st_birthtime"].getUInt(); + birthTime = QDateTime::fromSecsSinceEpoch(birthTimeNs / 1000000000); + + plist_free(info); + // Open file on device uint64_t handle = 0; afc_error_t openResult = ServiceManager::safeAfcFileOpen( @@ -251,7 +293,7 @@ ExportResult ExportManager::exportSingleItem(iDescriptorDevice *device, char buffer[8192]; uint32_t bytesRead = 0; - qint64 totalBytes = 0; + quint64 totalBytes = 0; while (true) { // Check for cancellation during file copy @@ -291,10 +333,25 @@ ExportResult ExportManager::exportSingleItem(iDescriptorDevice *device, } } - // Clean up outputFile.close(); ServiceManager::safeAfcFileClose(device, handle, altAfc); + // reopen is required for timestamps + QFile reopen(outputPath); + reopen.open(QIODevice::ReadOnly); + if (modificationTime.isValid()) { + if (!reopen.setFileTime(modificationTime, + QFileDevice::FileModificationTime)) { + qWarning() << "Could not set modification time for" << outputPath; + } + } + if (birthTime.isValid()) { + // fails on linux + if (!reopen.setFileTime(birthTime, QFileDevice::FileBirthTime)) { + qWarning() << "Could not set birth time for" << outputPath; + } + } + if (totalBytes == 0) { result.errorMessage = "No data read from device file"; outputFile.remove(); // Clean up empty file diff --git a/src/iDescriptor-ui.h b/src/iDescriptor-ui.h index 952e544..7140264 100644 --- a/src/iDescriptor-ui.h +++ b/src/iDescriptor-ui.h @@ -18,13 +18,16 @@ */ #pragma once +#include "settingsmanager.h" #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -42,8 +45,6 @@ #define COLOR_BLUE QColor("#2b5693") #define COLOR_ACCENT_BLUE QColor("#0b5ed7") -// A custom QGraphicsView that keeps the content fitted with aspect ratio on -// resize class ResponsiveGraphicsView : public QGraphicsView { public: @@ -72,8 +73,6 @@ signals: void clicked(); protected: - // On mouse release, if the click is inside the widget, emit the clicked - // signal void mouseReleaseEvent(QMouseEvent *event) override { if (event->button() == Qt::LeftButton && @@ -94,43 +93,45 @@ public: void setThemable(bool themable) { m_themable = themable; } - QPixmap getThemedPixmap(const QSize &size, const QPalette &palette) const + QPixmap getThemedPixmap(const QSize &logicalSize, const QPalette &palette, + qreal dpr = 1.0) const { - // If not themable, return the original pixmap without color filling. - if (!m_themable) { - return QIcon::pixmap(size); - } + QSize physical = logicalSize * dpr; - QPixmap pixmap = QIcon::pixmap(size); - if (pixmap.isNull()) { + QPixmap pixmap = QIcon::pixmap(physical); + if (pixmap.isNull()) return pixmap; - } - // Get the appropriate icon color based on theme + pixmap.setDevicePixelRatio(dpr); + + if (!m_themable) + return pixmap; + + // theme color QColor iconColor = palette.color(QPalette::WindowText); - // Create a colored version of the icon - QPixmap coloredPixmap(pixmap.size()); - coloredPixmap.fill(Qt::transparent); + QPixmap colored(pixmap.size()); + colored.setDevicePixelRatio(dpr); + colored.fill(Qt::transparent); - QPainter iconPainter(&coloredPixmap); - iconPainter.setCompositionMode(QPainter::CompositionMode_SourceOver); - iconPainter.drawPixmap(0, 0, pixmap); - iconPainter.setCompositionMode(QPainter::CompositionMode_SourceIn); - iconPainter.fillRect(coloredPixmap.rect(), iconColor); + QPainter p(&colored); + p.setRenderHint(QPainter::Antialiasing); + p.drawPixmap(0, 0, pixmap); + p.setCompositionMode(QPainter::CompositionMode_SourceIn); + p.fillRect(colored.rect(), iconColor); + p.end(); - return coloredPixmap; + return colored; } - void paint(QPainter *painter, const QRect &rect, - const QPalette &palette) const + void paint(QPainter *painter, const QRect &logicalRect, + const QPalette &palette, qreal dpr = 1.0) const { - QPixmap themedPixmap = getThemedPixmap(rect.size(), palette); - if (!themedPixmap.isNull()) { - painter->drawPixmap(rect, themedPixmap); - } else { - QIcon::paint(painter, rect); - } + QPixmap pm = getThemedPixmap(logicalRect.size(), palette, dpr); + if (pm.isNull()) + return; + + painter->drawPixmap(logicalRect, pm); } private: @@ -141,66 +142,93 @@ class ZIconWidget : public QAbstractButton { Q_OBJECT public: - ZIconWidget(const QIcon &icon, const QString &tooltip, - QWidget *parent = nullptr) - : QAbstractButton(parent), m_icon(icon) + ZIconWidget(const QIcon &icon, const QString &tooltip = "", + qreal iconSizeMultiplier = 1.0, QWidget *parent = nullptr) + : QAbstractButton(parent), m_icon(icon), + m_iconSizeMultiplier(iconSizeMultiplier) { - setToolTip(tooltip); - setFixedSize(32, 32); - setIconSize(QSize(24, 24)); + if (!tooltip.isEmpty()) + setToolTip(tooltip); + + updateIconSize(); setCursor(Qt::PointingHandCursor); + connect(qApp, &QApplication::paletteChanged, this, - [this]() { update(); }); + [this] { update(); }); + connect(qApp, &QApplication::fontChanged, this, + [this] { updateIconSize(); }); } - void setIcon(const QIcon &icon) + void setIcon(const ZIcon &icon) { - m_icon = ZIcon(icon); + m_icon = icon; update(); } + void setIconSizeMultiplier(qreal multiplier) + { + m_iconSizeMultiplier = multiplier; + updateIconSize(); + } + protected: void paintEvent(QPaintEvent *event) override { - Q_UNUSED(event) + Q_UNUSED(event); QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); - // Draw background circle when hovered or pressed if (underMouse() || isDown()) { - QColor bgColor = palette().color(QPalette::Highlight); - bgColor.setAlpha(isDown() ? 60 : 30); - painter.setBrush(bgColor); + QColor bg = palette().color(QPalette::Highlight); + bg.setAlpha(isDown() ? 60 : 30); + painter.setBrush(bg); painter.setPen(Qt::NoPen); painter.drawEllipse(rect().adjusted(2, 2, -2, -2)); } QRect iconRect = rect(); - iconRect.setSize(iconSize()); // Use iconSize() from QAbstractButton + iconRect.setSize(m_iconSize); iconRect.moveCenter(rect().center()); - m_icon.paint(&painter, iconRect, palette()); + m_icon.paint(&painter, iconRect, palette(), devicePixelRatioF()); + } + +private: + void updateIconSize() + { + QFontMetrics fm(font()); + int base = + qRound(fm.height() * m_iconSizeMultiplier * + SettingsManager::sharedInstance()->iconSizeBaseMultiplier()); + + m_iconSize = QSize(base, base); + + setFixedSize(base + 10, base + 10); + + update(); } private: ZIcon m_icon; + QSize m_iconSize; + qreal m_iconSizeMultiplier; }; -// Add this new class for display-only icons class ZIconLabel : public QLabel { Q_OBJECT public: ZIconLabel(const QIcon &icon, const QString &tooltip, - QWidget *parent = nullptr) - : QLabel(parent), m_icon(icon), m_iconSize(24, 24) + qreal iconSizeMultiplier = 1.0, QWidget *parent = nullptr) + : QLabel(parent), m_icon(icon), m_iconSizeMultiplier(iconSizeMultiplier) { setToolTip(tooltip); - // setFixedSize(32, 32); + updateIconSize(); connect(qApp, &QApplication::paletteChanged, this, [this]() { update(); }); + connect(qApp, &QApplication::fontChanged, this, + [this]() { updateIconSize(); }); } - void setIcon(const QIcon &icon) { m_icon = ZIcon(icon); @@ -213,10 +241,10 @@ public: update(); } - void setIconSize(const QSize &size) + void setIconSizeMultiplier(qreal multiplier) { - m_iconSize = size; - update(); + m_iconSizeMultiplier = multiplier; + updateIconSize(); } protected: @@ -230,12 +258,27 @@ protected: iconRect.setSize(m_iconSize); iconRect.moveCenter(rect().center()); - m_icon.paint(&painter, iconRect, palette()); + m_icon.paint(&painter, iconRect, palette(), devicePixelRatioF()); } private: + void updateIconSize() + { + QFontMetrics fm(font()); + int base = + qRound(fm.height() * m_iconSizeMultiplier * + SettingsManager::sharedInstance()->iconSizeBaseMultiplier()); + + m_iconSize = QSize(base, base); + + setFixedSize(base + 10, base + 10); + + update(); + } + ZIcon m_icon; QSize m_iconSize; + qreal m_iconSizeMultiplier; }; enum class iDescriptorTool { diff --git a/src/iDescriptor.h b/src/iDescriptor.h index defb1cf..d00dc82 100644 --- a/src/iDescriptor.h +++ b/src/iDescriptor.h @@ -54,6 +54,8 @@ "https://raw.githubusercontent.com/iDescriptor/iDescriptor/refs/heads/" \ "main/DeveloperDiskImages.json" +#define DONATE_URL "https://opencollective.com/idescriptor" + // This is because afc_read_directory accepts "/var/mobile/Media" as "/" #define POSSIBLE_ROOT "../../../../" diff --git a/src/ifusediskunmountbutton.cpp b/src/ifusediskunmountbutton.cpp index 965f00e..2dbb4b0 100644 --- a/src/ifusediskunmountbutton.cpp +++ b/src/ifusediskunmountbutton.cpp @@ -25,7 +25,7 @@ iFuseDiskUnmountButton::iFuseDiskUnmountButton(const QString &path, QWidget *parent) : ZIconWidget{QIcon(":/resources/icons/ClarityHardDiskSolidAlerted.png"), - "Unmount iFuse at " + path, parent} + "Unmount iFuse at " + path, 1.0, parent} { setCursor(Qt::PointingHandCursor); setFixedSize(24, 24); diff --git a/src/jailbrokenwidget.cpp b/src/jailbrokenwidget.cpp index de1906e..a06797b 100644 --- a/src/jailbrokenwidget.cpp +++ b/src/jailbrokenwidget.cpp @@ -89,7 +89,7 @@ JailbrokenWidget::createJailbreakTool(const JailbreakToolInfo &info) // Icon (using the theme-aware ZIcon pattern) // ZIconLabel *iconLabel = new ZIconLabel(); - ZIconLabel *iconLabel = new ZIconLabel(QIcon(), nullptr, this); + ZIconLabel *iconLabel = new ZIconLabel(QIcon(), nullptr, 1.5, this); // iconLabel->setAlignment(Qt::AlignCenter); // ZIcon toolIcon(QIcon(info.iconPath)); @@ -140,8 +140,7 @@ JailbrokenWidget::createJailbreakTool(const JailbreakToolInfo &info) iconLabel->setIcon( QIcon(":/resources/icons/IconParkTwotoneMoreTwo.png")); } - iconLabel->setFixedSize(60, 60); - iconLabel->setIconSize(QSize(45, 45)); + iconLabel->setIconSizeMultiplier(2); return b; } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 6ccdcdc..c82d111 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -19,16 +19,15 @@ #include "mainwindow.h" #include "./ui_mainwindow.h" -#include "detailwindow.h" -#include "ifusediskunmountbutton.h" -#include "ifusemanager.h" -#include "settingswidget.h" - #include "appswidget.h" #include "devicemanagerwidget.h" #include "iDescriptor-ui.h" #include "iDescriptor.h" +#include "ifusediskunmountbutton.h" +#include "ifusemanager.h" #include "jailbrokenwidget.h" +#include "releasechangelogdialog.h" +#include "settingswidget.h" #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT #include "libirecovery.h" #endif @@ -55,7 +54,7 @@ void handleCallback(const idevice_event_t *event, void *userData) { - printf("Device event received: "); + qDebug() << "Device event received"; switch (event->event) { case IDEVICE_DEVICE_ADD: { @@ -140,7 +139,6 @@ MainWindow::MainWindow(QWidget *parent) const QSize minSize(900, 600); setMinimumSize(minSize); resize(minSize); - m_ZTabWidget = new ZTabWidget(this); m_ZTabWidget->setAttribute(Qt::WA_ContentsMarginsRespectsSafeArea, false); @@ -179,7 +177,6 @@ MainWindow::MainWindow(QWidget *parent) ZIconWidget *settingsButton = new ZIconWidget( QIcon(":/resources/icons/MingcuteSettings7Line.png"), "Settings"); settingsButton->setCursor(Qt::PointingHandCursor); - settingsButton->setFixedSize(24, 24); connect(settingsButton, &ZIconWidget::clicked, this, [this]() { SettingsManager::sharedInstance()->showSettingsDialog(); }); @@ -187,7 +184,6 @@ MainWindow::MainWindow(QWidget *parent) ZIconWidget *githubButton = new ZIconWidget( QIcon(":/resources/icons/MdiGithub.png"), "iDescriptor on GitHub"); githubButton->setCursor(Qt::PointingHandCursor); - githubButton->setFixedSize(24, 24); connect(githubButton, &ZIconWidget::clicked, this, []() { QDesktopServices::openUrl(QUrl(REPO_URL)); }); @@ -325,6 +321,22 @@ MainWindow::MainWindow(QWidget *parent) .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..."; diff --git a/src/privateinfolabel.cpp b/src/privateinfolabel.cpp index 6c7c9bc..dcc188f 100644 --- a/src/privateinfolabel.cpp +++ b/src/privateinfolabel.cpp @@ -32,7 +32,7 @@ PrivateInfoLabel::PrivateInfoLabel(const QString &fullText, QWidget *parent) layout->addWidget(m_textLabel); m_toggleButton = new ZIconWidget( - QIcon(":/resources/icons/ClarityEyeHideLine.png"), "Show", this); + QIcon(":/resources/icons/ClarityEyeHideLine.png"), "Show", 1.0, this); m_toggleButton->setIconSize(QSize(20, 20)); connect(m_toggleButton, &ZIconWidget::clicked, this, &PrivateInfoLabel::toggleVisibility); diff --git a/src/releasechangelogdialog.cpp b/src/releasechangelogdialog.cpp new file mode 100644 index 0000000..811054c --- /dev/null +++ b/src/releasechangelogdialog.cpp @@ -0,0 +1,115 @@ +/* + * iDescriptor: A free and open-source idevice management tool. + * + * Copyright (C) 2025 Uncore + * + * 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 . + */ + +#include "releasechangelogdialog.h" +#include "iDescriptor.h" +#include "settingsmanager.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +ReleaseChangelogDialog::ReleaseChangelogDialog(QJsonDocument data, + QWidget *parent) + : QDialog(parent) +{ + setupUI(data); +} + +ReleaseChangelogDialog::~ReleaseChangelogDialog() {} + +void ReleaseChangelogDialog::setupUI(const QJsonDocument &data) +{ + setWindowTitle("iDescriptor - Release Changelog"); + setModal(true); + setMinimumSize(500, 250); + resize(600, 300); + + m_mainLayout = new QVBoxLayout(this); + m_mainLayout->setContentsMargins(20, 20, 20, 20); + m_mainLayout->setSpacing(15); + + m_titleLabel = + new QLabel(QString("iDescriptor has been updated to v") + APP_VERSION); + m_titleLabel->setAlignment(Qt::AlignCenter); + m_titleLabel->setStyleSheet( + "font-size: 18px; font-weight: bold; margin-bottom: 10px;"); + m_mainLayout->addWidget(m_titleLabel); + + QString description = "Failed to load changelog data."; + QJsonArray dataArr = data.array(); + if (!dataArr.isEmpty()) { + for (const QJsonValue &releaseVal : dataArr) { + QJsonObject releaseObj = releaseVal.toObject(); + if (!releaseObj.isEmpty()) { + QString tagName = releaseObj.value("tag_name").toString(); + + if (tagName.isEmpty()) { + continue; + } + if (tagName == QString("v") + APP_VERSION) { + if (releaseObj.value("body").isUndefined()) + break; + description = releaseObj.value("body").toString(); + break; + } + } + } + } + + m_descriptionLabel = new QLabel(description); + m_descriptionLabel->setAlignment(Qt::AlignCenter); + m_descriptionLabel->setWordWrap(true); + m_descriptionLabel->setStyleSheet("font-size: 14px; margin: 10px;"); + m_mainLayout->addWidget(m_descriptionLabel); + + m_mainLayout->addStretch(); + + QHBoxLayout *buttonsLayout = new QHBoxLayout(); + m_skipButton = new QPushButton("Ok, Thanks!"); + m_skipButton->setFixedHeight(40); + + m_donateButton = new QPushButton("Donate"); + m_donateButton->setDefault(true); + m_donateButton->setFixedHeight(40); + + buttonsLayout->addWidget(m_skipButton); + buttonsLayout->addWidget(m_donateButton); + + m_mainLayout->addLayout(buttonsLayout, Qt::AlignCenter); + + connect(m_donateButton, &QPushButton::clicked, this, + &ReleaseChangelogDialog::onDonateClicked); + connect(m_skipButton, &QPushButton::clicked, this, + &ReleaseChangelogDialog::onSkipButtonClicked); +} + +void ReleaseChangelogDialog::onDonateClicked() +{ + QDesktopServices::openUrl(QUrl(DONATE_URL)); + accept(); +} + +void ReleaseChangelogDialog::onSkipButtonClicked() { accept(); } diff --git a/src/releasechangelogdialog.h b/src/releasechangelogdialog.h new file mode 100644 index 0000000..c6acb15 --- /dev/null +++ b/src/releasechangelogdialog.h @@ -0,0 +1,33 @@ +#ifndef RELEASECHANGELOG_H +#define RELEASECHANGELOG_H + +#include +#include +#include +#include +#include + +class ReleaseChangelogDialog : public QDialog +{ + Q_OBJECT +public: + explicit ReleaseChangelogDialog(QJsonDocument data, + QWidget *parent = nullptr); + + ~ReleaseChangelogDialog(); +signals: + +private: + void setupUI(const QJsonDocument &data); + + QVBoxLayout *m_mainLayout = nullptr; + QPushButton *m_skipButton = nullptr; + QPushButton *m_donateButton = nullptr; + QLabel *m_titleLabel = nullptr; + QLabel *m_descriptionLabel = nullptr; + + void onDonateClicked(); + void onSkipButtonClicked(); +}; + +#endif // RELEASECHANGELOG_H diff --git a/src/settingsmanager.cpp b/src/settingsmanager.cpp index 5b7158d..a8be4f6 100644 --- a/src/settingsmanager.cpp +++ b/src/settingsmanager.cpp @@ -368,4 +368,26 @@ void SettingsManager::clearRecentLocations() { m_settings->remove("recentLocations"); m_settings->sync(); +} + +QString SettingsManager::appVersion() +{ + return m_settings->value("__APP_VERSION__", "").toString(); +} + +void SettingsManager::setAppVersion(const QString &version) +{ + m_settings->setValue("__APP_VERSION__", version); + m_settings->sync(); +} + +double SettingsManager::iconSizeBaseMultiplier() const +{ + return m_settings->value("iconSizeBaseMultiplier", 1.0).toDouble(); +} + +void SettingsManager::setIconSizeBaseMultiplier(double multiplier) +{ + m_settings->setValue("iconSizeBaseMultiplier", multiplier); + m_settings->sync(); } \ No newline at end of file diff --git a/src/settingsmanager.h b/src/settingsmanager.h index f0e3208..0e3b47a 100644 --- a/src/settingsmanager.h +++ b/src/settingsmanager.h @@ -99,6 +99,12 @@ public: void resetToDefaults(); void clear(); + + QString appVersion(); + void setAppVersion(const QString &version); + + double iconSizeBaseMultiplier() const; + void setIconSizeBaseMultiplier(double multiplier); signals: void favoritePlacesChanged(); void recentLocationsChanged(); diff --git a/src/settingswidget.cpp b/src/settingswidget.cpp index f38002e..1c06e78 100644 --- a/src/settingswidget.cpp +++ b/src/settingswidget.cpp @@ -153,6 +153,29 @@ void SettingsWidget::setupUI() scrollLayout->addWidget(jailbrokenGroup); + // === MISCELLANEOUS SETTINGS === + auto *miscGroup = new QGroupBox("Miscellaneous"); + auto *miscLayout = new QVBoxLayout(miscGroup); + + auto *iconSizeBaseMultiplierLayout = new QHBoxLayout(); + m_iconSizeBaseMultiplier = new QDoubleSpinBox(); + m_iconSizeBaseMultiplier->setRange(1.0, 5.0); + m_iconSizeBaseMultiplier->setSingleStep(0.1); + m_iconSizeBaseMultiplier->setDecimals(1); + m_iconSizeBaseMultiplier->setSuffix("x"); + m_iconSizeBaseMultiplier->setToolTip( + "Adjust the base multiplier for icon sizes. This affects how large " + "icons appear throughout the application. Requires restart to take " + "effect."); + + iconSizeBaseMultiplierLayout->addWidget( + new QLabel("Icon Size Base Multiplier:")); + iconSizeBaseMultiplierLayout->addWidget(m_iconSizeBaseMultiplier); + iconSizeBaseMultiplierLayout->addStretch(); + miscLayout->addLayout(iconSizeBaseMultiplierLayout); + + scrollLayout->addWidget(miscGroup); + scrollLayout->addSpacing(30); // Add a footer Author & Version & app info & app description @@ -163,7 +186,7 @@ void SettingsWidget::setupUI() "© 2025 See AUTHORS for details. Licensed under AGPLv3.") .arg(APP_VERSION)); footerLabel->setAlignment(Qt::AlignCenter); - footerLabel->setStyleSheet("color: gray; font-size: 10pt;"); + footerLabel->setStyleSheet("color: gray; font-size: 8pt;"); scrollLayout->addWidget(footerLabel); // Add stretch to push everything to the top @@ -225,6 +248,8 @@ void SettingsWidget::loadSettings() // Disable apply button initially m_applyButton->setEnabled(false); + + m_iconSizeBaseMultiplier->setValue(sm->iconSizeBaseMultiplier()); } void SettingsWidget::connectSignals() @@ -245,12 +270,20 @@ void SettingsWidget::connectSignals() connect(m_connectionTimeout, QOverload::of(&QSpinBox::valueChanged), this, &SettingsWidget::onSettingChanged); + connect(m_iconSizeBaseMultiplier, + QOverload::of(&QDoubleSpinBox::valueChanged), this, + [this]() { + m_restartRequired = true; + onSettingChanged(); + }); + connect(m_useUnsecureBackend, &QCheckBox::toggled, this, [this]() { // since this is unsafe if its being enabled, show a warning if (m_useUnsecureBackend->isChecked()) { auto reply = QMessageBox::warning( this, "Warning", - "Enabling this will not encrypt your Apple account which is a " + "Enabling this will not encrypt your Apple account which " + "is a " "security risk. Are you sure you want to enable this?", QMessageBox::Yes | QMessageBox::No, QMessageBox::No); @@ -345,6 +378,8 @@ void SettingsWidget::saveSettings() sm->setDefaultJailbrokenRootPassword( m_defaultJailbrokenRootPassword->text()); + sm->setIconSizeBaseMultiplier(m_iconSizeBaseMultiplier->value()); + m_applyButton->setEnabled(false); } diff --git a/src/settingswidget.h b/src/settingswidget.h index 36869aa..82276d9 100644 --- a/src/settingswidget.h +++ b/src/settingswidget.h @@ -66,6 +66,8 @@ private: // Jailbroken QLineEdit *m_defaultJailbrokenRootPassword; + QDoubleSpinBox *m_iconSizeBaseMultiplier; + // Buttons QPushButton *m_checkUpdatesButton; QPushButton *m_resetButton; diff --git a/src/sshterminalwidget.cpp b/src/sshterminalwidget.cpp index f570916..7c043d1 100644 --- a/src/sshterminalwidget.cpp +++ b/src/sshterminalwidget.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -339,6 +340,22 @@ void SSHTerminalWidget::startSSH(const QString &host, uint16_t port) { qDebug() << "Starting SSH to" << host << "on port" << port; + QString defaultPassword = + SettingsManager::sharedInstance()->defaultJailbrokenRootPassword(); + QByteArray passwordBytes = defaultPassword.toUtf8(); + + bool ok; + QString code = + QInputDialog::getText(nullptr, "SSH Root Password", + "Enter the root password: \n(leave empty if you " + "want to use the default)", + QLineEdit::Normal, QString(), &ok); + + if (!ok) { + showError("Root password input canceled"); + return; + } + if (m_sshConnected) return; @@ -385,9 +402,6 @@ void SSHTerminalWidget::startSSH(const QString &host, uint16_t port) qDebug() << "SSH connected successfully, attempting authentication..."; - QString defaultPassword = - SettingsManager::sharedInstance()->defaultJailbrokenRootPassword(); - QByteArray passwordBytes = defaultPassword.toUtf8(); rc = ssh_userauth_password(m_sshSession, nullptr, passwordBytes.constData()); if (rc != SSH_AUTH_SUCCESS) { diff --git a/src/toolboxwidget.cpp b/src/toolboxwidget.cpp index 2bb55d0..d6afc3d 100644 --- a/src/toolboxwidget.cpp +++ b/src/toolboxwidget.cpp @@ -220,7 +220,7 @@ ClickableWidget *ToolboxWidget::createToolbox(iDescriptorTool tool, QVBoxLayout *layout = new QVBoxLayout(b); - ZIconLabel *icon = new ZIconLabel(QIcon(), nullptr, this); + ZIconLabel *icon = new ZIconLabel(QIcon(), nullptr, 1.5, this); QString title; switch (tool) { case iDescriptorTool::Airplayer: @@ -298,8 +298,7 @@ ClickableWidget *ToolboxWidget::createToolbox(iDescriptorTool tool, descLabel->setWordWrap(true); descLabel->setAlignment(Qt::AlignCenter); descLabel->setStyleSheet("color: #666; font-size: 12px;"); - icon->setFixedSize(60, 60); - icon->setIconSize(QSize(45, 45)); + icon->setIconSizeMultiplier(1.90); layout->addWidget(icon, 0, Qt::AlignCenter); layout->addWidget(titleLabel);