mirror of
https://github.com/iDescriptor/iDescriptor.git
synced 2026-06-21 19:35:49 +08:00
@@ -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"
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
Vendored
+2
-1
@@ -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",
|
||||
|
||||
Vendored
+3
-1
@@ -202,6 +202,8 @@
|
||||
"qimage": "cpp",
|
||||
"qabstractbutton": "cpp",
|
||||
"qtnetwork": "cpp",
|
||||
"qtcore": "cpp"
|
||||
"qtcore": "cpp",
|
||||
"qbluetoothdevicediscoveryagent": "cpp",
|
||||
"qbluetoothuuid": "cpp"
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+1
-1
Submodule lib/zupdater updated: 3821cf82c8...61aea855c8
@@ -1,36 +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 "detailwindow.h"
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -1,38 +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/>.
|
||||
*/
|
||||
|
||||
#ifndef DETAILWINDOW_H
|
||||
#define DETAILWINDOW_H
|
||||
|
||||
#include <QMainWindow>
|
||||
|
||||
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
|
||||
+14
-12
@@ -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<int>(1, device->deviceInfo.batteryInfo.currentBatteryLevel, 100),
|
||||
device->deviceInfo.batteryInfo.isCharging, this);
|
||||
|
||||
+72
-15
@@ -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
|
||||
|
||||
+98
-55
@@ -18,13 +18,16 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "settingsmanager.h"
|
||||
#include <QAbstractButton>
|
||||
#include <QApplication>
|
||||
#include <QGraphicsView>
|
||||
#include <QGuiApplication>
|
||||
#include <QLabel>
|
||||
#include <QMainWindow>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QScreen>
|
||||
#include <QSlider>
|
||||
#include <QSplitter>
|
||||
#include <QSplitterHandle>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 "../../../../"
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+21
-9
@@ -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...";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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 "releasechangelogdialog.h"
|
||||
#include "iDescriptor.h"
|
||||
#include "settingsmanager.h"
|
||||
#include <QApplication>
|
||||
#include <QCheckBox>
|
||||
#include <QDesktopServices>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QUrl>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
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(); }
|
||||
@@ -0,0 +1,33 @@
|
||||
#ifndef RELEASECHANGELOG_H
|
||||
#define RELEASECHANGELOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QJsonArray>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
+37
-2
@@ -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<int>::of(&QSpinBox::valueChanged),
|
||||
this, &SettingsWidget::onSettingChanged);
|
||||
|
||||
connect(m_iconSizeBaseMultiplier,
|
||||
QOverload<double>::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);
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,8 @@ private:
|
||||
// Jailbroken
|
||||
QLineEdit *m_defaultJailbrokenRootPassword;
|
||||
|
||||
QDoubleSpinBox *m_iconSizeBaseMultiplier;
|
||||
|
||||
// Buttons
|
||||
QPushButton *m_checkUpdatesButton;
|
||||
QPushButton *m_resetButton;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <QFile>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHostAddress>
|
||||
#include <QInputDialog>
|
||||
#include <QLabel>
|
||||
#include <QMenu>
|
||||
#include <QProcess>
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user