pragma ComponentBehavior: Bound import QtQuick import "Util.js" as Util Flickable { id: root required property Theme theme required property Power powerService readonly property var panelState: powerService ? Util.parseJson(powerService.stateJson(), { "battery": {}, "externalDevices": [] }) : ({ "battery": {}, "externalDevices": [] }) readonly property var battery: panelState.battery || ({}) readonly property var devices: panelState.externalDevices || [] readonly property var sessionActions: panelState.sessionActions || ["lock", "suspend", "monitor-off", "monitor-on", "logout", "reboot", "poweroff"] readonly property bool hasMainBattery: !!(battery && battery.present) readonly property var safeActionNames: ["lock", "suspend", "monitor-off"] readonly property var destructiveActionNames: ["logout", "reboot", "poweroff"] readonly property var safeActions: actionSubset(safeActionNames) readonly property var destructiveActions: actionSubset(destructiveActionNames) function runSession(action) { if (powerService) powerService.sessionAction(action); } function restoreActionFocus(action) { const index = safeActions.concat(destructiveActions).indexOf(action); const button = actionRepeater.itemAt(index); if (button) button.forceActiveFocus(Qt.TabFocusReason); } function confirm(action) { if (powerService) powerService.confirmAction(action); restoreActionFocus(action); } function cancel() { const action = panelState.pendingConfirmation; if (powerService) powerService.cancelAction(); restoreActionFocus(action); } function roleColor(role) { if (!theme) return "white"; if (role === "warning") return theme.warning; if (role === "error") return theme.error; if (role === "ui.minor") return theme.ui.minor; return theme.ui.accent; } function actionSubset(names) { const rows = []; for (let i = 0; i < names.length; i++) { if (sessionActions.indexOf(names[i]) >= 0) rows.push(names[i]); } return rows; } function actionIcon(action) { if (action === "lock") return "\uF023"; if (action === "suspend") return "\uF186"; if (action === "monitor-off" || action === "monitor-on") return "\uF108"; if (action === "logout") return "\uF08B"; if (action === "reboot") return "\uF01E"; return "\uF011"; } function actionLabel(action) { if (action === "suspend") return "sleep"; if (action === "monitor-off") return "display off"; if (action === "poweroff") return "power off"; return action; } function actionTooltip(action) { if (action === "lock") return "lock your session"; if (action === "suspend") return "suspend the computer"; if (action === "monitor-off") return "turn the display off"; if (action === "logout") return "end your session"; if (action === "reboot") return "restart the computer"; return "shut down the computer"; } function batteryMeta() { const parts = []; if (battery.timeEstimate && battery.timeEstimate !== "unknown") parts.push(battery.timeEstimate); if (battery.health >= 0) parts.push(`health ${battery.health}%`); return parts.join(" ยท "); } width: parent ? parent.width : 0 contentWidth: width contentHeight: powerCol.height clip: true interactive: contentHeight > height boundsBehavior: Flickable.StopAtBounds flickableDirection: Flickable.VerticalFlick Column { id: powerCol width: root.width spacing: 12 Item { objectName: "batteryStatus" width: parent.width height: 48 visible: root.hasMainBattery Rectangle { id: batteryIcon anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter width: 34 height: 20 radius: 4 color: "transparent" border.width: 1 border.color: root.theme.ui.minor Accessible.ignored: true Rectangle { x: 4 y: 4 width: 26 * Math.max(0, Math.min(1, Number(root.battery.percent || 0) / 100)) height: 12 radius: 1 color: root.roleColor(root.battery.role) } Rectangle { x: 34 y: 6 width: 3 height: 8 radius: 1 color: root.theme.ui.minor } } Column { anchors.left: batteryIcon.right anchors.leftMargin: 16 anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: 2 Row { spacing: 10 NuguText { id: batteryPercent theme: root.theme variant: "title" font.pixelSize: 18 text: `${root.battery.percent || 0}%` } NuguText { anchors.baseline: batteryPercent.baseline theme: root.theme variant: "meta" text: root.battery.state || "unknown" color: root.theme.ui.minor } } NuguText { theme: root.theme variant: "meta" width: parent.width visible: root.batteryMeta().length > 0 text: root.batteryMeta() color: root.theme.ui.minor elide: Text.ElideRight } } } Flow { id: deviceFlow objectName: "externalDeviceBatteryChips" width: parent.width height: visible ? implicitHeight : 0 spacing: 8 visible: root.devices.length > 0 Repeater { model: root.devices delegate: PopupCard { id: deviceChip required property var modelData theme: root.theme padding: 0 width: Math.floor((root.width - 8) / 2) height: 32 NuguText { anchors.left: parent.left anchors.leftMargin: 9 anchors.right: devicePercent.left anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter theme: root.theme variant: "meta" text: `${deviceChip.modelData.name}` color: root.theme.ui.minor elide: Text.ElideRight } NuguText { id: devicePercent anchors.right: parent.right anchors.rightMargin: 9 anchors.verticalCenter: parent.verticalCenter theme: root.theme variant: "meta" text: `${deviceChip.modelData.percent}%` color: root.theme.content.normal } } } } Grid { id: actionGrid objectName: "powerActionGrid" width: parent.width columns: 3 spacing: 8 Repeater { id: actionRepeater model: root.safeActions.concat(root.destructiveActions) delegate: PopupButton { id: actionButton required property string modelData readonly property bool risky: root.destructiveActionNames.indexOf(modelData) >= 0 readonly property color labelColor: risky ? (hovered ? root.theme.error : root.theme.ui.normal) : root.theme.content.normal theme: root.theme width: Math.floor((root.width - actionGrid.spacing * 2) / 3) height: 66 text: root.actionLabel(modelData) showText: false tooltip: root.panelState.pendingConfirmation ? "" : root.actionTooltip(modelData) role: risky ? "ghost" : "normal" onControlFocusedChanged: if (controlFocused) Util.revealIn(this, root) onClicked: root.runSession(modelData) Column { anchors.centerIn: parent spacing: 6 Text { anchors.horizontalCenter: parent.horizontalCenter font.family: "Symbols Nerd Font Mono" font.pixelSize: 20 color: actionButton.labelColor text: root.actionIcon(actionButton.modelData) Accessible.ignored: true } NuguText { anchors.horizontalCenter: parent.horizontalCenter theme: root.theme variant: "meta" color: actionButton.labelColor text: actionButton.text Accessible.ignored: true } } } } } Item { objectName: "powerConfirmation" width: parent.width height: 104 visible: !!root.panelState.pendingConfirmation Rectangle { width: parent.width height: 1 color: root.theme.withAlpha(root.theme.ui.minor, 0.2) } Column { y: 13 width: parent.width spacing: 2 NuguText { theme: root.theme variant: "title" text: `${root.actionLabel(root.panelState.pendingConfirmation || "")}?` } NuguText { theme: root.theme variant: "meta" text: "unsaved work may be lost" color: root.theme.ui.minor } } Row { anchors.right: parent.right anchors.bottom: parent.bottom spacing: 8 PopupButton { theme: root.theme text: "cancel" tooltip: "keep this session running" onControlFocusedChanged: if (controlFocused) Util.revealIn(this, root) onClicked: root.cancel() } PopupButton { theme: root.theme text: root.actionLabel(root.panelState.pendingConfirmation || "") accessibleName: `confirm ${text}` tooltip: root.actionTooltip(root.panelState.pendingConfirmation) role: "destructive" primary: true onControlFocusedChanged: if (controlFocused) Util.revealIn(this, root) onClicked: root.confirm(root.panelState.pendingConfirmation) } } } } }