It is the system settings app for this desktop, so it is named the way one is. The window is "Settings", the titlebar is "Settings", the wordmark above the search field is gone, the sidebar's last row is "About", and the status line reads "Desktop is healthy". Prose followed the same rule. Forty-odd strings explained what "Panama" does -- "Panama never animates while idle", "Restore Panama defaults", "Panama looks in ~/Pictures/Wallpapers" -- which is how a product describes itself, not how a settings panel describes a setting. They now say what happens. No user-visible "Panama" remains anywhere in the app. Two consequences worth naming: The SUPER+I shortcut's description is user-visible, because the Shortcuts page is generated from it, so that is renamed too. Rebindings are keyed by shipped chord rather than description, so no existing override is orphaned by this. Three contracts matched the window by title and one matched that shortcut by description; all four are updated. There are no Hyprland window rules keyed on the title, so nothing about the window's placement changes. The desktop entry is now Name=Settings, but the FILE keeps its panama-settings name, as does the icon: the dock pins applications by desktop id, and renaming the file would silently unpin it. dock-pins-contract covers exactly that. The dated design docs under docs/superpowers keep the old name. They are a record of what was decided when, and editing them to agree with the present would make them lie about the past. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
479 lines
18 KiB
QML
479 lines
18 KiB
QML
import QtQuick
|
|
import qs.config
|
|
import qs.services
|
|
|
|
SettingsPage {
|
|
id: root
|
|
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()
|
|
|
|
readonly property var availableLights: HomeAssistant.catalog.filter(entity => {
|
|
if (HomeAssistant.selectedEntities.some(selected => selected.id === entity.id))
|
|
return false;
|
|
const haystack = (entity.sourceName + " " + entity.id).toLowerCase();
|
|
return root.lightQuery === "" || haystack.includes(root.lightQuery);
|
|
})
|
|
readonly property string availableEmptyText: root.availableLights.length > 0
|
|
? ""
|
|
: (root.lightQuery !== ""
|
|
? "No lights match that search"
|
|
: (HomeAssistant.catalog.length === 0
|
|
? "No lights discovered"
|
|
: "All discovered lights are already selected"))
|
|
readonly property var pageDiagnostics: ({
|
|
availableLightIds: root.availableLights.map(entity => entity.id),
|
|
availableEmptyText: root.availableEmptyText,
|
|
homeStatus: root.homeStatus()
|
|
})
|
|
|
|
function homeStatus(): string {
|
|
if (HomeAssistant.lastError === "authentication-required")
|
|
return "Authentication required";
|
|
if (HomeAssistant.lastError === "not-configured")
|
|
return "Home Assistant is not configured";
|
|
if (HomeAssistant.phase === "ready")
|
|
return `Connected · ${HomeAssistant.discoveredCount} lights discovered`;
|
|
if (HomeAssistant.phase === "degraded")
|
|
return "Last update unavailable · showing saved controls";
|
|
if (HomeAssistant.phase === "loading")
|
|
return "Connecting to Home Assistant";
|
|
return "Home Assistant is unavailable";
|
|
}
|
|
|
|
function saveHomeAssistantConfig(): void {
|
|
HomeAssistantConfig.save(
|
|
homeUrlInput.text,
|
|
homeEntitiesInput.text,
|
|
homeTokenInput.text
|
|
);
|
|
}
|
|
|
|
Connections {
|
|
target: HomeAssistantConfig
|
|
|
|
function onConfigurationSaved(): void {
|
|
homeTokenInput.clear();
|
|
homeUrlInput.text = HomeAssistantConfig.url;
|
|
homeEntitiesInput.text = HomeAssistantConfig.entities.join(", ");
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Home Assistant"
|
|
subtitle: root.homeStatus()
|
|
|
|
SettingRow {
|
|
label: "Connection"
|
|
detail: HomeAssistantConfig.tokenConfigured
|
|
? "A long-lived access token is stored privately"
|
|
: "Paste a long-lived access token to connect"
|
|
value: HomeAssistantConfig.configured ? "Configured" : "Not configured"
|
|
}
|
|
|
|
SettingRow {
|
|
label: "Server URL"
|
|
detail: "The local or remote address of Home Assistant"
|
|
controlWidth: 330
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
radius: Theme.pillRadius
|
|
color: Theme.alpha(Theme.fg, 0.07)
|
|
border.width: homeUrlInput.activeFocus ? 2 : 1
|
|
border.color: homeUrlInput.activeFocus
|
|
? Theme.alpha(Theme.accent, 0.55) : "transparent"
|
|
|
|
TextInput {
|
|
id: homeUrlInput
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 12
|
|
anchors.rightMargin: 12
|
|
activeFocusOnTab: true
|
|
text: HomeAssistantConfig.url
|
|
color: Theme.fg
|
|
selectionColor: Theme.alpha(Theme.accent, 0.5)
|
|
selectedTextColor: Theme.fg
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSize
|
|
verticalAlignment: TextInput.AlignVCenter
|
|
clip: true
|
|
|
|
Text {
|
|
anchors.fill: parent
|
|
visible: homeUrlInput.text === ""
|
|
text: "https://homeassistant.local:8123"
|
|
color: Theme.fgMuted
|
|
font: homeUrlInput.font
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingRow {
|
|
label: "Access token"
|
|
detail: HomeAssistantConfig.tokenConfigured
|
|
? "Stored · leave blank to keep it"
|
|
: "Create one in your Home Assistant profile"
|
|
controlWidth: 330
|
|
|
|
PasswordField {
|
|
id: homeTokenInput
|
|
anchors.fill: parent
|
|
placeholder: HomeAssistantConfig.tokenConfigured
|
|
? "Stored token" : "Long-lived access token"
|
|
onAccepted: root.saveHomeAssistantConfig()
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
spacing: 7
|
|
topPadding: 10
|
|
bottomPadding: 12
|
|
|
|
Text {
|
|
width: parent.width
|
|
text: "Light entities"
|
|
color: Theme.fg
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSize
|
|
font.weight: Font.Medium
|
|
}
|
|
|
|
Text {
|
|
width: parent.width
|
|
text: "Comma-separated entity IDs. These define the discoverable light catalog."
|
|
color: Theme.fgDim
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
|
|
Rectangle {
|
|
width: parent.width
|
|
height: 72
|
|
radius: 10
|
|
color: Theme.alpha(Theme.fg, 0.055)
|
|
border.width: homeEntitiesInput.activeFocus ? 2 : 1
|
|
border.color: homeEntitiesInput.activeFocus
|
|
? Theme.alpha(Theme.accent, 0.55) : Theme.alpha(Theme.fg, 0.06)
|
|
|
|
TextEdit {
|
|
id: homeEntitiesInput
|
|
anchors.fill: parent
|
|
anchors.margins: 10
|
|
activeFocusOnTab: true
|
|
text: HomeAssistantConfig.entities.join(", ")
|
|
color: Theme.fg
|
|
selectionColor: Theme.alpha(Theme.accent, 0.5)
|
|
selectedTextColor: Theme.fg
|
|
font.family: Theme.fontMono
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
wrapMode: TextEdit.Wrap
|
|
clip: true
|
|
|
|
Text {
|
|
anchors.fill: parent
|
|
visible: homeEntitiesInput.text === ""
|
|
text: "light.living_room, light.kitchen"
|
|
color: Theme.fgMuted
|
|
font: homeEntitiesInput.font
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Text {
|
|
width: parent.width
|
|
visible: HomeAssistantConfig.lastError !== ""
|
|
text: HomeAssistantConfig.lastError
|
|
color: Theme.danger
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
wrapMode: Text.WordWrap
|
|
bottomPadding: 9
|
|
}
|
|
|
|
SettingRow {
|
|
label: "Private configuration"
|
|
detail: "Saved with owner-only permissions in a private environment file"
|
|
controlWidth: 216
|
|
|
|
Row {
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 8
|
|
|
|
SettingsButton {
|
|
id: clearTokenButton
|
|
text: "Clear token"
|
|
enabled: HomeAssistantConfig.tokenConfigured && !HomeAssistantConfig.busy
|
|
activeFocusOnTab: enabled
|
|
border.width: activeFocus ? 2 : 1
|
|
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
|
onClicked: HomeAssistantConfig.clearToken()
|
|
Keys.onReturnPressed: if (enabled) HomeAssistantConfig.clearToken()
|
|
Keys.onSpacePressed: if (enabled) HomeAssistantConfig.clearToken()
|
|
}
|
|
|
|
SettingsButton {
|
|
id: saveHomeConfigButton
|
|
text: HomeAssistantConfig.busy ? "Saving…" : "Save"
|
|
tone: "accent"
|
|
enabled: !HomeAssistantConfig.busy
|
|
activeFocusOnTab: enabled
|
|
border.width: activeFocus ? 2 : 0
|
|
border.color: activeFocus ? Theme.fg : "transparent"
|
|
onClicked: root.saveHomeAssistantConfig()
|
|
Keys.onReturnPressed: if (enabled) root.saveHomeAssistantConfig()
|
|
Keys.onSpacePressed: if (enabled) root.saveHomeAssistantConfig()
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingRow {
|
|
label: "Light catalog"
|
|
detail: "Light state is read 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 {
|
|
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
|
|
canMoveEarlier: index > 0
|
|
canMoveLater: index < favoritesGrid.count - 1
|
|
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 {
|
|
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 {
|
|
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 {
|
|
width: parent.width
|
|
visible: root.availableLights.length > 0
|
|
|
|
Repeater {
|
|
model: root.availableLights
|
|
|
|
AvailableLightRow {
|
|
required property var modelData
|
|
width: parent.width
|
|
entity: modelData
|
|
onAddRequested: id => HomePreferences.add(id)
|
|
}
|
|
}
|
|
}
|
|
|
|
Text {
|
|
width: parent.width
|
|
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
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Phone continuity"
|
|
subtitle: "Keep the Messages handoff independent from phone connectivity."
|
|
|
|
SettingRow {
|
|
label: "Messages"
|
|
detail: "Opens BlueBubbles"
|
|
divider: false
|
|
controlWidth: 204
|
|
|
|
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")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|