Finish Home and Phone settings cohesion
This commit is contained in:
@@ -82,6 +82,14 @@ Singleton {
|
|||||||
values.initialized = true;
|
values.initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetHomeDefaults(): void {
|
||||||
|
persistTimer.stop();
|
||||||
|
values.favorites = [];
|
||||||
|
values.initialized = false;
|
||||||
|
root.saveError = "";
|
||||||
|
preferencesFile.writeAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
function isSelected(entityId: string): bool {
|
function isSelected(entityId: string): bool {
|
||||||
for (var index = 0; index < values.favorites.length; index++) {
|
for (var index = 0; index < values.favorites.length; index++) {
|
||||||
if (values.favorites[index].id === entityId) {
|
if (values.favorites[index].id === entityId) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ ShellRoot {
|
|||||||
function alias(id: string, value: string): void { HomePreferences.setAlias(id, value); }
|
function alias(id: string, value: string): void { HomePreferences.setAlias(id, value); }
|
||||||
function move(id: string, index: int): void { HomePreferences.move(id, index); }
|
function move(id: string, index: int): void { HomePreferences.move(id, index); }
|
||||||
function remove(id: string): void { HomePreferences.remove(id); }
|
function remove(id: string): void { HomePreferences.remove(id); }
|
||||||
|
function reset(): void { HomePreferences.resetHomeDefaults(); }
|
||||||
function status(): string {
|
function status(): string {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
initialized: HomePreferences.initialized,
|
initialized: HomePreferences.initialized,
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import QtQuick
|
|||||||
import qs.config
|
import qs.config
|
||||||
import qs.services
|
import qs.services
|
||||||
|
|
||||||
Item {
|
SettingsPage {
|
||||||
id: root
|
id: root
|
||||||
objectName: "home-phone-page"
|
objectName: "home-phone-page"
|
||||||
|
title: "Home & Phone"
|
||||||
|
lede: "Choose what appears in Control Center and keep phone continuity close at hand."
|
||||||
|
|
||||||
property string lightQuery: searchInput.text.trim().toLowerCase()
|
property string lightQuery: searchInput.text.trim().toLowerCase()
|
||||||
|
|
||||||
@@ -41,274 +43,242 @@ Item {
|
|||||||
return "Home Assistant is unavailable";
|
return "Home Assistant is unavailable";
|
||||||
}
|
}
|
||||||
|
|
||||||
Flickable {
|
SettingsCard {
|
||||||
anchors.fill: parent
|
title: "Home Assistant"
|
||||||
clip: true
|
subtitle: root.homeStatus()
|
||||||
contentWidth: width
|
|
||||||
contentHeight: content.implicitHeight + 64
|
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
|
||||||
|
|
||||||
Column {
|
SettingRow {
|
||||||
id: content
|
label: "Light catalog"
|
||||||
width: parent.width - 68
|
detail: "Panama reads light state through the Home Assistant helper."
|
||||||
x: 34
|
divider: false
|
||||||
y: 30
|
controlWidth: 176
|
||||||
spacing: 16
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
id: refreshButton
|
||||||
|
text: "Refresh"
|
||||||
|
activeFocusOnTab: true
|
||||||
|
border.width: activeFocus ? 2 : 1
|
||||||
|
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
||||||
|
onClicked: HomeAssistant.refresh()
|
||||||
|
Keys.onReturnPressed: HomeAssistant.refresh()
|
||||||
|
Keys.onSpacePressed: HomeAssistant.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
id: openHomeButton
|
||||||
|
text: "Open"
|
||||||
|
activeFocusOnTab: true
|
||||||
|
border.width: activeFocus ? 2 : 1
|
||||||
|
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
||||||
|
onClicked: HomeAssistant.open()
|
||||||
|
Keys.onReturnPressed: HomeAssistant.open()
|
||||||
|
Keys.onSpacePressed: HomeAssistant.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Control Center lights"
|
||||||
|
subtitle: HomeAssistant.selectedEntities.length === 0
|
||||||
|
? "Select the lights that belong on your shelf."
|
||||||
|
: `${Math.min(4, HomeAssistant.selectedEntities.length)} in Control Center · ${HomeAssistant.selectedEntities.length} selected`
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: HomeAssistant.selectedEntities.length === 0
|
||||||
|
text: "Choose lights below to build your Control Center shelf."
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
topPadding: 3
|
||||||
|
bottomPadding: 13
|
||||||
|
}
|
||||||
|
|
||||||
|
GridView {
|
||||||
|
id: favoritesGrid
|
||||||
|
width: parent.width
|
||||||
|
height: Math.ceil(count / 2) * cellHeight
|
||||||
|
visible: count > 0
|
||||||
|
interactive: false
|
||||||
|
clip: false
|
||||||
|
cellWidth: width / 2
|
||||||
|
cellHeight: 116
|
||||||
|
model: HomeAssistant.selectedEntities
|
||||||
|
|
||||||
|
delegate: HomeFavoriteCard {
|
||||||
|
required property var modelData
|
||||||
|
width: GridView.view.cellWidth - 6
|
||||||
|
height: 108
|
||||||
|
favorite: ({
|
||||||
|
id: modelData.id,
|
||||||
|
alias: modelData.name === modelData.sourceName ? "" : modelData.name
|
||||||
|
})
|
||||||
|
sourceName: modelData.sourceName
|
||||||
|
featured: index < 4
|
||||||
|
onAliasCommitted: (id, alias) => HomePreferences.setAlias(id, alias)
|
||||||
|
onMoveRequested: (id, targetIndex) => HomePreferences.move(id, targetIndex)
|
||||||
|
onRemoveRequested: id => HomePreferences.remove(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 46
|
||||||
|
visible: HomePreferences.saveError !== ""
|
||||||
|
radius: 9
|
||||||
|
color: Theme.alpha(Theme.warn, 0.09)
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.alpha(Theme.warn, 0.24)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "Home & Phone"
|
anchors.left: parent.left
|
||||||
color: Theme.fg
|
anchors.leftMargin: 12
|
||||||
|
anchors.right: retryButton.left
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: HomePreferences.saveError
|
||||||
|
color: Theme.warn
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 27
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
font.weight: Font.DemiBold
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
id: retryButton
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 8
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Retry"
|
||||||
|
activeFocusOnTab: true
|
||||||
|
border.width: activeFocus ? 2 : 1
|
||||||
|
border.color: activeFocus ? Theme.warn : Theme.alpha(Theme.fg, 0.08)
|
||||||
|
onClicked: HomePreferences.retrySave()
|
||||||
|
Keys.onReturnPressed: HomePreferences.retrySave()
|
||||||
|
Keys.onSpacePressed: HomePreferences.retrySave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Available lights"
|
||||||
|
subtitle: "Search the Home Assistant catalog by source name or entity ID."
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 38
|
||||||
|
radius: 10
|
||||||
|
color: Theme.alpha(Theme.fg, searchInput.activeFocus ? 0.08 : 0.05)
|
||||||
|
border.width: searchInput.activeFocus ? 2 : 1
|
||||||
|
border.color: searchInput.activeFocus
|
||||||
|
? Theme.alpha(Theme.accent, 0.72)
|
||||||
|
: Theme.alpha(Theme.fg, 0.065)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "Choose what appears in Control Center and keep phone continuity close at hand."
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 11
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "\u{F0349}"
|
||||||
color: Theme.fgDim
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontMono
|
||||||
|
font.pixelSize: 14
|
||||||
|
}
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
id: searchInput
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 36
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 11
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
activeFocusOnTab: true
|
||||||
|
color: Theme.fg
|
||||||
|
selectionColor: Theme.accent
|
||||||
|
selectedTextColor: Theme.bgDark
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: Theme.fontSize
|
font.pixelSize: Theme.fontSize
|
||||||
bottomPadding: 6
|
clip: true
|
||||||
}
|
|
||||||
|
|
||||||
SettingsCard {
|
|
||||||
title: "Home Assistant"
|
|
||||||
subtitle: root.homeStatus()
|
|
||||||
|
|
||||||
SettingRow {
|
|
||||||
label: "Light catalog"
|
|
||||||
detail: "Panama reads light state through the Home Assistant helper."
|
|
||||||
divider: false
|
|
||||||
controlWidth: 176
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
SettingsButton {
|
|
||||||
id: refreshButton
|
|
||||||
text: "Refresh"
|
|
||||||
activeFocusOnTab: true
|
|
||||||
border.width: activeFocus ? 2 : 1
|
|
||||||
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
|
||||||
onClicked: HomeAssistant.refresh()
|
|
||||||
Keys.onReturnPressed: HomeAssistant.refresh()
|
|
||||||
Keys.onSpacePressed: HomeAssistant.refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsButton {
|
|
||||||
id: openHomeButton
|
|
||||||
text: "Open"
|
|
||||||
activeFocusOnTab: true
|
|
||||||
border.width: activeFocus ? 2 : 1
|
|
||||||
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
|
||||||
onClicked: HomeAssistant.open()
|
|
||||||
Keys.onReturnPressed: HomeAssistant.open()
|
|
||||||
Keys.onSpacePressed: HomeAssistant.open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsCard {
|
|
||||||
title: "Control Center lights"
|
|
||||||
subtitle: HomeAssistant.selectedEntities.length === 0
|
|
||||||
? "Select the lights that belong on your shelf."
|
|
||||||
: `${Math.min(4, HomeAssistant.selectedEntities.length)} in Control Center · ${HomeAssistant.selectedEntities.length} selected`
|
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
width: parent.width
|
anchors.fill: parent
|
||||||
visible: HomeAssistant.selectedEntities.length === 0
|
visible: searchInput.text === "" && !searchInput.activeFocus
|
||||||
text: "Choose lights below to build your Control Center shelf."
|
text: "Search available lights"
|
||||||
color: Theme.fgDim
|
color: Theme.fgMuted
|
||||||
font.family: Theme.fontFamily
|
font: searchInput.font
|
||||||
font.pixelSize: Theme.fontSize
|
verticalAlignment: Text.AlignVCenter
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
topPadding: 3
|
|
||||||
bottomPadding: 13
|
|
||||||
}
|
|
||||||
|
|
||||||
GridView {
|
|
||||||
id: favoritesGrid
|
|
||||||
width: parent.width
|
|
||||||
height: Math.ceil(count / 2) * cellHeight
|
|
||||||
visible: count > 0
|
|
||||||
interactive: false
|
|
||||||
clip: false
|
|
||||||
cellWidth: width / 2
|
|
||||||
cellHeight: 116
|
|
||||||
model: HomeAssistant.selectedEntities
|
|
||||||
|
|
||||||
delegate: HomeFavoriteCard {
|
|
||||||
required property var modelData
|
|
||||||
width: GridView.view.cellWidth - 6
|
|
||||||
height: 108
|
|
||||||
favorite: ({
|
|
||||||
id: modelData.id,
|
|
||||||
alias: modelData.name === modelData.sourceName ? "" : modelData.name
|
|
||||||
})
|
|
||||||
sourceName: modelData.sourceName
|
|
||||||
featured: index < 4
|
|
||||||
onAliasCommitted: (id, alias) => HomePreferences.setAlias(id, alias)
|
|
||||||
onMoveRequested: (id, targetIndex) => HomePreferences.move(id, targetIndex)
|
|
||||||
onRemoveRequested: id => HomePreferences.remove(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 46
|
|
||||||
visible: HomePreferences.saveError !== ""
|
|
||||||
radius: 9
|
|
||||||
color: Theme.alpha(Theme.warn, 0.09)
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.alpha(Theme.warn, 0.24)
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 12
|
|
||||||
anchors.right: retryButton.left
|
|
||||||
anchors.rightMargin: 12
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: HomePreferences.saveError
|
|
||||||
color: Theme.warn
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsButton {
|
|
||||||
id: retryButton
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: 8
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: "Retry"
|
|
||||||
activeFocusOnTab: true
|
|
||||||
border.width: activeFocus ? 2 : 1
|
|
||||||
border.color: activeFocus ? Theme.warn : Theme.alpha(Theme.fg, 0.08)
|
|
||||||
onClicked: HomePreferences.retrySave()
|
|
||||||
Keys.onReturnPressed: HomePreferences.retrySave()
|
|
||||||
Keys.onSpacePressed: HomePreferences.retrySave()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SettingsCard {
|
Column {
|
||||||
title: "Available lights"
|
width: parent.width
|
||||||
subtitle: "Search the Home Assistant catalog by source name or entity ID."
|
visible: root.availableLights.length > 0
|
||||||
|
|
||||||
Rectangle {
|
Repeater {
|
||||||
|
model: root.availableLights
|
||||||
|
|
||||||
|
AvailableLightRow {
|
||||||
|
required property var modelData
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 38
|
entity: modelData
|
||||||
radius: 10
|
onAddRequested: id => HomePreferences.add(id)
|
||||||
color: Theme.alpha(Theme.fg, searchInput.activeFocus ? 0.08 : 0.05)
|
|
||||||
border.width: searchInput.activeFocus ? 2 : 1
|
|
||||||
border.color: searchInput.activeFocus
|
|
||||||
? Theme.alpha(Theme.accent, 0.72)
|
|
||||||
: Theme.alpha(Theme.fg, 0.065)
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 11
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: "\u{F0349}"
|
|
||||||
color: Theme.fgDim
|
|
||||||
font.family: Theme.fontMono
|
|
||||||
font.pixelSize: 14
|
|
||||||
}
|
|
||||||
|
|
||||||
TextInput {
|
|
||||||
id: searchInput
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 36
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: 11
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
activeFocusOnTab: true
|
|
||||||
color: Theme.fg
|
|
||||||
selectionColor: Theme.accent
|
|
||||||
selectedTextColor: Theme.bgDark
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSize
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: searchInput.text === "" && !searchInput.activeFocus
|
|
||||||
text: "Search available lights"
|
|
||||||
color: Theme.fgMuted
|
|
||||||
font: searchInput.font
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column {
|
Text {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
visible: root.availableLights.length > 0
|
visible: root.availableLights.length === 0
|
||||||
|
text: root.availableEmptyText
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
topPadding: 18
|
||||||
|
bottomPadding: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Repeater {
|
SettingsCard {
|
||||||
model: root.availableLights
|
title: "Phone continuity"
|
||||||
|
subtitle: "Keep the Messages handoff independent from phone connectivity."
|
||||||
|
|
||||||
AvailableLightRow {
|
SettingRow {
|
||||||
required property var modelData
|
label: "Messages"
|
||||||
width: parent.width
|
detail: "Opens BlueBubbles"
|
||||||
entity: modelData
|
divider: false
|
||||||
onAddRequested: id => HomePreferences.add(id)
|
controlWidth: 204
|
||||||
}
|
|
||||||
}
|
Row {
|
||||||
}
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 12
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
width: parent.width
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: root.availableLights.length === 0
|
text: SystemSettings.bluebubblesAvailable ? "Installed" : "Unavailable"
|
||||||
text: root.availableEmptyText
|
color: SystemSettings.bluebubblesAvailable ? Theme.ok : Theme.fgMuted
|
||||||
color: Theme.fgDim
|
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: Theme.fontSize
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
topPadding: 18
|
|
||||||
bottomPadding: 10
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
SettingsCard {
|
SettingsButton {
|
||||||
title: "Phone continuity"
|
id: openBlueBubblesButton
|
||||||
subtitle: "Keep the Messages handoff independent from phone connectivity."
|
text: "Open"
|
||||||
|
enabled: SystemSettings.bluebubblesAvailable
|
||||||
SettingRow {
|
activeFocusOnTab: enabled
|
||||||
label: "Messages"
|
border.width: activeFocus ? 2 : 1
|
||||||
detail: "Opens BlueBubbles"
|
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
||||||
divider: false
|
onClicked: SystemSettings.openApplication("bluebubbles")
|
||||||
controlWidth: 204
|
Keys.onReturnPressed: if (enabled) SystemSettings.openApplication("bluebubbles")
|
||||||
|
Keys.onSpacePressed: if (enabled) SystemSettings.openApplication("bluebubbles")
|
||||||
Row {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 12
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: SystemSettings.bluebubblesAvailable ? "Installed" : "Unavailable"
|
|
||||||
color: SystemSettings.bluebubblesAvailable ? Theme.ok : Theme.fgMuted
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsButton {
|
|
||||||
id: openBlueBubblesButton
|
|
||||||
text: "Open"
|
|
||||||
enabled: SystemSettings.bluebubblesAvailable
|
|
||||||
activeFocusOnTab: enabled
|
|
||||||
border.width: activeFocus ? 2 : 1
|
|
||||||
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
|
||||||
onClicked: SystemSettings.openApplication("bluebubbles")
|
|
||||||
Keys.onReturnPressed: if (enabled) SystemSettings.openApplication("bluebubbles")
|
|
||||||
Keys.onSpacePressed: if (enabled) SystemSettings.openApplication("bluebubbles")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# Settings Home & Phone Completion Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Finish the Home & Phone Settings page on Panama's shared Settings vocabulary and give the global reset path a named, durable Home preference API.
|
||||||
|
|
||||||
|
**Architecture:** `HomePhonePage` adopts `SettingsPage` for the shared scrolling/title/lede scaffold while retaining its specialized catalog, reorder, alias, and phone controls. `HomePreferences.resetHomeDefaults()` becomes the sole Home-store reset boundary: it returns the store to fresh-install state and writes immediately so `SystemSettings.restoreDefaults()` can call it without mutating aliases.
|
||||||
|
|
||||||
|
**Tech Stack:** Quickshell 0.3 QML, QtQuick, Bash contract tests, Hyprland.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-08-17-panama-cohesion-design.md`
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Do not touch `SystemSettings.qml`, `PreferenceSchema.qml`, keybinds, or Claude's shared row implementations in this branch.
|
||||||
|
- Preserve ordered Home favorites, aliases, first-four Control Center badges, search, drag and keyboard reorder, retry state, Home Assistant status copy, and BlueBubbles availability behavior.
|
||||||
|
- Automated tests must not toggle or dim a real light and must not launch BlueBubbles.
|
||||||
|
- New behavior follows red-green TDD; fixture and state directories remain isolated from the live desktop.
|
||||||
|
- No mock phase and no new visual direction: this is cohesion work against the already-approved design.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Shared Home & Phone page and named reset boundary
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `config/dot/quickshell/config/HomePreferences.qml`
|
||||||
|
- Modify: `config/dot/quickshell/home-preferences-harness.qml`
|
||||||
|
- Modify: `config/dot/quickshell/modules/settings/HomePhonePage.qml`
|
||||||
|
- Modify only if required for shared vocabulary compatibility: `config/dot/quickshell/modules/settings/HomeFavoriteCard.qml`
|
||||||
|
- Modify only if required for shared vocabulary compatibility: `config/dot/quickshell/modules/settings/AvailableLightRow.qml`
|
||||||
|
- Modify: `tests/quickshell/home-preferences-contract.sh`
|
||||||
|
- Modify: `tests/quickshell/home-phone-settings-contract.sh`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `SettingsPage { title; lede; default content }`, existing `SettingsCard`, `SettingRow`, `ActionRow`, `TextRow`, `HomeAssistant`, `SystemSettings.bluebubblesAvailable`, and writable `HomePreferences` adapter state.
|
||||||
|
- Produces: `HomePreferences.resetHomeDefaults(): void`, which sets `favorites` to `[]`, sets `initialized` to `false`, clears stale save error state, and invokes `preferencesFile.writeAdapter()` immediately after stopping the debounce timer.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing contracts**
|
||||||
|
|
||||||
|
Add `reset()` to the isolated `home-pref-test` IPC harness. Extend `home-preferences-contract.sh` to seed aliases/order, invoke reset, and require both IPC state and `panama-home.json` to become exactly `{"initialized":false,"favorites":[]}` without waiting for the 180 ms debounce interval. Extend `home-phone-settings-contract.sh` to require `SettingsPage {`, `title: "Home & Phone"`, and the existing lede through `lede:`, while rejecting the copied root `Flickable` scaffold.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run contracts to verify RED**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/quickshell/home-preferences-contract.sh
|
||||||
|
tests/quickshell/home-phone-settings-contract.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: the preference contract fails because `reset` is missing; the page contract fails because the page still owns a copied `Flickable` scaffold.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the reset API and shared page scaffold**
|
||||||
|
|
||||||
|
Implement this public boundary in `HomePreferences.qml`:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
function resetHomeDefaults(): void {
|
||||||
|
persistTimer.stop();
|
||||||
|
values.favorites = [];
|
||||||
|
values.initialized = false;
|
||||||
|
root.saveError = "";
|
||||||
|
preferencesFile.writeAdapter();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the `HomePhonePage` root `Item` plus nested `Flickable`/title/lede scaffold with:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
SettingsPage {
|
||||||
|
id: root
|
||||||
|
objectName: "home-phone-page"
|
||||||
|
title: "Home & Phone"
|
||||||
|
lede: "Choose what appears in Control Center and keep phone continuity close at hand."
|
||||||
|
// Existing SettingsCard content remains in order.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use shared `ActionRow` or `TextRow` only where their single-action/read-only contracts preserve all current status and accessibility behavior. Keep specialized rows when the shared primitive would lose information.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run focused contracts to GREEN**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/quickshell/home-preferences-contract.sh
|
||||||
|
tests/quickshell/home-phone-settings-contract.sh
|
||||||
|
tests/quickshell/settings-pages-contract.sh
|
||||||
|
tests/quickshell/settings-rows-contract.sh
|
||||||
|
tests/quickshell/settings-commit-reset-contract.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: every command exits 0; no test launches BlueBubbles or changes a real Home Assistant entity.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add config/dot/quickshell/config/HomePreferences.qml \
|
||||||
|
config/dot/quickshell/home-preferences-harness.qml \
|
||||||
|
config/dot/quickshell/modules/settings/HomePhonePage.qml \
|
||||||
|
config/dot/quickshell/modules/settings/HomeFavoriteCard.qml \
|
||||||
|
config/dot/quickshell/modules/settings/AvailableLightRow.qml \
|
||||||
|
tests/quickshell/home-preferences-contract.sh \
|
||||||
|
tests/quickshell/home-phone-settings-contract.sh \
|
||||||
|
docs/superpowers/plans/2026-08-18-settings-home-phone-completion.md
|
||||||
|
git commit -m "Finish Home and Phone settings cohesion"
|
||||||
|
```
|
||||||
@@ -80,8 +80,12 @@ system_settings="$repo_dir/config/dot/quickshell/services/SystemSettings.qml"
|
|||||||
[[ -f "$home_page" ]] || fail 'HomePhonePage.qml is missing'
|
[[ -f "$home_page" ]] || fail 'HomePhonePage.qml is missing'
|
||||||
[[ -f "$favorite_card" ]] || fail 'HomeFavoriteCard.qml is missing'
|
[[ -f "$favorite_card" ]] || fail 'HomeFavoriteCard.qml is missing'
|
||||||
[[ -f "$available_row" ]] || fail 'AvailableLightRow.qml is missing'
|
[[ -f "$available_row" ]] || fail 'AvailableLightRow.qml is missing'
|
||||||
assert_contains 'text: "Home & Phone"' "$home_page"
|
assert_contains 'SettingsPage {' "$home_page"
|
||||||
assert_contains 'text: "Choose what appears in Control Center and keep phone continuity close at hand."' "$home_page"
|
assert_contains 'title: "Home & Phone"' "$home_page"
|
||||||
|
assert_contains 'lede: "Choose what appears in Control Center and keep phone continuity close at hand."' "$home_page"
|
||||||
|
if rg -q '^\s*Flickable \{' "$home_page"; then
|
||||||
|
fail 'HomePhonePage.qml still owns a copied Flickable scaffold'
|
||||||
|
fi
|
||||||
assert_contains 'Connected · ' "$home_page"
|
assert_contains 'Connected · ' "$home_page"
|
||||||
assert_contains 'Last update unavailable · showing saved controls' "$home_page"
|
assert_contains 'Last update unavailable · showing saved controls' "$home_page"
|
||||||
assert_contains 'Authentication required' "$home_page"
|
assert_contains 'Authentication required' "$home_page"
|
||||||
|
|||||||
@@ -83,6 +83,21 @@ wait_for_file_content() {
|
|||||||
fail 'preferences file did not contain the complete atomic update'
|
fail 'preferences file did not contain the complete atomic update'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wait_for_reset_file_content() {
|
||||||
|
local expected='{"initialized":false,"favorites":[]}'
|
||||||
|
local state_file=""
|
||||||
|
|
||||||
|
for _ in $(seq 1 18); do
|
||||||
|
state_file="$(find "$state_home" -name panama-home.json -print -quit)"
|
||||||
|
if [[ -n "$state_file" ]] \
|
||||||
|
&& jq -e --argjson expected "$expected" '. == $expected' "$state_file" >/dev/null; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
sleep 0.01
|
||||||
|
done
|
||||||
|
fail 'reset did not persist the default Home state before the debounce interval'
|
||||||
|
}
|
||||||
|
|
||||||
# A leading JSON whitespace prevents qs from expanding the array into IPC
|
# A leading JSON whitespace prevents qs from expanding the array into IPC
|
||||||
# positional arguments; JSON.parse() intentionally accepts that whitespace.
|
# positional arguments; JSON.parse() intentionally accepts that whitespace.
|
||||||
initial_ids=' ["light.kitchen","light.hall","light.desk"]'
|
initial_ids=' ["light.kitchen","light.hall","light.desk"]'
|
||||||
@@ -90,6 +105,7 @@ expected='{"initialized":true,"favorites":[{"id":"light.desk","alias":""},{"id":
|
|||||||
expected_file='{"initialized":true,"favorites":[{"id":"light.desk","alias":""},{"id":"light.kitchen","alias":"Island"}]}'
|
expected_file='{"initialized":true,"favorites":[{"id":"light.desk","alias":""},{"id":"light.kitchen","alias":"Island"}]}'
|
||||||
empty_expected='{"initialized":true,"favorites":[],"saveError":""}'
|
empty_expected='{"initialized":true,"favorites":[],"saveError":""}'
|
||||||
empty_file='{"initialized":true,"favorites":[]}'
|
empty_file='{"initialized":true,"favorites":[]}'
|
||||||
|
reset_expected='{"initialized":false,"favorites":[],"saveError":""}'
|
||||||
|
|
||||||
start_harness
|
start_harness
|
||||||
qs_for_harness ipc call home-pref-test initialize "$initial_ids" >/dev/null
|
qs_for_harness ipc call home-pref-test initialize "$initial_ids" >/dev/null
|
||||||
@@ -99,9 +115,20 @@ qs_for_harness ipc call home-pref-test remove light.hall >/dev/null
|
|||||||
wait_for_status "$expected"
|
wait_for_status "$expected"
|
||||||
wait_for_file_content "$expected_file"
|
wait_for_file_content "$expected_file"
|
||||||
|
|
||||||
|
qs_for_harness ipc call home-pref-test reset >/dev/null
|
||||||
|
wait_for_status "$reset_expected"
|
||||||
|
wait_for_reset_file_content
|
||||||
|
|
||||||
stop_harness
|
stop_harness
|
||||||
start_harness
|
start_harness
|
||||||
|
wait_for_status "$reset_expected"
|
||||||
|
|
||||||
|
qs_for_harness ipc call home-pref-test initialize "$initial_ids" >/dev/null
|
||||||
|
qs_for_harness ipc call home-pref-test alias light.kitchen ' Island ' >/dev/null
|
||||||
|
qs_for_harness ipc call home-pref-test move light.desk 0 >/dev/null
|
||||||
|
qs_for_harness ipc call home-pref-test remove light.hall >/dev/null
|
||||||
wait_for_status "$expected"
|
wait_for_status "$expected"
|
||||||
|
wait_for_file_content "$expected_file"
|
||||||
|
|
||||||
qs_for_harness ipc call home-pref-test remove light.desk >/dev/null
|
qs_for_harness ipc call home-pref-test remove light.desk >/dev/null
|
||||||
qs_for_harness ipc call home-pref-test remove light.kitchen >/dev/null
|
qs_for_harness ipc call home-pref-test remove light.kitchen >/dev/null
|
||||||
|
|||||||
Reference in New Issue
Block a user