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"; } }