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: phoneActions.actionModels PhoneActions { id: phoneActions } 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: 4 spacing: 7 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) 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 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: 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) } } } } Text { width: parent.width visible: root.phone && !KdeConnect.phoneReachable && root.actionModels.length > 0 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 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(); 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 { 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"; } }