Build the Panama Control Center

This commit is contained in:
Gabriel Brown
2026-08-17 13:44:18 -04:00
parent 190e3200e0
commit 5940d6b4bd
12 changed files with 867 additions and 8 deletions
@@ -0,0 +1,273 @@
import QtQuick
import QtQuick.Dialogs
import qs.config
import qs.services
Item {
id: root
property bool expanded: false
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))
implicitHeight: content.implicitHeight
Column {
id: content
width: parent.width
spacing: 6
ControlSectionHeader {
width: parent.width
label: "Phone"
action: root.phone ? (root.expanded ? "Details ⌃" : "Details ") : "Refresh"
onActionTriggered: {
if (root.phone)
root.toggleExpanded();
else
KdeConnect.refresh();
}
}
Rectangle {
width: parent.width
height: phoneContent.implicitHeight + 22
radius: Theme.cardRadius + 2
color: Theme.alpha(Theme.fg, 0.055)
border.width: 1
border.color: Theme.alpha(Theme.fg, 0.05)
Column {
id: phoneContent
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 11
spacing: 9
Item {
width: parent.width
height: 46
Rectangle {
id: phoneSilhouette
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 29
height: 43
radius: 8
color: Theme.alpha(Theme.accent, 0.07)
border.width: 1
border.color: Theme.alpha(Theme.fg, 0.42)
Rectangle {
anchors.top: parent.top
anchors.topMargin: 4
anchors.horizontalCenter: parent.horizontalCenter
width: 10
height: 2
radius: 1
color: Theme.alpha(Theme.fg, 0.35)
}
Rectangle {
anchors.right: parent.right
anchors.rightMargin: -3
anchors.top: parent.top
anchors.topMargin: 10
width: 6
height: 6
radius: 3
visible: KdeConnect.phoneReachable
color: Theme.ok
}
}
Column {
anchors.left: phoneSilhouette.right
anchors.leftMargin: 11
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 3
Text {
width: parent.width
text: root.phone?.name ?? (KdeConnect.available ? "No paired phone" : "KDE Connect unavailable")
color: Theme.fg
elide: Text.ElideRight
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
font.weight: Font.DemiBold
}
Text {
width: parent.width
text: root.phone
? (KdeConnect.phoneReachable ? "● Nearby on Wi-Fi" : "Not nearby")
: (KdeConnect.available ? "Pair a device to continue" : "Open settings to repair the service")
color: KdeConnect.phoneReachable ? Theme.ok : Theme.fgDim
elide: Text.ElideRight
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
}
Grid {
id: actionsGrid
width: parent.width
columns: Math.max(1, root.actionModels.length)
spacing: 7
visible: root.actionModels.length > 0
Repeater {
model: root.actionModels
Rectangle {
id: actionButton
required property var modelData
width: (actionsGrid.width - actionsGrid.spacing * Math.max(0, root.actionModels.length - 1)) / Math.max(1, root.actionModels.length)
height: 48
radius: 11
color: actionMouse.containsMouse && actionMouse.enabled
? Theme.alpha(Theme.accent, 0.15)
: Theme.alpha(Theme.accent, 0.075)
opacity: actionMouse.enabled ? 1 : 0.48
Text {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 7
text: actionButton.modelData.glyph
color: Theme.accent
font.family: Theme.fontMono
font.pixelSize: 13
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: 6
text: actionButton.modelData.label
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: 9
font.weight: Font.Medium
}
MouseArea {
id: actionMouse
anchors.fill: parent
enabled: KdeConnect.phoneReachable && !KdeConnect.transferActive
hoverEnabled: true
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: root.invoke(actionButton.modelData.id)
}
}
}
}
Text {
width: parent.width
visible: root.phone && !KdeConnect.phoneReachable && root.actionModels.length > 0
text: "Actions become available when the iPhone reconnects"
color: Theme.fgMuted
horizontalAlignment: Text.AlignHCenter
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
Text {
width: parent.width
visible: KdeConnect.lastError !== ""
text: root.errorText(KdeConnect.lastError)
color: Theme.warn
horizontalAlignment: Text.AlignHCenter
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
Rectangle {
width: parent.width
height: 1
visible: KdeConnect.recentExchange !== null
color: Theme.alpha(Theme.fg, 0.07)
}
RecentExchange {
width: parent.width
height: visible ? implicitHeight : 0
visible: KdeConnect.recentExchange !== null
exchange: KdeConnect.recentExchange ?? ({})
}
}
}
Section {
width: parent.width
expanded: root.expanded && root.phone !== null
Column {
width: parent.width
spacing: 2
RowButton {
width: parent.width
icon: "network-wireless-symbolic"
label: "Connection"
sublabel: KdeConnect.phoneReachable ? "Nearby on Wi-Fi" : "Waiting for the phone"
selected: KdeConnect.phoneReachable
onClicked: KdeConnect.refresh()
}
RowButton {
width: parent.width
icon: "preferences-system-symbolic"
label: "Device settings"
sublabel: "Pairing, plugins and service health"
onClicked: {
ShellState.close();
ShellState.openSettings("connectivity");
}
}
}
}
}
FileDialog {
id: fileDialog
title: root.phone ? "Send a file to " + root.phone.name : "Send a file"
fileMode: FileDialog.OpenFile
nameFilters: ["All files (*)"]
onAccepted: {
const path = root.localPath(selectedFile);
if (path !== "")
KdeConnect.sendFile(path);
}
}
function invoke(action: string): void {
if (action === "share")
fileDialog.open();
else if (action === "clipboard")
KdeConnect.sendClipboard();
else if (action === "ring")
KdeConnect.ring();
}
function localPath(selectedUrl: url): string {
const value = String(selectedUrl);
if (!value.startsWith("file://"))
return "";
return decodeURIComponent(value.slice(7));
}
function errorText(code: string): string {
if (code === "device-offline")
return "The iPhone is not nearby";
if (code === "unsupported-action")
return "That action is not available on this iPhone";
return "The phone action did not finish";
}
}