From b89369497715e04ab317b3294231aef9c3ee3f14 Mon Sep 17 00:00:00 2001 From: bbedward Date: Wed, 24 Sep 2025 18:06:46 -0400 Subject: [PATCH] Add disk usage component for TopBar and ControlCenter --- Common/SettingsData.qml | 6 +- .../ControlCenter/Components/DetailHost.qml | 36 +++- .../ControlCenter/Components/WidgetGrid.qml | 61 ++++++- Modules/ControlCenter/ControlCenterPopout.qml | 29 ++- .../ControlCenter/Details/DiskUsageDetail.qml | 169 +++++++++++++++++ Modules/ControlCenter/Models/WidgetModel.qml | 10 + .../ControlCenter/Widgets/DiskUsagePill.qml | 78 ++++++++ Modules/ControlCenter/utils/layout.js | 2 +- Modules/ControlCenter/utils/widgets.js | 10 + Modules/Settings/TopBarTab.qml | 72 ++++++++ Modules/Settings/WidgetsTabSection.qml | 33 ++++ Modules/TopBar/DiskUsage.qml | 171 ++++++++++++++++++ Modules/TopBar/TopBar.qml | 10 + Widgets/DankDropdown.qml | 1 - 14 files changed, 676 insertions(+), 12 deletions(-) create mode 100644 Modules/ControlCenter/Details/DiskUsageDetail.qml create mode 100644 Modules/ControlCenter/Widgets/DiskUsagePill.qml create mode 100644 Modules/TopBar/DiskUsage.qml diff --git a/Common/SettingsData.qml b/Common/SettingsData.qml index f7c648fb..6edce4f6 100644 --- a/Common/SettingsData.qml +++ b/Common/SettingsData.qml @@ -171,7 +171,8 @@ Singleton { "enabled": true, "size": 20, "selectedGpuIndex": 0, - "pciId": "" + "pciId": "", + "mountPath": "/" } leftWidgetsModel.append(dummyItem) centerWidgetsModel.append(dummyItem) @@ -741,6 +742,7 @@ Singleton { var size = typeof order[i] === "string" ? undefined : order[i].size var selectedGpuIndex = typeof order[i] === "string" ? undefined : order[i].selectedGpuIndex var pciId = typeof order[i] === "string" ? undefined : order[i].pciId + var mountPath = typeof order[i] === "string" ? undefined : order[i].mountPath var item = { "widgetId": widgetId, "enabled": enabled @@ -751,6 +753,8 @@ Singleton { item.selectedGpuIndex = selectedGpuIndex if (pciId !== undefined) item.pciId = pciId + if (mountPath !== undefined) + item.mountPath = mountPath listModel.append(item) } diff --git a/Modules/ControlCenter/Components/DetailHost.qml b/Modules/ControlCenter/Components/DetailHost.qml index cfcc32b6..91f074a7 100644 --- a/Modules/ControlCenter/Components/DetailHost.qml +++ b/Modules/ControlCenter/Components/DetailHost.qml @@ -6,12 +6,14 @@ Item { id: root property string expandedSection: "" + property var expandedWidgetData: null Loader { width: parent.width height: 250 y: Theme.spacingS active: parent.height > 0 + property string sectionKey: root.expandedSection sourceComponent: { switch (root.expandedSection) { case "network": @@ -20,9 +22,17 @@ Item { case "audioOutput": return audioOutputDetailComponent case "audioInput": return audioInputDetailComponent case "battery": return batteryDetailComponent - default: return null + default: + if (root.expandedSection.startsWith("diskUsage_")) { + return diskUsageDetailComponent + } + return null } } + onSectionKeyChanged: { + active = false + active = true + } } Component { @@ -49,4 +59,28 @@ Item { id: batteryDetailComponent BatteryDetail {} } + + Component { + id: diskUsageDetailComponent + DiskUsageDetail { + currentMountPath: root.expandedWidgetData?.mountPath || "/" + instanceId: root.expandedWidgetData?.instanceId || "" + + + onMountPathChanged: (newMountPath) => { + if (root.expandedWidgetData && root.expandedWidgetData.id === "diskUsage") { + const widgets = SettingsData.controlCenterWidgets || [] + const newWidgets = widgets.map(w => { + if (w.id === "diskUsage" && w.instanceId === root.expandedWidgetData.instanceId) { + const updatedWidget = Object.assign({}, w) + updatedWidget.mountPath = newMountPath + return updatedWidget + } + return w + }) + SettingsData.setControlCenterWidgets(newWidgets) + } + } + } + } } \ No newline at end of file diff --git a/Modules/ControlCenter/Components/WidgetGrid.qml b/Modules/ControlCenter/Components/WidgetGrid.qml index 73ba7439..4b82f260 100644 --- a/Modules/ControlCenter/Components/WidgetGrid.qml +++ b/Modules/ControlCenter/Components/WidgetGrid.qml @@ -12,6 +12,7 @@ Column { property string expandedSection: "" property int expandedWidgetIndex: -1 property var model: null + property var expandedWidgetData: null signal expandClicked(var widgetData, int globalIndex) signal removeWidget(int index) @@ -28,12 +29,17 @@ Column { return LayoutUtils.calculateRowsAndWidgets(root, expandedSection, expandedWidgetIndex) } + property var layoutResult: { + const dummy = [expandedSection, expandedWidgetIndex, model?.controlCenterWidgets] + return calculateRowsAndWidgets() + } + + onLayoutResultChanged: { + expandedRowIndex = layoutResult.expandedRowIndex + } + Repeater { - model: { - const result = root.calculateRowsAndWidgets() - root.expandedRowIndex = result.expandedRowIndex - return result.rows - } + model: root.layoutResult.rows Column { width: root.width @@ -102,6 +108,8 @@ Column { return inputAudioSliderComponent } else if (id === "battery") { return widgetWidth <= 25 ? smallBatteryComponent : batteryPillComponent + } else if (id === "diskUsage") { + return diskUsagePillComponent } else { return widgetWidth <= 25 ? smallToggleComponent : toggleButtonComponent } @@ -115,9 +123,19 @@ Column { DetailHost { width: parent.width height: active ? (250 + Theme.spacingS) : 0 - property bool active: root.expandedSection !== "" && rowIndex === root.expandedRowIndex + property bool active: { + if (root.expandedSection === "") return false + + if (root.expandedSection.startsWith("diskUsage_") && root.expandedWidgetData) { + const expandedInstanceId = root.expandedWidgetData.instanceId + return rowWidgets.some(w => w.id === "diskUsage" && w.instanceId === expandedInstanceId) + } + + return rowIndex === root.expandedRowIndex + } visible: active expandedSection: root.expandedSection + expandedWidgetData: root.expandedWidgetData } } } @@ -676,4 +694,35 @@ Column { } } } + + Component { + id: diskUsagePillComponent + DiskUsagePill { + property var widgetData: parent.widgetData || {} + property int widgetIndex: parent.widgetIndex || 0 + width: parent.width + height: 60 + + mountPath: widgetData.mountPath || "/" + instanceId: widgetData.instanceId || "" + + onExpandClicked: { + if (!root.editMode) { + root.expandClicked(widgetData, widgetIndex) + } + } + + EditModeOverlay { + anchors.fill: parent + editMode: root.editMode + widgetData: parent.widgetData + widgetIndex: parent.widgetIndex + showSizeControls: true + isSlider: false + onRemoveWidget: (index) => root.removeWidget(index) + onToggleWidgetSize: (index) => root.toggleWidgetSize(index) + onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex) + } + } + } } \ No newline at end of file diff --git a/Modules/ControlCenter/ControlCenterPopout.qml b/Modules/ControlCenter/ControlCenterPopout.qml index 2fb442f1..a8c1a4a7 100644 --- a/Modules/ControlCenter/ControlCenterPopout.qml +++ b/Modules/ControlCenter/ControlCenterPopout.qml @@ -26,10 +26,29 @@ DankPopout { property var triggerScreen: null property bool editMode: false property int expandedWidgetIndex: -1 + property var expandedWidgetData: null signal powerActionRequested(string action, string title, string message) signal lockRequested + function collapseAll() { + expandedSection = "" + expandedWidgetIndex = -1 + expandedWidgetData = null + } + + onEditModeChanged: { + if (editMode) { + collapseAll() + } + } + + onVisibleChanged: { + if (!visible) { + collapseAll() + } + } + readonly property color _containerBg: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.60) function setTriggerPosition(x, y, width, section, screen) { @@ -132,10 +151,16 @@ DankPopout { editMode: root.editMode expandedSection: root.expandedSection expandedWidgetIndex: root.expandedWidgetIndex + expandedWidgetData: root.expandedWidgetData model: widgetModel onExpandClicked: (widgetData, globalIndex) => { root.expandedWidgetIndex = globalIndex - root.toggleSection(widgetData.id) + root.expandedWidgetData = widgetData + if (widgetData.id === "diskUsage") { + root.toggleSection("diskUsage_" + (widgetData.instanceId || "default")) + } else { + root.toggleSection(widgetData.id) + } } onRemoveWidget: (index) => widgetModel.removeWidget(index) onMoveWidget: (fromIndex, toIndex) => widgetModel.moveWidget(fromIndex, toIndex) @@ -147,7 +172,7 @@ DankPopout { visible: editMode availableWidgets: { const existingIds = (SettingsData.controlCenterWidgets || []).map(w => w.id) - return widgetModel.baseWidgetDefinitions.filter(w => !existingIds.includes(w.id)) + return widgetModel.baseWidgetDefinitions.filter(w => w.allowMultiple || !existingIds.includes(w.id)) } onAddWidget: (widgetId) => widgetModel.addWidget(widgetId) onResetToDefault: () => widgetModel.resetToDefault() diff --git a/Modules/ControlCenter/Details/DiskUsageDetail.qml b/Modules/ControlCenter/Details/DiskUsageDetail.qml new file mode 100644 index 00000000..3e96d93f --- /dev/null +++ b/Modules/ControlCenter/Details/DiskUsageDetail.qml @@ -0,0 +1,169 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + property string currentMountPath: "/" + property string instanceId: "" + + signal mountPathChanged(string newMountPath) + + implicitHeight: diskContent.height + Theme.spacingM + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + + Component.onCompleted: { + DgopService.addRef(["diskmounts"]) + } + + Component.onDestruction: { + DgopService.removeRef(["diskmounts"]) + } + + DankFlickable { + id: diskContent + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Theme.spacingM + anchors.topMargin: Theme.spacingM + contentHeight: diskColumn.height + clip: true + + Column { + id: diskColumn + width: parent.width + spacing: Theme.spacingS + + Item { + width: parent.width + height: 100 + visible: !DgopService.dgopAvailable || !DgopService.diskMounts || DgopService.diskMounts.length === 0 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingM + + DankIcon { + anchors.horizontalCenter: parent.horizontalCenter + name: DgopService.dgopAvailable ? "storage" : "error" + size: 32 + color: DgopService.dgopAvailable ? Theme.primary : Theme.error + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + text: DgopService.dgopAvailable ? "No disk data available" : "dgop not available" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + horizontalAlignment: Text.AlignHCenter + } + } + } + + Repeater { + model: DgopService.diskMounts || [] + delegate: Rectangle { + required property var modelData + required property int index + + width: parent.width + height: 80 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2) + border.color: modelData.mount === currentMountPath ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + border.width: modelData.mount === currentMountPath ? 2 : 1 + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingM + spacing: Theme.spacingM + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + DankIcon { + name: "storage" + size: Theme.iconSize + color: { + const percentStr = modelData.percent?.replace("%", "") || "0" + const percent = parseFloat(percentStr) || 0 + if (percent > 90) return Theme.error + if (percent > 75) return Theme.warning + return modelData.mount === currentMountPath ? Theme.primary : Theme.surfaceText + } + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: { + const percentStr = modelData.percent?.replace("%", "") || "0" + const percent = parseFloat(percentStr) || 0 + return percent.toFixed(0) + "%" + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - 50 - Theme.spacingM + + StyledText { + text: modelData.mount === "/" ? "Root Filesystem" : modelData.mount + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: modelData.mount === currentMountPath ? Font.Medium : Font.Normal + elide: Text.ElideRight + width: parent.width + } + + StyledText { + text: modelData.mount + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + elide: Text.ElideRight + width: parent.width + visible: modelData.mount !== "/" + } + + StyledText { + text: `${modelData.used || "?"} / ${modelData.size || "?"}` + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + elide: Text.ElideRight + width: parent.width + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (modelData.mount !== currentMountPath) { + currentMountPath = modelData.mount + mountPathChanged(modelData.mount) + } + } + } + + Behavior on border.color { + ColorAnimation { duration: Theme.shortDuration } + } + } + } + } + } +} \ No newline at end of file diff --git a/Modules/ControlCenter/Models/WidgetModel.qml b/Modules/ControlCenter/Models/WidgetModel.qml index 6daf7b5f..76d6c290 100644 --- a/Modules/ControlCenter/Models/WidgetModel.qml +++ b/Modules/ControlCenter/Models/WidgetModel.qml @@ -106,6 +106,16 @@ QtObject { "icon": "battery_std", "type": "action", "enabled": true + }, + { + "id": "diskUsage", + "text": "Disk Usage", + "description": "Filesystem usage monitoring", + "icon": "storage", + "type": "action", + "enabled": DgopService.dgopAvailable, + "warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : undefined, + "allowMultiple": true } ] diff --git a/Modules/ControlCenter/Widgets/DiskUsagePill.qml b/Modules/ControlCenter/Widgets/DiskUsagePill.qml new file mode 100644 index 00000000..01bd4ea8 --- /dev/null +++ b/Modules/ControlCenter/Widgets/DiskUsagePill.qml @@ -0,0 +1,78 @@ +import QtQuick +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.ControlCenter.Widgets + +CompoundPill { + id: root + + property string mountPath: "/" + property string instanceId: "" + + iconName: "storage" + + property var selectedMount: { + if (!DgopService.diskMounts || DgopService.diskMounts.length === 0) { + return null + } + + const targetMount = DgopService.diskMounts.find(mount => mount.mount === mountPath) + return targetMount || DgopService.diskMounts.find(mount => mount.mount === "/") || DgopService.diskMounts[0] + } + + property real usagePercent: { + if (!selectedMount || !selectedMount.percent) { + return 0 + } + const percentStr = selectedMount.percent.replace("%", "") + return parseFloat(percentStr) || 0 + } + + isActive: DgopService.dgopAvailable && selectedMount !== null + + primaryText: { + if (!DgopService.dgopAvailable) { + return "Disk Usage" + } + if (!selectedMount) { + return "No disk data" + } + return `Disk Usage • ${selectedMount.mount}` + } + + secondaryText: { + if (!DgopService.dgopAvailable) { + return "dgop not available" + } + if (!selectedMount) { + return "No disk data available" + } + return `${selectedMount.used} / ${selectedMount.size} (${usagePercent.toFixed(0)}%)` + } + + iconColor: { + if (!DgopService.dgopAvailable || !selectedMount) { + return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) + } + if (usagePercent > 90) { + return Theme.error + } + if (usagePercent > 75) { + return Theme.warning + } + return Theme.surfaceText + } + + Component.onCompleted: { + DgopService.addRef(["diskmounts"]) + } + Component.onDestruction: { + DgopService.removeRef(["diskmounts"]) + } + + onToggled: { + expandClicked() + } +} \ No newline at end of file diff --git a/Modules/ControlCenter/utils/layout.js b/Modules/ControlCenter/utils/layout.js index 187a107e..b1f9b44b 100644 --- a/Modules/ControlCenter/utils/layout.js +++ b/Modules/ControlCenter/utils/layout.js @@ -32,7 +32,7 @@ function calculateRowsAndWidgets(controlCenterColumn, expandedSection, expandedW currentWidth += (currentRow.length > 1 ? spacing : 0) + itemWidth } - if (widget.id === expandedSection && expandedWidgetIndex === i) { + if (expandedWidgetIndex === i) { expandedRow = rows.length } } diff --git a/Modules/ControlCenter/utils/widgets.js b/Modules/ControlCenter/utils/widgets.js index 611de131..0d33683f 100644 --- a/Modules/ControlCenter/utils/widgets.js +++ b/Modules/ControlCenter/utils/widgets.js @@ -9,10 +9,20 @@ function addWidget(widgetId) { "enabled": true, "width": 50 } + + if (widgetId === "diskUsage") { + widget.instanceId = generateUniqueId() + widget.mountPath = "/" + } + widgets.push(widget) SettingsData.setControlCenterWidgets(widgets) } +function generateUniqueId() { + return Date.now().toString(36) + Math.random().toString(36).substr(2) +} + function removeWidget(index) { var widgets = SettingsData.controlCenterWidgets.slice() if (index >= 0 && index < widgets.length) { diff --git a/Modules/Settings/TopBarTab.qml b/Modules/Settings/TopBarTab.qml index bf448f65..01ae532d 100644 --- a/Modules/Settings/TopBarTab.qml +++ b/Modules/Settings/TopBarTab.qml @@ -67,6 +67,13 @@ Item { "id": "memUsage", "text": "Memory Usage", "description": "Memory usage indicator", + "icon": "developer_board", + "enabled": DgopService.dgopAvailable, + "warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : undefined + }, { + "id": "diskUsage", + "text": "Disk Usage", + "description": "Percentage", "icon": "storage", "enabled": DgopService.dgopAvailable, "warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : undefined @@ -223,6 +230,9 @@ Item { widgetObj.showBluetoothIcon = true widgetObj.showAudioIcon = true } + if (widgetId === "diskUsage") { + widgetObj.mountPath = "/" + } var widgets = [] if (targetSection === "left") { @@ -412,6 +422,52 @@ Item { SettingsData.setTopBarRightWidgets(widgets) } + function handleDiskMountSelectionChanged(sectionId, widgetIndex, mountPath) { + var widgets = [] + if (sectionId === "left") + widgets = SettingsData.topBarLeftWidgets.slice() + else if (sectionId === "center") + widgets = SettingsData.topBarCenterWidgets.slice() + else if (sectionId === "right") + widgets = SettingsData.topBarRightWidgets.slice() + + if (widgetIndex >= 0 && widgetIndex < widgets.length) { + var widget = widgets[widgetIndex] + if (typeof widget === "string") { + widgets[widgetIndex] = { + "id": widget, + "enabled": true, + "mountPath": mountPath + } + } else { + var newWidget = { + "id": widget.id, + "enabled": widget.enabled, + "mountPath": mountPath + } + if (widget.size !== undefined) + newWidget.size = widget.size + if (widget.selectedGpuIndex !== undefined) + newWidget.selectedGpuIndex = widget.selectedGpuIndex + if (widget.pciId !== undefined) + newWidget.pciId = widget.pciId + if (widget.id === "controlCenterButton") { + newWidget.showNetworkIcon = widget.showNetworkIcon !== undefined ? widget.showNetworkIcon : true + newWidget.showBluetoothIcon = widget.showBluetoothIcon !== undefined ? widget.showBluetoothIcon : true + newWidget.showAudioIcon = widget.showAudioIcon !== undefined ? widget.showAudioIcon : true + } + widgets[widgetIndex] = newWidget + } + } + + if (sectionId === "left") + SettingsData.setTopBarLeftWidgets(widgets) + else if (sectionId === "center") + SettingsData.setTopBarCenterWidgets(widgets) + else if (sectionId === "right") + SettingsData.setTopBarRightWidgets(widgets) + } + function handleControlCenterSettingChanged(sectionId, widgetIndex, settingName, value) { // Control Center settings are global, not per-widget instance if (settingName === "showNetworkIcon") { @@ -441,6 +497,8 @@ Item { === "string" ? undefined : widget.selectedGpuIndex var widgetPciId = typeof widget === "string" ? undefined : widget.pciId + var widgetMountPath = typeof widget + === "string" ? undefined : widget.mountPath var widgetShowNetworkIcon = typeof widget === "string" ? undefined : widget.showNetworkIcon var widgetShowBluetoothIcon = typeof widget === "string" ? undefined : widget.showBluetoothIcon var widgetShowAudioIcon = typeof widget === "string" ? undefined : widget.showAudioIcon @@ -456,6 +514,8 @@ Item { item.selectedGpuIndex = widgetSelectedGpuIndex if (widgetPciId !== undefined) item.pciId = widgetPciId + if (widgetMountPath !== undefined) + item.mountPath = widgetMountPath if (widgetShowNetworkIcon !== undefined) item.showNetworkIcon = widgetShowNetworkIcon if (widgetShowBluetoothIcon !== undefined) @@ -1045,6 +1105,10 @@ Item { sectionId, widgetIndex, selectedIndex) } + onDiskMountSelectionChanged: (sectionId, widgetIndex, mountPath) => { + topBarTab.handleDiskMountSelectionChanged( + sectionId, widgetIndex, mountPath) + } } } @@ -1115,6 +1179,10 @@ Item { sectionId, widgetIndex, selectedIndex) } + onDiskMountSelectionChanged: (sectionId, widgetIndex, mountPath) => { + topBarTab.handleDiskMountSelectionChanged( + sectionId, widgetIndex, mountPath) + } } } @@ -1185,6 +1253,10 @@ Item { sectionId, widgetIndex, selectedIndex) } + onDiskMountSelectionChanged: (sectionId, widgetIndex, mountPath) => { + topBarTab.handleDiskMountSelectionChanged( + sectionId, widgetIndex, mountPath) + } } } } diff --git a/Modules/Settings/WidgetsTabSection.qml b/Modules/Settings/WidgetsTabSection.qml index e7340d06..69967b90 100644 --- a/Modules/Settings/WidgetsTabSection.qml +++ b/Modules/Settings/WidgetsTabSection.qml @@ -20,6 +20,7 @@ Column { signal spacerSizeChanged(string sectionId, int widgetIndex, int newSize) signal compactModeChanged(string widgetId, var value) signal gpuSelectionChanged(string sectionId, int widgetIndex, int selectedIndex) + signal diskMountSelectionChanged(string sectionId, int widgetIndex, string mountPath) signal controlCenterSettingChanged(string sectionId, int widgetIndex, string settingName, bool value) width: parent.width @@ -187,6 +188,38 @@ Column { } } + Item { + width: 120 + height: 32 + visible: modelData.id === "diskUsage" + DankDropdown { + id: diskMountDropdown + anchors.fill: parent + currentValue: { + const mountPath = modelData.mountPath || "/" + if (mountPath === "/") { + return "root (/)" + } + return mountPath + } + options: { + if (!DgopService.diskMounts || DgopService.diskMounts.length === 0) { + return ["root (/)"] + } + return DgopService.diskMounts.map(mount => { + if (mount.mount === "/") { + return "root (/)" + } + return mount.mount + }) + } + onValueChanged: value => { + const newPath = value === "root (/)" ? "/" : value + root.diskMountSelectionChanged(root.sectionId, index, newPath) + } + } + } + Item { width: 32 height: 32 diff --git a/Modules/TopBar/DiskUsage.qml b/Modules/TopBar/DiskUsage.qml new file mode 100644 index 00000000..74724ef9 --- /dev/null +++ b/Modules/TopBar/DiskUsage.qml @@ -0,0 +1,171 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property var widgetData: null + property real widgetHeight: 30 + property string mountPath: (widgetData && widgetData.mountPath !== undefined) ? widgetData.mountPath : "/" + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30)) + + property var selectedMount: { + if (!DgopService.diskMounts || DgopService.diskMounts.length === 0) { + return null + } + + // Force re-evaluation when mountPath changes + const currentMountPath = root.mountPath || "/" + + // First try to find exact match + for (let i = 0; i < DgopService.diskMounts.length; i++) { + if (DgopService.diskMounts[i].mount === currentMountPath) { + return DgopService.diskMounts[i] + } + } + + // Fallback to root + for (let i = 0; i < DgopService.diskMounts.length; i++) { + if (DgopService.diskMounts[i].mount === "/") { + return DgopService.diskMounts[i] + } + } + + // Last resort - first mount + return DgopService.diskMounts[0] || null + } + + property real diskUsagePercent: { + if (!selectedMount || !selectedMount.percent) { + return 0 + } + const percentStr = selectedMount.percent.replace("%", "") + return parseFloat(percentStr) || 0 + } + + width: diskContent.implicitWidth + horizontalPadding * 2 + height: widgetHeight + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SettingsData.topBarNoBackground) { + return "transparent" + } + + const baseColor = Theme.widgetBaseBackgroundColor + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency) + } + Component.onCompleted: { + DgopService.addRef(["diskmounts"]) + } + Component.onDestruction: { + DgopService.removeRef(["diskmounts"]) + } + + Connections { + function onWidgetDataChanged() { + // Force property re-evaluation by triggering change detection + root.mountPath = Qt.binding(() => { + return (root.widgetData && root.widgetData.mountPath !== undefined) ? root.widgetData.mountPath : "/" + }) + + root.selectedMount = Qt.binding(() => { + if (!DgopService.diskMounts || DgopService.diskMounts.length === 0) { + return null + } + + const currentMountPath = root.mountPath || "/" + + // First try to find exact match + for (let i = 0; i < DgopService.diskMounts.length; i++) { + if (DgopService.diskMounts[i].mount === currentMountPath) { + return DgopService.diskMounts[i] + } + } + + // Fallback to root + for (let i = 0; i < DgopService.diskMounts.length; i++) { + if (DgopService.diskMounts[i].mount === "/") { + return DgopService.diskMounts[i] + } + } + + // Last resort - first mount + return DgopService.diskMounts[0] || null + }) + } + + target: SettingsData + } + + + Row { + id: diskContent + + anchors.centerIn: parent + spacing: 3 + + DankIcon { + name: "storage" + size: Theme.iconSize - 8 + color: { + if (root.diskUsagePercent > 90) { + return Theme.tempDanger + } + if (root.diskUsagePercent > 75) { + return Theme.tempWarning + } + return Theme.surfaceText + } + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: { + if (!root.selectedMount) { + return "--" + } + return root.selectedMount.mount + } + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignLeft + elide: Text.ElideNone + } + + StyledText { + text: { + if (root.diskUsagePercent === undefined || root.diskUsagePercent === null || root.diskUsagePercent === 0) { + return "--%" + } + return root.diskUsagePercent.toFixed(0) + "%" + } + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignLeft + elide: Text.ElideNone + + StyledTextMetrics { + id: diskBaseline + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + text: "100%" + } + + width: Math.max(diskBaseline.width, paintedWidth) + + Behavior on width { + NumberAnimation { + duration: 120 + easing.type: Easing.OutCubic + } + } + } + } +} \ No newline at end of file diff --git a/Modules/TopBar/TopBar.qml b/Modules/TopBar/TopBar.qml index be06e753..6680c4cc 100644 --- a/Modules/TopBar/TopBar.qml +++ b/Modules/TopBar/TopBar.qml @@ -483,6 +483,7 @@ PanelWindow { "clipboard": clipboardComponent, "cpuUsage": cpuUsageComponent, "memUsage": memUsageComponent, + "diskUsage": diskUsageComponent, "cpuTemp": cpuTempComponent, "gpuTemp": gpuTempComponent, "notificationButton": notificationButtonComponent, @@ -1003,6 +1004,15 @@ PanelWindow { } } + Component { + id: diskUsageComponent + + DiskUsage { + widgetHeight: root.widgetHeight + widgetData: parent.widgetData + } + } + Component { id: cpuTempComponent diff --git a/Widgets/DankDropdown.qml b/Widgets/DankDropdown.qml index e4ceb6e2..b42fd685 100644 --- a/Widgets/DankDropdown.qml +++ b/Widgets/DankDropdown.qml @@ -335,7 +335,6 @@ Rectangle { onClicked: { root.currentValue = modelData root.valueChanged(modelData) - dropdownMenu.close() } } }