Merge branch 'feat/home-accessories-customization'
# Conflicts: # config/dot/quickshell/config/qmldir # config/dot/quickshell/services/SystemSettings.qml # tests/quickshell/settings-pages-contract.sh
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
// A light dimmer that previews locally and sends one value when interaction
|
||||
// ends. Home Assistant never sees the intermediate pointer positions.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property int value: 0
|
||||
property int previewValue: 0
|
||||
property bool interactionActive: false
|
||||
property string accessibleName: "Brightness"
|
||||
|
||||
readonly property bool pressed: root.interactionActive
|
||||
|
||||
signal previewChanged(int value)
|
||||
signal committed(int value)
|
||||
|
||||
implicitWidth: 160
|
||||
implicitHeight: 32
|
||||
activeFocusOnTab: root.enabled
|
||||
opacity: root.enabled ? 1 : 0.42
|
||||
|
||||
Accessible.role: Accessible.Slider
|
||||
Accessible.name: root.accessibleName
|
||||
// Qt 6.11's installed Accessible attached type has no structured value or
|
||||
// range properties, so expose both in the supported live description.
|
||||
Accessible.description: root.previewValue + " percent, range 0 to 100"
|
||||
Accessible.focusable: root.enabled
|
||||
Accessible.focused: root.activeFocus
|
||||
Accessible.onIncreaseAction: root.commitStep(5)
|
||||
Accessible.onDecreaseAction: root.commitStep(-5)
|
||||
|
||||
onValueChanged: {
|
||||
if (!root.interactionActive)
|
||||
root.updatePreview(root.value, false);
|
||||
}
|
||||
|
||||
Component.onCompleted: root.updatePreview(root.value, false)
|
||||
|
||||
Keys.onLeftPressed: root.commitStep(-5)
|
||||
Keys.onDownPressed: root.commitStep(-5)
|
||||
Keys.onRightPressed: root.commitStep(5)
|
||||
Keys.onUpPressed: root.commitStep(5)
|
||||
|
||||
Rectangle {
|
||||
id: track
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: 10
|
||||
radius: 5
|
||||
color: Theme.alpha(Theme.fg, 0.105)
|
||||
border.width: root.activeFocus ? 1 : 0
|
||||
border.color: Theme.alpha(Theme.warn, 0.72)
|
||||
|
||||
Rectangle {
|
||||
id: fill
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: track.width * root.previewValue / 100
|
||||
radius: track.radius
|
||||
color: Theme.warn
|
||||
|
||||
Behavior on width {
|
||||
enabled: !root.interactionActive
|
||||
NumberAnimation {
|
||||
duration: Theme.durFast
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: knob
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: Math.max(0, Math.min(track.width - width,
|
||||
track.width * root.previewValue / 100 - width / 2))
|
||||
width: 16
|
||||
height: 16
|
||||
radius: 8
|
||||
color: Theme.fg
|
||||
border.width: 1
|
||||
border.color: Theme.alpha(Theme.bgDark, 0.38)
|
||||
}
|
||||
}
|
||||
|
||||
// The 32 px interaction surface is intentionally much taller than the
|
||||
// 10 px track, while remaining inside this component's layout bounds.
|
||||
MouseArea {
|
||||
id: drag
|
||||
anchors.fill: parent
|
||||
enabled: root.enabled
|
||||
hoverEnabled: true
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
|
||||
onPressed: event => {
|
||||
root.forceActiveFocus();
|
||||
root.beginPointerInteraction(event.x);
|
||||
event.accepted = true;
|
||||
}
|
||||
onPositionChanged: event => root.movePointerInteraction(event.x)
|
||||
onReleased: event => {
|
||||
root.releasePointerInteraction();
|
||||
event.accepted = true;
|
||||
}
|
||||
onCanceled: root.cancelPointerInteraction()
|
||||
onWheel: event => {
|
||||
root.commitWheel(event.angleDelta.y);
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
function clamp(candidate: real): int {
|
||||
return Math.max(0, Math.min(100, Math.round(candidate)));
|
||||
}
|
||||
|
||||
function updatePreview(candidate: real, announce: bool): void {
|
||||
const nextValue = root.clamp(candidate);
|
||||
if (root.previewValue === nextValue)
|
||||
return;
|
||||
root.previewValue = nextValue;
|
||||
if (announce)
|
||||
root.previewChanged(nextValue);
|
||||
}
|
||||
|
||||
function previewAt(pointerX: real): void {
|
||||
root.updatePreview(pointerX / Math.max(1, drag.width) * 100, true);
|
||||
}
|
||||
|
||||
function beginPointerInteraction(pointerX: real): void {
|
||||
if (!root.enabled)
|
||||
return;
|
||||
root.interactionActive = true;
|
||||
root.previewAt(pointerX);
|
||||
}
|
||||
|
||||
function movePointerInteraction(pointerX: real): void {
|
||||
if (root.interactionActive)
|
||||
root.previewAt(pointerX);
|
||||
}
|
||||
|
||||
function releasePointerInteraction(): void {
|
||||
if (!root.interactionActive)
|
||||
return;
|
||||
root.interactionActive = false;
|
||||
root.committed(root.previewValue);
|
||||
}
|
||||
|
||||
function cancelPointerInteraction(): void {
|
||||
root.interactionActive = false;
|
||||
root.updatePreview(root.value, false);
|
||||
}
|
||||
|
||||
function commitWheel(delta: int): void {
|
||||
if (delta === 0)
|
||||
return;
|
||||
root.commitStep(delta > 0 ? 5 : -5);
|
||||
}
|
||||
|
||||
function commitStep(delta: int): void {
|
||||
if (!root.enabled)
|
||||
return;
|
||||
root.updatePreview(root.previewValue + delta, true);
|
||||
root.committed(root.previewValue);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@ Item {
|
||||
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 {
|
||||
@@ -18,46 +21,50 @@ Item {
|
||||
ControlSectionHeader {
|
||||
width: parent.width
|
||||
label: "Home"
|
||||
action: HomeAssistant.entities.length > 0
|
||||
? HomeAssistant.entities.length + " accessories " + (root.expanded ? "⌃" : "›")
|
||||
: "Retry"
|
||||
action: root.headerAction()
|
||||
actionEnabled: HomeAssistant.phase !== "loading"
|
||||
onActionTriggered: {
|
||||
if (HomeAssistant.entities.length > 0)
|
||||
if (root.hasSelection) {
|
||||
root.toggleExpanded();
|
||||
else
|
||||
} else if (root.showSetup) {
|
||||
root.openHomeSettings();
|
||||
} else {
|
||||
HomeAssistant.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: homeCard
|
||||
id: restingCard
|
||||
width: parent.width
|
||||
height: visible ? homeGrid.implicitHeight + 20 : 0
|
||||
visible: HomeAssistant.entities.length > 0
|
||||
height: visible ? restingGrid.implicitHeight + 20 : 0
|
||||
visible: root.hasSelection && !root.expanded
|
||||
radius: Theme.cardRadius + 2
|
||||
color: Theme.alpha(Theme.accent, 0.045)
|
||||
color: Theme.alpha(Theme.warn, 0.028)
|
||||
border.width: 1
|
||||
border.color: Theme.alpha(Theme.accent, 0.12)
|
||||
border.color: Theme.alpha(Theme.warn, 0.095)
|
||||
|
||||
Grid {
|
||||
id: homeGrid
|
||||
id: restingGrid
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 10
|
||||
columns: 4
|
||||
spacing: 7
|
||||
columns: 2
|
||||
spacing: 8
|
||||
|
||||
Repeater {
|
||||
model: HomeAssistant.visibleEntities
|
||||
|
||||
HomeTile {
|
||||
required property var modelData
|
||||
width: (homeGrid.width - homeGrid.spacing * 3) / 4
|
||||
width: (restingGrid.width - restingGrid.spacing) / 2
|
||||
entity: modelData
|
||||
busy: HomeAssistant.busyEntityId === modelData.id
|
||||
onActivated: HomeAssistant.toggleEntity(modelData.id)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,28 +72,32 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: visible ? 66 : 0
|
||||
visible: HomeAssistant.entities.length === 0
|
||||
height: visible ? 72 : 0
|
||||
visible: !root.hasSelection
|
||||
radius: Theme.cardRadius
|
||||
color: Theme.alpha(Theme.fg, 0.05)
|
||||
color: root.showSetup
|
||||
? Theme.alpha(Theme.accent, 0.055)
|
||||
: Theme.alpha(Theme.fg, 0.045)
|
||||
border.width: 1
|
||||
border.color: Theme.alpha(Theme.fg, 0.045)
|
||||
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: 12
|
||||
anchors.leftMargin: 13
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: HomeAssistant.phase === "loading" ? "\u{F0772}" : "\u{F02DC}"
|
||||
color: HomeAssistant.phase === "loading" ? Theme.accent : Theme.fgDim
|
||||
text: root.showSetup ? "\u{F0335}" : "\u{F02DC}"
|
||||
color: root.showSetup ? Theme.accent : Theme.fgDim
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: 16
|
||||
font.pixelSize: 17
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.left: stateIcon.right
|
||||
anchors.leftMargin: 11
|
||||
anchors.right: openHome.left
|
||||
anchors.right: emptyAction.left
|
||||
anchors.rightMargin: 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 3
|
||||
@@ -111,61 +122,66 @@ Item {
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: openHome
|
||||
id: emptyAction
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 48
|
||||
height: 28
|
||||
radius: 9
|
||||
width: root.showSetup ? 68 : 52
|
||||
height: 30
|
||||
radius: 10
|
||||
visible: HomeAssistant.phase !== "loading"
|
||||
color: openMouse.containsMouse
|
||||
color: emptyActionMouse.containsMouse
|
||||
? Theme.alpha(Theme.accent, 0.20)
|
||||
: Theme.alpha(Theme.accent, 0.11)
|
||||
: Theme.alpha(Theme.accent, 0.105)
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Open"
|
||||
text: root.showSetup ? "Manage" : "Open"
|
||||
color: Theme.accent
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
MouseArea {
|
||||
id: openMouse
|
||||
id: emptyActionMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: HomeAssistant.open()
|
||||
onClicked: {
|
||||
if (root.showSetup)
|
||||
root.openHomeSettings();
|
||||
else
|
||||
HomeAssistant.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: visible ? 30 : 0
|
||||
height: visible ? 34 : 0
|
||||
visible: HomeAssistant.stale
|
||||
radius: 9
|
||||
color: Theme.alpha(Theme.warn, 0.08)
|
||||
radius: 10
|
||||
color: Theme.alpha(Theme.warn, 0.075)
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 10
|
||||
anchors.leftMargin: 11
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Showing the last known state"
|
||||
text: "Last known state · " + root.countSummary()
|
||||
color: Theme.warn
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 10
|
||||
anchors.rightMargin: 11
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Retry"
|
||||
color: Theme.accent
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
font.weight: Font.DemiBold
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -6
|
||||
@@ -177,88 +193,66 @@ Item {
|
||||
|
||||
Section {
|
||||
width: parent.width
|
||||
expanded: root.expanded && HomeAssistant.entities.length > 0
|
||||
expanded: root.expanded && root.hasSelection
|
||||
|
||||
ScrollColumn {
|
||||
width: parent.width
|
||||
maxHeight: 270
|
||||
spacing: 2
|
||||
maxHeight: 324
|
||||
spacing: 8
|
||||
|
||||
Repeater {
|
||||
model: HomeAssistant.entities
|
||||
Grid {
|
||||
id: expandedGrid
|
||||
width: parent.width
|
||||
columns: 2
|
||||
spacing: 8
|
||||
|
||||
Rectangle {
|
||||
id: entityRow
|
||||
required property var modelData
|
||||
width: parent.width
|
||||
height: 44
|
||||
radius: 10
|
||||
color: entityMouse.containsMouse
|
||||
? Theme.alpha(Theme.fg, 0.09)
|
||||
: "transparent"
|
||||
Repeater {
|
||||
model: HomeAssistant.selectedEntities
|
||||
|
||||
Text {
|
||||
id: entityGlyph
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 11
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "\u{F0335}"
|
||||
color: entityRow.modelData.active ? Theme.warn : Theme.fgDim
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: 14
|
||||
}
|
||||
Column {
|
||||
anchors.left: entityGlyph.right
|
||||
anchors.leftMargin: 10
|
||||
anchors.right: entityState.left
|
||||
anchors.rightMargin: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
Text {
|
||||
width: parent.width
|
||||
text: entityRow.modelData.name
|
||||
color: Theme.fg
|
||||
elide: Text.ElideRight
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
}
|
||||
Text {
|
||||
width: parent.width
|
||||
text: !entityRow.modelData.available ? "Unavailable" : entityRow.modelData.state
|
||||
color: Theme.fgMuted
|
||||
elide: Text.ElideRight
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
}
|
||||
Text {
|
||||
id: entityState
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 11
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: HomeAssistant.busyEntityId === entityRow.modelData.id
|
||||
? "Working"
|
||||
: (entityRow.modelData.active ? "On" : "Off")
|
||||
color: entityRow.modelData.active ? Theme.warn : Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
MouseArea {
|
||||
id: entityMouse
|
||||
anchors.fill: parent
|
||||
enabled: entityRow.modelData.available && HomeAssistant.busyEntityId === ""
|
||||
hoverEnabled: true
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: HomeAssistant.toggleEntity(entityRow.modelData.id)
|
||||
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")
|
||||
@@ -269,12 +263,23 @@ Item {
|
||||
}
|
||||
|
||||
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 "Reading configured favourites";
|
||||
return "Finding your selected lights";
|
||||
if (HomeAssistant.lastError === "authentication-required")
|
||||
return "Update the long-lived access token";
|
||||
if (HomeAssistant.lastError === "not-configured")
|
||||
return "Add a URL, token and favourites";
|
||||
return "Check the connection and retry";
|
||||
return "Connect Home Assistant in Settings";
|
||||
return "Check the connection, then retry";
|
||||
}
|
||||
|
||||
function openHomeSettings(): void {
|
||||
ShellState.close();
|
||||
ShellState.openSettings("home-phone");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,76 +6,165 @@ Rectangle {
|
||||
|
||||
required property var entity
|
||||
property bool busy: false
|
||||
signal activated
|
||||
property int pendingBrightness: -1
|
||||
property string actionError: ""
|
||||
|
||||
implicitHeight: 72
|
||||
radius: Theme.cardRadius
|
||||
signal powerRequested
|
||||
signal brightnessRequested(int value)
|
||||
|
||||
readonly property int confirmedBrightness: root.clamp(root.entity.brightnessPct ?? 0)
|
||||
readonly property int displayedBrightness: brightnessSlider.previewValue
|
||||
readonly property bool powerEnabled: root.entity.available && !root.busy
|
||||
readonly property bool dimmerEnabled: root.entity.available
|
||||
&& root.entity.dimmable
|
||||
&& !root.busy
|
||||
|
||||
implicitHeight: 124
|
||||
radius: 14
|
||||
color: {
|
||||
if (root.entity.active)
|
||||
return Theme.alpha(Theme.warn, tileMouse.containsMouse ? 0.20 : 0.14);
|
||||
return Theme.alpha(Theme.fg, tileMouse.containsMouse ? 0.10 : 0.055);
|
||||
return Theme.alpha(Theme.warn, powerArea.containsMouse ? 0.145 : 0.095);
|
||||
return Theme.alpha(Theme.fg, powerArea.containsMouse ? 0.078 : 0.045);
|
||||
}
|
||||
border.width: 1
|
||||
border.color: root.entity.active
|
||||
? Theme.alpha(Theme.warn, 0.20)
|
||||
: Theme.alpha(Theme.fg, 0.045)
|
||||
border.color: {
|
||||
if (root.activeFocus)
|
||||
return Theme.alpha(Theme.accent, 0.72);
|
||||
if (root.entity.active)
|
||||
return Theme.alpha(Theme.warn, 0.17);
|
||||
return Theme.alpha(Theme.fg, 0.065);
|
||||
}
|
||||
activeFocusOnTab: root.powerEnabled
|
||||
|
||||
Behavior on color { ColorAnimation { duration: Theme.durFast } }
|
||||
Behavior on border.color { ColorAnimation { duration: Theme.durFast } }
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 10
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 9
|
||||
text: root.busy ? "\u{F0772}" : root.glyphFor(root.entity.domain)
|
||||
color: root.entity.active ? Theme.warn : Theme.fgDim
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: 15
|
||||
Keys.onSpacePressed: {
|
||||
if (root.powerEnabled)
|
||||
root.powerRequested();
|
||||
}
|
||||
Keys.onReturnPressed: {
|
||||
if (root.powerEnabled)
|
||||
root.powerRequested();
|
||||
}
|
||||
|
||||
Column {
|
||||
Rectangle {
|
||||
id: bulb
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 10
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 8
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 8
|
||||
spacing: 1
|
||||
anchors.leftMargin: 13
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 13
|
||||
width: 30
|
||||
height: 30
|
||||
radius: 10
|
||||
color: root.entity.active
|
||||
? Theme.alpha(Theme.warn, 0.16)
|
||||
: Theme.alpha(Theme.fg, 0.065)
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: root.entity.name
|
||||
color: Theme.fg
|
||||
elide: Text.ElideRight
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
Text {
|
||||
width: parent.width
|
||||
text: !root.entity.available ? "Unavailable" : (root.entity.active ? "On" : "Off")
|
||||
color: root.entity.active ? Theme.warn : Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: 9
|
||||
anchors.centerIn: parent
|
||||
text: root.busy ? "\u{F0772}" : "\u{F0335}"
|
||||
color: root.entity.active ? Theme.warn : Theme.fgDim
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: 15
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 13
|
||||
anchors.verticalCenter: bulb.verticalCenter
|
||||
text: root.entity.dimmable ? root.displayedBrightness + "%" : "—"
|
||||
color: root.entity.active || root.pendingBrightness >= 0
|
||||
? Theme.warn
|
||||
: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.DemiBold
|
||||
font.features: Theme.tabularFigures
|
||||
}
|
||||
|
||||
Text {
|
||||
id: aliasLabel
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 13
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 13
|
||||
anchors.top: bulb.bottom
|
||||
anchors.topMargin: 8
|
||||
text: root.entity.name
|
||||
color: Theme.fg
|
||||
elide: Text.ElideRight
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.left: aliasLabel.left
|
||||
anchors.right: aliasLabel.right
|
||||
anchors.top: aliasLabel.bottom
|
||||
anchors.topMargin: 2
|
||||
text: root.secondaryText()
|
||||
color: root.actionError !== ""
|
||||
? Theme.warn
|
||||
: (root.entity.active ? Theme.warn : Theme.fgMuted)
|
||||
elide: Text.ElideRight
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: tileMouse
|
||||
anchors.fill: parent
|
||||
enabled: root.entity.available && !root.busy
|
||||
id: powerArea
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: brightnessSlider.top
|
||||
anchors.bottomMargin: 1
|
||||
enabled: root.powerEnabled
|
||||
hoverEnabled: true
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: root.activated()
|
||||
onPressed: root.forceActiveFocus()
|
||||
onClicked: root.powerRequested()
|
||||
}
|
||||
|
||||
function glyphFor(domain: string): string {
|
||||
if (domain === "light")
|
||||
return "\u{F0335}";
|
||||
if (domain === "switch")
|
||||
return "\u{F0521}";
|
||||
if (domain === "scene")
|
||||
return "\u{F0FCE}";
|
||||
return "\u{F02DC}";
|
||||
HomeBrightnessSlider {
|
||||
id: brightnessSlider
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 13
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 13
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 5
|
||||
value: root.pendingBrightness >= 0
|
||||
? root.pendingBrightness
|
||||
: root.confirmedBrightness
|
||||
enabled: root.dimmerEnabled
|
||||
accessibleName: root.entity.name + " brightness"
|
||||
onCommitted: value => root.brightnessRequested(value)
|
||||
}
|
||||
|
||||
function clamp(candidate: real): int {
|
||||
return Math.max(0, Math.min(100, Math.round(candidate)));
|
||||
}
|
||||
|
||||
function secondaryText(): string {
|
||||
if (root.actionError !== "")
|
||||
return root.errorText(root.actionError);
|
||||
if (!root.entity.available)
|
||||
return "Unavailable";
|
||||
if (root.busy && root.pendingBrightness >= 0)
|
||||
return "Setting " + root.pendingBrightness + "%";
|
||||
if (root.busy)
|
||||
return "Updating";
|
||||
return root.entity.active ? "On" : "Off";
|
||||
}
|
||||
|
||||
function errorText(code: string): string {
|
||||
if (code === "authentication-required")
|
||||
return "Authentication required";
|
||||
if (code === "entity-not-discovered")
|
||||
return "Light is unavailable";
|
||||
return "Couldn’t update light";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import QtQuick
|
||||
import qs.services
|
||||
|
||||
// Nonvisual owner of Phone Controls' stable action model and enablement rules.
|
||||
// Keeping this logic free of delegates and icons makes it safe to exercise in
|
||||
// disposable diagnostic engines.
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
readonly property var actionModels: [
|
||||
{ id: "share", glyph: "\u{F0142}", label: "Send file" },
|
||||
{ id: "clipboard", glyph: "\u{F014C}", label: "Clipboard" },
|
||||
{ id: "ring", glyph: "\u{F009A}", label: "Ring" },
|
||||
{ id: "messages", glyph: "\u{F0365}", label: "Messages" }
|
||||
]
|
||||
|
||||
function actionEnabled(action: string): bool {
|
||||
if (action === "messages")
|
||||
return SystemSettings.bluebubblesAvailable;
|
||||
return KdeConnect.phoneReachable
|
||||
&& !KdeConnect.transferActive
|
||||
&& KdeConnect.supports(action);
|
||||
}
|
||||
}
|
||||
@@ -10,11 +10,11 @@ Item {
|
||||
signal toggleExpanded
|
||||
|
||||
readonly property var phone: KdeConnect.preferredPhone
|
||||
readonly property var actionModels: [
|
||||
{ id: "share", glyph: "\u{F0142}", label: "Send file" },
|
||||
{ id: "clipboard", glyph: "\u{F014C}", label: "Clipboard" },
|
||||
{ id: "ring", glyph: "\u{F009A}", label: "Ring" }
|
||||
].filter(item => KdeConnect.supports(item.id))
|
||||
readonly property var actionModels: phoneActions.actionModels
|
||||
|
||||
PhoneActions {
|
||||
id: phoneActions
|
||||
}
|
||||
|
||||
implicitHeight: content.implicitHeight
|
||||
|
||||
@@ -120,9 +120,8 @@ Item {
|
||||
Grid {
|
||||
id: actionsGrid
|
||||
width: parent.width
|
||||
columns: Math.max(1, root.actionModels.length)
|
||||
columns: 4
|
||||
spacing: 7
|
||||
visible: root.actionModels.length > 0
|
||||
|
||||
Repeater {
|
||||
model: root.actionModels
|
||||
@@ -136,8 +135,20 @@ Item {
|
||||
color: actionMouse.containsMouse && actionMouse.enabled
|
||||
? Theme.alpha(Theme.accent, 0.15)
|
||||
: Theme.alpha(Theme.accent, 0.075)
|
||||
border.width: actionMouse.activeFocus ? 2 : 0
|
||||
border.color: Theme.accent
|
||||
opacity: actionMouse.enabled ? 1 : 0.48
|
||||
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: actionButton.modelData.label
|
||||
Accessible.description: root.actionAccessibleDescription(actionButton.modelData.id)
|
||||
Accessible.focusable: actionMouse.enabled
|
||||
Accessible.focused: actionMouse.activeFocus
|
||||
Accessible.onPressAction: {
|
||||
if (actionMouse.enabled)
|
||||
root.invoke(actionButton.modelData.id);
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
@@ -160,10 +171,13 @@ Item {
|
||||
MouseArea {
|
||||
id: actionMouse
|
||||
anchors.fill: parent
|
||||
enabled: KdeConnect.phoneReachable && !KdeConnect.transferActive
|
||||
enabled: root.actionEnabled(actionButton.modelData.id)
|
||||
hoverEnabled: true
|
||||
activeFocusOnTab: enabled
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: root.invoke(actionButton.modelData.id)
|
||||
Keys.onReturnPressed: root.invoke(actionButton.modelData.id)
|
||||
Keys.onSpacePressed: root.invoke(actionButton.modelData.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,7 +186,17 @@ Item {
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: root.phone && !KdeConnect.phoneReachable && root.actionModels.length > 0
|
||||
text: "Actions become available when the iPhone reconnects"
|
||||
text: "KDE Connect actions are unavailable until the iPhone reconnects"
|
||||
color: Theme.fgMuted
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: !SystemSettings.bluebubblesAvailable
|
||||
text: "BlueBubbles is not installed"
|
||||
color: Theme.fgMuted
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.family: Theme.fontFamily
|
||||
@@ -254,6 +278,20 @@ Item {
|
||||
KdeConnect.sendClipboard();
|
||||
else if (action === "ring")
|
||||
KdeConnect.ring();
|
||||
else if (action === "messages") {
|
||||
if (SystemSettings.bluebubblesAvailable && SystemSettings.openApplication("bluebubbles"))
|
||||
ShellState.close();
|
||||
}
|
||||
}
|
||||
|
||||
function actionEnabled(action: string): bool {
|
||||
return phoneActions.actionEnabled(action);
|
||||
}
|
||||
|
||||
function actionAccessibleDescription(action: string): string {
|
||||
if (action === "messages")
|
||||
return SystemSettings.bluebubblesAvailable ? "Opens BlueBubbles" : "BlueBubbles is not installed";
|
||||
return root.actionEnabled(action) ? "Available through KDE Connect" : "Unavailable through KDE Connect";
|
||||
}
|
||||
|
||||
function localPath(selectedUrl: url): string {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
module qs.modules.quicksettings
|
||||
AudioDeviceList 1.0 AudioDeviceList.qml
|
||||
AudioSlider 1.0 AudioSlider.qml
|
||||
BluetoothList 1.0 BluetoothList.qml
|
||||
BrightnessControl 1.0 BrightnessControl.qml
|
||||
ControlSectionHeader 1.0 ControlSectionHeader.qml
|
||||
HomeBrightnessSlider 1.0 HomeBrightnessSlider.qml
|
||||
HomeControls 1.0 HomeControls.qml
|
||||
HomeTile 1.0 HomeTile.qml
|
||||
IconButton 1.0 IconButton.qml
|
||||
PhoneActions 1.0 PhoneActions.qml
|
||||
PhoneControls 1.0 PhoneControls.qml
|
||||
QuickSettings 1.0 QuickSettings.qml
|
||||
QuickSettingsPanel 1.0 QuickSettingsPanel.qml
|
||||
RecentExchange 1.0 RecentExchange.qml
|
||||
RowButton 1.0 RowButton.qml
|
||||
ScrollColumn 1.0 ScrollColumn.qml
|
||||
Section 1.0 Section.qml
|
||||
WifiList 1.0 WifiList.qml
|
||||
@@ -0,0 +1,75 @@
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var entity
|
||||
signal addRequested(string id)
|
||||
|
||||
width: parent ? parent.width : 620
|
||||
implicitHeight: 62
|
||||
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.right: stateCopy.left
|
||||
anchors.rightMargin: 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 3
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: root.entity.sourceName
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: root.entity.id
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
elide: Text.ElideMiddle
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: stateCopy
|
||||
anchors.right: addButton.left
|
||||
anchors.rightMargin: 14
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 92
|
||||
text: root.entity.available === false ? "Unavailable" : String(root.entity.state || "Unknown")
|
||||
color: root.entity.available === false ? Theme.fgMuted : Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
horizontalAlignment: Text.AlignRight
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
id: addButton
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Add"
|
||||
tone: "accent"
|
||||
activeFocusOnTab: true
|
||||
border.width: activeFocus ? 2 : 0
|
||||
border.color: activeFocus ? Theme.fg : Theme.alpha(Theme.fg, 0)
|
||||
onClicked: root.addRequested(root.entity.id)
|
||||
Keys.onReturnPressed: root.addRequested(root.entity.id)
|
||||
Keys.onSpacePressed: root.addRequested(root.entity.id)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: 1
|
||||
color: Theme.alpha(Theme.fg, 0.06)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
required property var favorite
|
||||
required property string sourceName
|
||||
required property int index
|
||||
required property bool featured
|
||||
|
||||
signal aliasCommitted(string id, string alias)
|
||||
signal removeRequested(string id)
|
||||
signal moveRequested(string id, int targetIndex)
|
||||
|
||||
readonly property bool dragging: dragHandler.active
|
||||
|
||||
implicitHeight: 108
|
||||
radius: Theme.cardRadius
|
||||
color: root.dragging
|
||||
? Theme.mix(Theme.bgDark, Theme.accent, 0.09)
|
||||
: Theme.alpha(Theme.bgDark, 0.7)
|
||||
border.width: root.dragging ? 2 : 1
|
||||
border.color: root.dragging
|
||||
? Theme.alpha(Theme.accent, 0.82)
|
||||
: Theme.alpha(Theme.fg, 0.07)
|
||||
z: root.dragging ? 10 : 0
|
||||
|
||||
transform: Translate {
|
||||
x: root.dragging ? dragHandler.translation.x : 0
|
||||
y: root.dragging ? dragHandler.translation.y : 0
|
||||
}
|
||||
|
||||
PrismEdge {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
inset: root.radius
|
||||
opacity: root.dragging ? 0.82 : 0.2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: dragHandle
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 11
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 30
|
||||
height: 42
|
||||
radius: 9
|
||||
activeFocusOnTab: true
|
||||
color: root.dragging || activeFocus
|
||||
? Theme.alpha(Theme.accent, 0.14)
|
||||
: (handleMouse.containsMouse ? Theme.alpha(Theme.fg, 0.09) : Theme.alpha(Theme.fg, 0.045))
|
||||
border.width: activeFocus ? 2 : 1
|
||||
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.06)
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "⠿"
|
||||
color: root.dragging ? Theme.accent : Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: 16
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: handleMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: Qt.SizeAllCursor
|
||||
}
|
||||
|
||||
DragHandler {
|
||||
id: dragHandler
|
||||
target: null
|
||||
onActiveChanged: {
|
||||
if (!active)
|
||||
root.commitDrag();
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Left || event.key === Qt.Key_Up) {
|
||||
root.moveRequested(root.favorite.id, Math.max(0, root.index - 1));
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Right || event.key === Qt.Key_Down) {
|
||||
const grid = root.GridView.view;
|
||||
const lastIndex = grid ? grid.count - 1 : root.index;
|
||||
root.moveRequested(root.favorite.id, Math.min(lastIndex, root.index + 1));
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: aliasFrame
|
||||
anchors.left: dragHandle.right
|
||||
anchors.leftMargin: 10
|
||||
anchors.right: removeButton.left
|
||||
anchors.rightMargin: 12
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 12
|
||||
height: 34
|
||||
radius: 8
|
||||
color: Theme.alpha(Theme.fg, aliasInput.activeFocus ? 0.075 : 0.045)
|
||||
border.width: aliasInput.activeFocus ? 2 : 1
|
||||
border.color: aliasInput.activeFocus
|
||||
? Theme.alpha(Theme.accent, 0.78)
|
||||
: Theme.alpha(Theme.fg, 0.065)
|
||||
|
||||
TextInput {
|
||||
id: aliasInput
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
activeFocusOnTab: true
|
||||
text: String(root.favorite.alias || "")
|
||||
color: Theme.fg
|
||||
selectionColor: Theme.accent
|
||||
selectedTextColor: Theme.bgDark
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
font.weight: Font.Medium
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
selectByMouse: true
|
||||
clip: true
|
||||
onEditingFinished: root.aliasCommitted(root.favorite.id, text)
|
||||
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
visible: aliasInput.text === "" && !aliasInput.activeFocus
|
||||
text: root.sourceName
|
||||
color: Theme.fgDim
|
||||
font: aliasInput.font
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.left: aliasFrame.left
|
||||
anchors.right: removeButton.left
|
||||
anchors.rightMargin: 12
|
||||
anchors.top: aliasFrame.bottom
|
||||
anchors.topMargin: 7
|
||||
text: root.sourceName
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: aliasFrame.left
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 10
|
||||
width: badgeCopy.implicitWidth + 14
|
||||
height: 21
|
||||
radius: Theme.pillRadius
|
||||
visible: root.featured
|
||||
color: Theme.alpha(Theme.accent, 0.1)
|
||||
border.width: 1
|
||||
border.color: Theme.alpha(Theme.accent, 0.2)
|
||||
|
||||
Text {
|
||||
id: badgeCopy
|
||||
anchors.centerIn: parent
|
||||
text: "Control Center"
|
||||
color: Theme.accent
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: 9
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
id: removeButton
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 11
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Remove"
|
||||
activeFocusOnTab: true
|
||||
border.width: activeFocus ? 2 : 1
|
||||
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
||||
onClicked: root.removeRequested(root.favorite.id)
|
||||
Keys.onReturnPressed: root.removeRequested(root.favorite.id)
|
||||
Keys.onSpacePressed: root.removeRequested(root.favorite.id)
|
||||
}
|
||||
|
||||
function commitDrag(): void {
|
||||
const grid = root.GridView.view;
|
||||
if (!grid || grid.count <= 0)
|
||||
return;
|
||||
const centerX = root.x + dragHandler.translation.x + root.width / 2;
|
||||
const centerY = root.y + dragHandler.translation.y + root.height / 2;
|
||||
const modelCount = grid.count;
|
||||
const column = Math.max(0, Math.min(1, Math.floor(centerX / grid.cellWidth)));
|
||||
const row = Math.max(0, Math.floor(centerY / grid.cellHeight));
|
||||
root.moveRequested(root.favorite.id, Math.min(modelCount - 1, row * 2 + column));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
objectName: "home-phone-page"
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
Flickable {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
contentWidth: width
|
||||
contentHeight: content.implicitHeight + 64
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
Column {
|
||||
id: content
|
||||
width: parent.width - 68
|
||||
x: 34
|
||||
y: 30
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: "Home & Phone"
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: 27
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Choose what appears in Control Center and keep phone continuity close at hand."
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
bottomPadding: 6
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,11 @@ Rectangle {
|
||||
id: root
|
||||
|
||||
property var hostWindow: null
|
||||
readonly property var homePhoneDiagnostics: pageLoader.status === Loader.Ready
|
||||
&& pageLoader.item
|
||||
&& pageLoader.item.objectName === "home-phone-page"
|
||||
? pageLoader.item.pageDiagnostics
|
||||
: ({})
|
||||
|
||||
color: Theme.bg
|
||||
radius: 18
|
||||
@@ -88,6 +93,7 @@ Rectangle {
|
||||
case "appearance": return appearancePage;
|
||||
case "displays": return displaysPage;
|
||||
case "connectivity": return connectivityPage;
|
||||
case "home-phone": return homePhonePage;
|
||||
case "desktop": return desktopPage;
|
||||
case "sound": return soundPage;
|
||||
case "notifications": return notificationsPage;
|
||||
@@ -133,6 +139,7 @@ Rectangle {
|
||||
Component { id: appearancePage; AppearancePage {} }
|
||||
Component { id: displaysPage; DisplaysPage {} }
|
||||
Component { id: connectivityPage; ConnectivityPage {} }
|
||||
Component { id: homePhonePage; HomePhonePage {} }
|
||||
Component { id: desktopPage; DesktopPage {} }
|
||||
Component { id: soundPage; SoundPage {} }
|
||||
Component { id: notificationsPage; NotificationsPage {} }
|
||||
|
||||
@@ -13,6 +13,7 @@ Rectangle {
|
||||
{ page: "appearance", label: "Appearance", icon: "\u{F0E0D}" },
|
||||
{ page: "displays", label: "Displays", icon: "\u{F0379}" },
|
||||
{ page: "connectivity", label: "Network & Devices", icon: "\u{F08D4}" },
|
||||
{ page: "home-phone", label: "Home & Phone", icon: "\u{F02DC}" },
|
||||
{ page: "desktop", label: "Desktop & Dock", icon: "\u{F04A4}" },
|
||||
{ page: "sound", label: "Sound", icon: "\u{F057E}" },
|
||||
{ page: "notifications", label: "Notifications & Focus", icon: "\u{F009A}" },
|
||||
|
||||
@@ -6,6 +6,8 @@ import qs.services
|
||||
FloatingWindow {
|
||||
id: root
|
||||
|
||||
readonly property var homePhoneDiagnostics: settingsShell.homePhoneDiagnostics
|
||||
|
||||
title: "Panama Settings"
|
||||
visible: ShellState.settingsOpen
|
||||
implicitWidth: 1120
|
||||
@@ -23,6 +25,7 @@ FloatingWindow {
|
||||
}
|
||||
|
||||
SettingsShell {
|
||||
id: settingsShell
|
||||
anchors.fill: parent
|
||||
hostWindow: root
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ module qs.modules.settings
|
||||
AboutPage 1.0 AboutPage.qml
|
||||
AppearancePage 1.0 AppearancePage.qml
|
||||
ConnectivityPage 1.0 ConnectivityPage.qml
|
||||
HomePhonePage 1.0 HomePhonePage.qml
|
||||
HomeFavoriteCard 1.0 HomeFavoriteCard.qml
|
||||
AvailableLightRow 1.0 AvailableLightRow.qml
|
||||
DesktopPage 1.0 DesktopPage.qml
|
||||
DisplaysPage 1.0 DisplaysPage.qml
|
||||
HomePage 1.0 HomePage.qml
|
||||
|
||||
Reference in New Issue
Block a user