From e6b33ffb6ad4787cee5b32a7d4b48df15362ad3b Mon Sep 17 00:00:00 2001 From: uncor3 Date: Mon, 8 Jun 2026 20:58:13 +0000 Subject: [PATCH] feat(DeviceInfo): add DiskUsage component and refactor layout --- src/ui/DeviceInfo.qml | 178 +++++++++++---------- src/ui/DiskUsage.qml | 352 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 448 insertions(+), 82 deletions(-) create mode 100644 src/ui/DiskUsage.qml diff --git a/src/ui/DeviceInfo.qml b/src/ui/DeviceInfo.qml index 58d5d9d..84bdd90 100644 --- a/src/ui/DeviceInfo.qml +++ b/src/ui/DeviceInfo.qml @@ -1,10 +1,11 @@ -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts Item { + id : root required property var info - // property string udid: "" + required property var device function v(key, fallback) { if (!info) return fallback @@ -13,100 +14,113 @@ Item { return val } - RowLayout { - spacing: 12 + ColumnLayout { + anchors.fill: parent + spacing: 20 - DeviceImage { } + RowLayout { + spacing: 12 - ColumnLayout { - spacing: 10 - Layout.fillWidth: true + DeviceImage { + iosVersion: info ? info.ios_version_major : 0 + displayName: v("product_type", "Unknown Device") + } - RowLayout { - Layout.fillWidth: true + ColumnLayout { spacing: 10 - - Label { - text: v("DeviceClass", "TODO") - font.bold: true - elide: Text.ElideRight - Layout.fillWidth: true - } - - Label { - // FIXME: hardcoded - text: "5W/USB" - color: "#666" - } - } - - GridLayout { - id: grid - columns: 4 - columnSpacing: 14 - rowSpacing: 8 Layout.fillWidth: true - // Row 0 - Label { text: "iOS Version:"; font.bold: true } - Label { text: v("ProductVersion", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } - Label { text: "Device Name:"; font.bold: true } - Label { text: v("DeviceName", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } + RowLayout { + Layout.fillWidth: true + spacing: 10 - // Row 1 - Label { text: "Activation State:"; font.bold: true } - Label { text: "TODO"; elide: Text.ElideRight; Layout.fillWidth: true } - Label { text: "Device Class:"; font.bold: true } - Label { text: v("DeviceClass", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } + Label { + text: v("product_type", "TODO") + font.bold: true + elide: Text.ElideRight + Layout.fillWidth: true + } - // Row 2 - Label { text: "Jailbroken:"; font.bold: true } - Label { text: "TODO"; elide: Text.ElideRight; Layout.fillWidth: true } - Label { text: "Model Number:"; font.bold: true } - Label { text: v("ModelNumber", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } + Label { + // FIXME: hardcoded + text: "5W/USB" + color: "#666" + } + } - // Row 3 - Label { text: "CPU Architecture:"; font.bold: true } - Label { text: v("CPUArchitecture", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } - Label { text: "Build Version:"; font.bold: true } - Label { text: v("BuildVersion", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } + GridLayout { + id: grid + columns: 4 + columnSpacing: 14 + rowSpacing: 8 + Layout.fillWidth: true - // Row 4 - Label { text: "Hardware Model:"; font.bold: true } - Label { text: v("HardwareModel", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } - Label { text: "Region:"; font.bold: true } - Label { text: "TODO"; elide: Text.ElideRight; Layout.fillWidth: true } + // Row 0 + Label { text: "iOS Version:"; font.bold: true } + Label { text: v("ProductVersion", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } + Label { text: "Device Name:"; font.bold: true } + Label { text: v("DeviceName", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } - // Row 5 - Label { text: "Hardware Platform:"; font.bold: true } - Label { text: v("HardwarePlatform", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } - Label { text: "Firmware Version:"; font.bold: true } - Label { text: v("FirmwareVersion", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } + // Row 1 + Label { text: "Activation State:"; font.bold: true } + Label { text: v("ActivationState", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } + Label { text: "Device Class:"; font.bold: true } + Label { text: v("DeviceClass", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } - // Row 6 - Label { text: "Bluetooth Address:"; font.bold: true } - Label { text: v("BluetoothAddress", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } - Label { text: "Wi‑Fi Address:"; font.bold: true } - Label { text: v("WiFiAddress", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } + // Row 2 + Label { text: "Jailbroken:"; font.bold: true } + Label { text: v("Jailbroken", "TODO") ? "Yes" : "No"; elide: Text.ElideRight; Layout.fillWidth: true } + Label { text: "Model Number:"; font.bold: true } + Label { text: v("ModelNumber", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } - // Row 7 - Label { text: "Ethernet Address:"; font.bold: true } - Label { text: v("EthernetAddress", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } - Label { text: "Battery Health:"; font.bold: true } - Label { text: "TODO"; elide: Text.ElideRight; Layout.fillWidth: true } + // Row 3 + Label { text: "CPU Architecture:"; font.bold: true } + Label { text: v("CPUArchitecture", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } + Label { text: "Build Version:"; font.bold: true } + Label { text: v("BuildVersion", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } - // Row 8 - Label { text: "Production Device:"; font.bold: true } - Label { text: "TODO"; elide: Text.ElideRight; Layout.fillWidth: true } - Label { text: "Serial Number:"; font.bold: true } - Label { text: "TODO"; elide: Text.ElideRight; Layout.fillWidth: true } + // Row 4 + Label { text: "Hardware Model:"; font.bold: true } + Label { text: v("HardwareModel", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } + Label { text: "Region:"; font.bold: true } + Label { text: v("region", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } - // Row 9 - Label { text: "IMEI:"; font.bold: true } - Label { text: "TODO"; elide: Text.ElideRight; Layout.fillWidth: true } - Label { text: "UDID:"; font.bold: true } - Label { text: v("UniqueDeviceID", "TODO"); elide: Text.ElideMiddle; Layout.fillWidth: true } + // Row 5 + Label { text: "Hardware Platform:"; font.bold: true } + Label { text: v("HardwarePlatform", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } + Label { text: "Firmware Version:"; font.bold: true } + Label { text: v("FirmwareVersion", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } + + // Row 6 + Label { text: "Bluetooth Address:"; font.bold: true } + Label { text: v("BluetoothAddress", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } + Label { text: "Wi‑Fi Address:"; font.bold: true } + Label { text: v("WiFiAddress", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } + + // Row 7 + Label { text: "Ethernet Address:"; font.bold: true } + Label { text: v("EthernetAddress", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } + Label { text: "Battery Health:"; font.bold: true } + Label { text: "TODO"; elide: Text.ElideRight; Layout.fillWidth: true } + + // Row 8 + Label { text: "Production Device:"; font.bold: true } + Label { text: "TODO"; elide: Text.ElideRight; Layout.fillWidth: true } + Label { text: "Serial Number:"; font.bold: true } + Label { text: v("SerialNumber", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } + + // Row 9 + Label { text: "IMEI:"; font.bold: true } + Label { text: v("InternationalMobileEquipmentIdentity", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true } + Label { text: "UDID:"; font.bold: true } + Label { text: v("UniqueDeviceID", "TODO"); elide: Text.ElideMiddle; Layout.fillWidth: true } + } } } + + DiskUsage { + Layout.fillWidth: true + device : root.device + } } } \ No newline at end of file diff --git a/src/ui/DiskUsage.qml b/src/ui/DiskUsage.qml new file mode 100644 index 0000000..87c7546 --- /dev/null +++ b/src/ui/DiskUsage.qml @@ -0,0 +1,352 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Controls.impl +import Qt5Compat.GraphicalEffects +import "./base" + +Item { + id: root + implicitHeight: 80 + + required property var device + + property var totalCapacity: 0 + property var systemUsage: 0 + property var appsUsage: 0 + property var mediaUsage: 0 + property var galleryUsage: 0 + property var othersUsage: 0 + property var freeSpace: 0 + + property string errorMessage: "" + property bool ready: false + property bool loading: true + + function isDarkMode() { + if (typeof Utils !== "undefined" && Utils.isDarkMode) + return Utils.isDarkMode() + return false + } + + function percent(value) { + if (totalCapacity <= 0) + return "0.0" + return ((value / totalCapacity) * 100).toFixed(1) + } + + function segmentWidth(value, totalWidth) { + if (totalCapacity <= 0 || value <= 0) + return 0 + var w = Math.floor(value / totalCapacity * totalWidth) + return w <= 0 ? 1 : w + } + + function updateState() { + if (loading) { + stateView.viewState = StateView.State.Loading + // stateView.loadingText = qsTr("Loading disk usage...") + return + } + + if (errorMessage.length > 0) { + stateView.viewState = StateView.State.Error + stateView.errorText = errorMessage + return + } + + if (totalCapacity <= 0) { + stateView.viewState = StateView.State.Error + // stateView.errorText = qsTr("No disk information available.") + return + } + + stateView.viewState = StateView.State.Content + } + + Timer { + id: initTimer + interval: 100 + repeat: false + onTriggered: { + if (!root.device || !root.device.service_manager) { + root.loading = false + root.errorMessage = qsTr("Failed to retrieve disk usage data.") + root.updateState() + return + } + + // var skipGalleryUsage = root.device.deviceInfo.is_iPhone + // && root.device.deviceInfo.isWireless + // && typeof Utils !== "undefined" + // && Utils.isProductTypeNewer + // && !Utils.isProductTypeNewer( + // root.device.deviceInfo.rawProductType, + // "iPhone10,1") + + root.device.service_manager.fetch_disk_usage() + } + } + + Component.onCompleted: { + loading = true + errorMessage = "" + updateState() + initTimer.start() + } + + Connections { + target: root.device ? root.device.service_manager : null + + function onDisk_usage_retrieved(success, apps_usage) { + if (!success) { + loading = false + errorMessage = qsTr("Failed to retrieve disk usage data.") + updateState() + return + } + + loading = false + errorMessage = "" + + totalCapacity = root.device.info["TotalDiskCapacity"] || 0 + systemUsage = root.device.info["TotalSystemCapacity"] || 0 + appsUsage = apps_usage + mediaUsage = 0 + freeSpace = root.device.info["TotalDataAvailable"] || 0 + // galleryUsage = gallery_usage + + var usedKnown = systemUsage + appsUsage + mediaUsage + galleryUsage + if (totalCapacity > (freeSpace + usedKnown)) { + othersUsage = totalCapacity - freeSpace - usedKnown + } else { + othersUsage = 0 + } + + updateState() + } + } + + ColumnLayout { + anchors.fill: parent + anchors.leftMargin: 0 + anchors.topMargin: 0 + anchors.rightMargin: 14 + anchors.bottomMargin: 10 + spacing: 0 + + Text { + Layout.fillWidth: true + text: qsTr("Disk Usage") + horizontalAlignment: Text.AlignHCenter + font.bold: true + color: palette.text + } + + StateView { + id: stateView + Layout.fillWidth: true + Layout.fillHeight: true + viewState: StateView.State.Loading + // loadingText: qsTr("Loading disk usage...") + // errorText: qsTr("Failed to retrieve disk usage data.") + + contentItem: ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + Item { + id: barContainer + Layout.fillWidth: true + Layout.preferredHeight: 20 + + Rectangle { + id: clipContainer + anchors.fill: parent + color: "transparent" + border.width: 0 + radius: 5 + clip: true + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: clipContainer.width + height: clipContainer.height + radius: clipContainer.radius + visible: false + } + } + Row { + id: barRow + anchors.fill: parent + spacing: 0 + + Rectangle { + id: systemBar + width: segmentWidth(systemUsage, barContainer.width) + height: barContainer.height + color: "#a1384d" + border.color: "#e64a5b" + border.width: 1 + visible: width > 0 + + HoverHandler { id: systemHover } + ToolTip.visible: systemHover.hovered + ToolTip.text: qsTr("System: %1 (%2%)") + // FIXME: cache sizes, do not recompute + .arg(Helpers.formatSize(systemUsage)) + .arg(percent(systemUsage)) + } + + Rectangle { + id: appsBar + width: segmentWidth(appsUsage, barContainer.width) + height: barContainer.height + color: "#4f869f" + border.color: "#63b4da" + border.width: 1 + visible: width > 0 + + HoverHandler { id: appsHover } + ToolTip.visible: appsHover.hovered + ToolTip.text: qsTr("Apps: %1 (%2%)") + .arg(Helpers.formatSize(appsUsage)) + .arg(percent(appsUsage)) + } + + Rectangle { + id: mediaBar + width: segmentWidth(mediaUsage, barContainer.width) + height: barContainer.height + color: "#2ECC71" + border.width: 0 + visible: width > 0 + + HoverHandler { id: mediaHover } + ToolTip.visible: mediaHover.hovered + ToolTip.text: qsTr("Media: %1 (%2%)") + .arg(Helpers.formatSize(mediaUsage)) + .arg(percent(mediaUsage)) + } + + Rectangle { + id: galleryBar + width: segmentWidth(galleryUsage, barContainer.width) + height: barContainer.height + color: "#9b59b6" + border.color: "#b36cd1" + border.width: 1 + visible: width > 0 + + HoverHandler { id: galleryHover } + ToolTip.visible: galleryHover.hovered + ToolTip.text: qsTr("Gallery: %1 (%2%)") + .arg(Helpers.formatSize(galleryUsage)) + .arg(percent(galleryUsage)) + } + + Rectangle { + id: othersBar + width: segmentWidth(othersUsage, barContainer.width) + height: barContainer.height + color: "#a28729" + border.color: "#c4a32d" + border.width: 1 + visible: width > 0 + + HoverHandler { id: othersHover } + ToolTip.visible: othersHover.hovered + ToolTip.text: qsTr("Others: %1 (%2%)") + .arg(Helpers.formatSize(othersUsage)) + .arg(percent(othersUsage)) + } + + Rectangle { + id: freeBar + width: segmentWidth(freeSpace, barContainer.width) + height: barContainer.height + color: isDarkMode() + ? "rgba(255, 255, 255, 0.10)" + : "rgba(0, 0, 0, 0.25)" + border.color: "#4f4f4f" + border.width: 1 + radius: 3 + visible: width > 0 + + HoverHandler { id: freeHover } + ToolTip.visible: freeHover.hovered + ToolTip.text: qsTr("Free: %1 (%2%)") + .arg(Helpers.formatSize(freeSpace)) + .arg(percent(freeSpace)) + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 0 + + Text { + visible: systemUsage > 0 + text: qsTr("System (%1)").arg(Helpers.formatSize(systemUsage)) + font.pixelSize: 10 + color: palette.text + leftPadding: 0 + rightPadding: 4 + } + Item { Layout.fillWidth: true } + + Text { + visible: appsUsage > 0 + text: qsTr("Apps (%1)").arg(Helpers.formatSize(appsUsage)) + font.pixelSize: 10 + color: palette.text + leftPadding: 0 + rightPadding: 4 + } + Item { Layout.fillWidth: true } + + Text { + visible: mediaUsage > 0 + text: qsTr("Media (%1)").arg(Helpers.formatSize(mediaUsage)) + font.pixelSize: 10 + color: palette.text + leftPadding: 0 + rightPadding: 4 + } + Item { Layout.fillWidth: true } + + Text { + visible: galleryUsage > 0 + text: qsTr("Gallery (%1)").arg(Helpers.formatSize(galleryUsage)) + font.pixelSize: 10 + color: palette.text + leftPadding: 0 + rightPadding: 4 + } + Item { Layout.fillWidth: true } + + Text { + visible: othersUsage > 0 + text: qsTr("Others (%1)").arg(Helpers.formatSize(othersUsage)) + font.pixelSize: 10 + color: palette.text + leftPadding: 0 + rightPadding: 4 + } + Item { Layout.fillWidth: true } + + Text { + visible: freeSpace > 0 + text: qsTr("Free (%1)").arg(Helpers.formatSize(freeSpace)) + font.pixelSize: 10 + color: palette.text + leftPadding: 0 + rightPadding: 4 + } + } + } + } + } +} \ No newline at end of file