import QtQuick import "../services" as S Column { id: root required property color accentColor property bool active: true readonly property color _stateColor: S.BatteryService.charging ? S.Theme.base0B : S.BatteryService.critical ? S.Theme.base09 : S.BatteryService.percent < S.BatteryService.warnThresh ? S.Theme.base0A : root.accentColor // Header - pct + time Item { width: parent.width height: 28 Text { anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter text: { const t = S.BatteryService.charging ? S.BatteryService.timeToFull : S.BatteryService.timeToEmpty; const ts = S.BatteryService.fmtTime(t); return Math.round(S.BatteryService.percent) + "%" + (ts ? " " + ts : ""); } color: root._stateColor font.pixelSize: S.Theme.fontSize font.family: S.Theme.fontFamily font.bold: true } } // Progress bar Item { width: parent.width height: 14 Item { anchors.left: parent.left anchors.leftMargin: 12 anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter height: 6 Rectangle { anchors.fill: parent color: S.Theme.base02 radius: 3 } Rectangle { width: parent.width * Math.min(1, S.BatteryService.percent / 100) height: parent.height color: root._stateColor radius: 3 Behavior on width { enabled: root.active NumberAnimation { duration: 300 easing.type: Easing.OutCubic } } } // Warning threshold marker Rectangle { x: parent.width * (S.BatteryService.warnThresh / 100) - 1 width: 1 height: parent.height + 4 anchors.verticalCenter: parent.verticalCenter color: S.Theme.base0A opacity: 0.6 } // Critical threshold marker Rectangle { x: parent.width * (S.BatteryService.critThresh / 100) - 1 width: 1 height: parent.height + 4 anchors.verticalCenter: parent.verticalCenter color: S.Theme.base08 opacity: 0.6 } } } // 24h history sparkline (area chart) Canvas { id: _sparkline anchors.left: parent.left anchors.leftMargin: 12 anchors.right: parent.right anchors.rightMargin: 12 height: 44 property var _hist: S.BatteryService.history property color _col: root._stateColor on_HistChanged: if (root.active) requestPaint() on_ColChanged: if (root.active) requestPaint() onVisibleChanged: if (visible) requestPaint() onPaint: { const ctx = getContext("2d"); if (!ctx) return; ctx.clearRect(0, 0, width, height); const d = _hist; if (d.length < 2) return; const maxSamples = 1440; const xScale = width / maxSamples; const xOffset = (maxSamples - d.length) * xScale; // Background tint ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.07).toString(); ctx.fillRect(0, 0, width, height); // Warning threshold line const warnY = height - height * (S.BatteryService.warnThresh / 100); ctx.strokeStyle = S.Theme.base0A.toString(); ctx.globalAlpha = 0.3; ctx.lineWidth = 1; ctx.setLineDash([3, 3]); ctx.beginPath(); ctx.moveTo(0, warnY); ctx.lineTo(width, warnY); ctx.stroke(); // Critical threshold line const critY = height - height * (S.BatteryService.critThresh / 100); ctx.strokeStyle = S.Theme.base08.toString(); ctx.beginPath(); ctx.moveTo(0, critY); ctx.lineTo(width, critY); ctx.stroke(); ctx.setLineDash([]); ctx.globalAlpha = 1.0; // Filled area under the curve ctx.beginPath(); ctx.moveTo(xOffset, height); for (let i = 0; i < d.length; i++) { const x = xOffset + i * xScale; const y = height - height * (d[i] / 100); ctx.lineTo(x, y); } ctx.lineTo(xOffset + (d.length - 1) * xScale, height); ctx.closePath(); ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.18).toString(); ctx.fill(); // Top line ctx.beginPath(); for (let i = 0; i < d.length; i++) { const x = xOffset + i * xScale; const y = height - height * (d[i] / 100); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.strokeStyle = _col.toString(); ctx.lineWidth = 1.5; ctx.stroke(); } } // Footer: thresholds + time label Item { width: parent.width height: 16 Text { anchors.left: parent.left anchors.leftMargin: 12 anchors.verticalCenter: parent.verticalCenter text: "warn " + S.BatteryService.warnThresh + "% crit " + S.BatteryService.critThresh + "%" color: S.Theme.base03 font.pixelSize: S.Theme.fontSize - 3 font.family: S.Theme.fontFamily font.letterSpacing: 0.5 } Text { anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter text: "24h" color: S.Theme.base03 font.pixelSize: S.Theme.fontSize - 3 font.family: S.Theme.fontFamily } } // Separator Rectangle { width: parent.width - 16 height: 1 anchors.horizontalCenter: parent.horizontalCenter color: S.Theme.base03 } // Rate row Item { width: parent.width height: 20 visible: S.BatteryService.changeRate !== 0 Text { anchors.left: parent.left anchors.leftMargin: 12 anchors.verticalCenter: parent.verticalCenter text: S.BatteryService.charging ? "Charging" : "Discharging" color: S.Theme.base04 font.pixelSize: S.Theme.fontSize - 2 font.family: S.Theme.fontFamily } Text { anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter text: { const r = Math.abs(S.BatteryService.changeRate); return r > 0 ? r.toFixed(1) + " W" : ""; } color: root._stateColor font.pixelSize: S.Theme.fontSize - 2 font.family: S.Theme.fontFamily } } // Health row Item { width: parent.width height: 20 visible: S.BatteryService.healthSupported Text { anchors.left: parent.left anchors.leftMargin: 12 anchors.verticalCenter: parent.verticalCenter text: "Health" color: S.Theme.base04 font.pixelSize: S.Theme.fontSize - 2 font.family: S.Theme.fontFamily } Text { anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter text: Math.round(S.BatteryService.healthPercent) + "%" color: { const h = S.BatteryService.healthPercent; return h < 50 ? S.Theme.base08 : h < 75 ? S.Theme.base0A : S.Theme.base0B; } font.pixelSize: S.Theme.fontSize - 2 font.family: S.Theme.fontFamily } } Item { width: 1 height: 4 } }