Audited against the real Forge configuration rather than its defaults: the bindings are still in dconf, and the extension is still installed, so both the keys and the code behind them could be read directly. The port was faithful. Forge's resize() grows the window for a positive amount in every direction -- the edge only decides which side moves -- so "increase" meant grow and "decrease" meant shrink, and Y/O/I/U mapped to Wider and Taller exactly as they should have. What did not survive is direction. Forge resized one named EDGE: Y pulled the left edge leftward, O pushed the right edge rightward, and the window opened from the side you asked for. Hyprland resizes along an axis and lets the layout pick the border, so eight distinct behaviours collapse onto four and the direction is not expressible at all in dwindle. Correct on paper, wrong under the hands: the keys that used to pull a window open from one side now push it from the other, which reads as the pair being swapped. So the sizes are inverted from Forge's naming on purpose. Y and O shrink, B and M grow, I and U shorten, N and Comma lengthen. Faithfulness to a mapping nobody can feel is worth less than keys that behave the way their owner reaches for them, and the reason is written where the next person will find it. Also fixes the Home Assistant accessories dropdown, which looked glitchy for a reason that was not performance. The resting card hid the instant the section opened -- a Column skips invisible children, so everything above snapped up while the expanded grid was still sliding open underneath. It now collapses on the same curve and duration as the section replacing it. This was the only control with a resting and an expanded form that swap, which is why it was the only one that looked wrong. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
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("home-phone");
|
||
}
|
||
}
|