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
307 lines
11 KiB
QML
307 lines
11 KiB
QML
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");
|
||
}
|
||
}
|