Files
Panama/config/dot/quickshell/modules/quicksettings/HomeControls.qml
T
Gabriel Brown 7578348db1 Merge Home & Phone into a three-tab Home that knows your house
Home is now Overview | My Home | Phone. Overview leads with quick-action
tiles (focus, Do Not Disturb, health, snapshots, storage), keeps the
findings card — updates fold in, the reclaim-space prompt is gone on
purpose — and adds glance cards, the next calendar event, and weather.

My Home groups every light by Home Assistant area: the helper gained an
`areas` command (one REST template render, no websocket), and the rooms
degrade to a flat list on setups without areas. The favorites editor and
connection card moved intact. Phone gains a vitals strip — battery and
cell signal read from KDE Connect's plugin D-Bus objects, where absence
is data, not an error — beside ring, clipboard, send-a-file, and the
BlueBubbles handoff.

The retired home-phone id resolves to my-home forever via a new alias
map in SettingsRoutes (with a hasOwnProperty guard so prototype names
cannot leak into settingsPage). Storage no longer claims 0 B free — the
old page read a field the disks helper never emitted.

Contracts updated alongside; per the new workflow, the full suite runs
once at the end of the redesign (see the test backlog note).

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-23 22:06:18 -04:00

307 lines
11 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import QtQuick
import qs.config
import qs.services
Item {
id: root
property bool expanded: false
signal toggleExpanded
readonly property bool hasSelection: HomeAssistant.selectedEntities.length > 0
readonly property bool showSetup: HomeAssistant.phase === "ready" && !root.hasSelection
implicitHeight: content.implicitHeight
Column {
id: content
width: parent.width
spacing: 6
ControlSectionHeader {
width: parent.width
label: "Home"
action: root.headerAction()
actionEnabled: HomeAssistant.phase !== "loading"
onActionTriggered: {
if (root.hasSelection) {
root.toggleExpanded();
} else if (root.showSetup) {
root.openHomeSettings();
} else {
HomeAssistant.refresh();
}
}
}
Rectangle {
id: restingCard
// Shown when the section is closed; the expanded grid below replaces
// it when open.
readonly property bool shown: root.hasSelection && !root.expanded
width: parent.width
// Animated, and on the same curve and duration as the Section that
// replaces it. Previously this hid the instant `expanded` flipped:
// a Column skips invisible children, so everything above snapped up
// while the expanded grid was still sliding open underneath. That
// read as a glitch rather than as an animation, and only here,
// because this is the only control with a resting and an expanded
// form that swap.
height: restingCard.shown ? restingGrid.implicitHeight + 20 : 0
visible: height > 0
clip: true
Behavior on height {
NumberAnimation {
duration: Theme.durNormal
easing.type: Easing.OutCubic
}
}
radius: Theme.cardRadius + 2
color: Theme.alpha(Theme.warn, 0.028)
border.width: 1
border.color: Theme.alpha(Theme.warn, 0.095)
Grid {
id: restingGrid
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 10
columns: 2
spacing: 8
Repeater {
model: HomeAssistant.visibleEntities
HomeTile {
required property var modelData
width: (restingGrid.width - restingGrid.spacing) / 2
entity: modelData
busy: HomeAssistant.isBusy(modelData.id)
pendingBrightness: HomeAssistant.pendingFor(modelData.id)
actionError: HomeAssistant.errorFor(modelData.id)
onPowerRequested: HomeAssistant.toggleEntity(modelData.id)
onBrightnessRequested: value => HomeAssistant.setBrightness(modelData.id, value)
}
}
}
}
Rectangle {
width: parent.width
height: visible ? 72 : 0
visible: !root.hasSelection
radius: Theme.cardRadius
color: root.showSetup
? Theme.alpha(Theme.accent, 0.055)
: Theme.alpha(Theme.fg, 0.045)
border.width: 1
border.color: root.showSetup
? Theme.alpha(Theme.accent, 0.12)
: Theme.alpha(Theme.fg, 0.055)
Text {
id: stateIcon
anchors.left: parent.left
anchors.leftMargin: 13
anchors.verticalCenter: parent.verticalCenter
text: root.showSetup ? "\u{F0335}" : "\u{F02DC}"
color: root.showSetup ? Theme.accent : Theme.fgDim
font.family: Theme.fontMono
font.pixelSize: 17
}
Column {
anchors.left: stateIcon.right
anchors.leftMargin: 11
anchors.right: emptyAction.left
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
spacing: 3
Text {
width: parent.width
text: root.emptyTitle()
color: Theme.fg
elide: Text.ElideRight
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
font.weight: Font.DemiBold
}
Text {
width: parent.width
text: root.emptyDetail()
color: Theme.fgDim
elide: Text.ElideRight
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
Rectangle {
id: emptyAction
anchors.right: parent.right
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
width: root.showSetup ? 68 : 52
height: 30
radius: 10
visible: HomeAssistant.phase !== "loading"
color: emptyActionMouse.containsMouse
? Theme.alpha(Theme.accent, 0.20)
: Theme.alpha(Theme.accent, 0.105)
Text {
anchors.centerIn: parent
text: root.showSetup ? "Manage" : "Open"
color: Theme.accent
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
}
MouseArea {
id: emptyActionMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.showSetup)
root.openHomeSettings();
else
HomeAssistant.open();
}
}
}
}
Rectangle {
width: parent.width
height: visible ? 34 : 0
visible: HomeAssistant.stale
radius: 10
color: Theme.alpha(Theme.warn, 0.075)
Text {
anchors.left: parent.left
anchors.leftMargin: 11
anchors.verticalCenter: parent.verticalCenter
text: "Last known state · " + root.countSummary()
color: Theme.warn
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
Text {
anchors.right: parent.right
anchors.rightMargin: 11
anchors.verticalCenter: parent.verticalCenter
text: "Retry"
color: Theme.accent
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
MouseArea {
anchors.fill: parent
anchors.margins: -6
cursorShape: Qt.PointingHandCursor
onClicked: HomeAssistant.refresh()
}
}
}
Section {
width: parent.width
expanded: root.expanded && root.hasSelection
ScrollColumn {
width: parent.width
maxHeight: 324
spacing: 8
Grid {
id: expandedGrid
width: parent.width
columns: 2
spacing: 8
Repeater {
model: HomeAssistant.selectedEntities
HomeTile {
required property var modelData
width: (expandedGrid.width - expandedGrid.spacing) / 2
entity: modelData
busy: HomeAssistant.isBusy(modelData.id)
pendingBrightness: HomeAssistant.pendingFor(modelData.id)
actionError: HomeAssistant.errorFor(modelData.id)
onPowerRequested: HomeAssistant.toggleEntity(modelData.id)
onBrightnessRequested: value => HomeAssistant.setBrightness(modelData.id, value)
}
}
}
RowButton {
width: parent.width
icon: "preferences-system-symbolic"
label: "Manage in Settings"
sublabel: root.countSummary()
onClicked: root.openHomeSettings()
}
}
}
}
function headerAction(): string {
if (root.hasSelection) {
const noun = HomeAssistant.configuredCount === 1 ? "accessory" : "accessories";
return HomeAssistant.configuredCount + " " + noun + (root.expanded ? " ⌃" : " ");
}
if (HomeAssistant.phase === "loading")
return "Loading";
if (root.showSetup)
return "Manage";
return "Retry";
}
function countSummary(): string {
return HomeAssistant.configuredCount + " selected · "
+ HomeAssistant.discoveredCount + " discovered";
}
function emptyTitle(): string {
if (root.showSetup)
return "Choose your accessories";
if (HomeAssistant.phase === "loading")
return "Loading your home";
if (HomeAssistant.lastError === "authentication-required")
return "Authentication required";
if (HomeAssistant.lastError === "not-configured")
return "Home Assistant is not configured";
return "Home Assistant is unavailable";
}
function emptyDetail(): string {
if (root.showSetup) {
if (HomeAssistant.discoveredCount === 0)
return "No lights discovered yet";
const noun = HomeAssistant.discoveredCount === 1 ? "light" : "lights";
return HomeAssistant.discoveredCount + " " + noun + " ready to add";
}
if (HomeAssistant.phase === "loading")
return "Finding your selected lights";
if (HomeAssistant.lastError === "authentication-required")
return "Update the long-lived access token";
if (HomeAssistant.lastError === "not-configured")
return "Connect Home Assistant in Settings";
return "Check the connection, then retry";
}
function openHomeSettings(): void {
ShellState.close();
ShellState.openSettings("my-home");
}
}