Files
Dotfiles/noctalia/plugins/kde-connect/Services/KDEConnect.qml
T
2026-04-19 17:07:18 +02:00

457 lines
14 KiB
QML

pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
QtObject {
id: root
property list<var> devices: []
property bool daemonAvailable: false
property int pendingDeviceCount: 0
property list<var> pendingDevices: []
property var mainDevice: null
property string mainDeviceId: ""
property string busctlCmd: ""
property bool anyDevicesConnected: false;
onDevicesChanged: {
setMainDevice(root.mainDeviceId)
}
Component.onCompleted: {
checkDaemon();
}
// Check if KDE Connect daemon is available
function checkDaemon(): void {
detectBusctlProc.running = true;
}
// Refresh the list of devices
function refreshDevices(): void {
getDevicesProc.running = true;
}
function setMainDevice(deviceId: string): void {
root.mainDeviceId = deviceId;
updateMainDevice(false);
}
function updateMainDevice(checkReachable) {
let newMain;
if (checkReachable) {
newMain = devices.find((device) => device.id === root.mainDeviceId && device.reachable);
if (newMain === undefined)
newMain = devices.find((device) => device.reachable);
if (newMain === undefined)
newMain = devices.length === 0 ? null : devices[0];
} else {
newMain = devices.find((device) => device.id === root.mainDeviceId);
if (newMain === undefined)
newMain = devices.length === 0 ? null : devices[0];
}
if (root.mainDevice !== newMain) {
root.mainDevice = newMain;
}
anyDevicesConnected = devices.find((device) => device.reachable) !== undefined;
}
function triggerFindMyPhone(deviceId: string): void {
const proc = findMyPhoneComponent.createObject(root, { deviceId: deviceId });
proc.running = true;
}
function browseFiles(deviceId: string): void {
const proc = browseFilesComponent.createObject(root, { deviceId: deviceId });
proc.running = true;
}
// Share a file with a device
function shareFile(deviceId: string, filePath: string): void {
var proc = shareComponent.createObject(root, {
deviceId: deviceId,
filePath: filePath
});
proc.running = true;
}
function requestPairing(deviceId: string): void {
const proc = requestPairingComponent.createObject(root, { deviceId: deviceId });
proc.running = true;
}
function unpairDevice(deviceId: string): void {
const proc = unpairingComponent.createObject(root, { deviceId: deviceId });
proc.running = true;
}
function wakeUpDevice(deviceId: string): void {
const proc = wakeUpDeviceComponent.createObject(root, { deviceId: deviceId });
proc.running = true;
}
function busctlCall(obj, itf, method, params = []) {
let result = [ root.busctlCmd, "--user", "call", "--json=short", "org.kde.kdeconnect", obj, itf, method ];
return result.concat(params);
}
function busctlGet(obj, itf, prop) {
return [ root.busctlCmd, "--user", "get-property", "--json=short", "org.kde.kdeconnect", obj, itf, prop ];
}
function busctlData(text) {
if (text === "")
return "";
try {
let result = JSON.parse(text)?.data;
if (Array.isArray(result) && Array.isArray(result[0]))
return result[0]
else
return result;
} catch (e) {
Logger.e("KDEConnect", "Failed to parse busctl response: ", text)
return null;
}
}
property Process detectBusctlProc: Process {
command: ["which", "busctl"]
stdout: StdioCollector {
onStreamFinished: {
if (root.busctlCmd !== "") {
root.daemonCheckProc.running = true
return
}
let location = text.trim()
if (location !== "") {
root.busctlCmd = location
root.daemonCheckProc.running = true
Logger.i("KDEConnect", "Found busctl command:", location)
}
}
}
}
// Check daemon
property Process daemonCheckProc: Process {
command: [root.busctlCmd, "--user", "status", "org.kde.kdeconnect"]
onExited: (exitCode, exitStatus) => {
root.daemonAvailable = exitCode == 0;
if (root.daemonAvailable) {
forceOnNetworkChange.running = true;
} else {
root.devices = []
root.mainDevice = null
}
}
}
property Process forceOnNetworkChange: Process {
command: busctlCall("/modules/kdeconnect", "org.kde.kdeconnect.daemon", "forceOnNetworkChange")
stdout: StdioCollector {
onStreamFinished: {
getDevicesProc.running = true;
}
}
}
// Get device list
property Process getDevicesProc: Process {
command: busctlCall("/modules/kdeconnect", "org.kde.kdeconnect.daemon", "devices")
stdout: StdioCollector {
onStreamFinished: {
const deviceIds = busctlData(text);
root.pendingDevices = [];
root.pendingDeviceCount = deviceIds.length;
deviceIds.forEach(deviceId => {
const loader = deviceLoaderComponent.createObject(root, { deviceId: deviceId });
loader.start();
});
}
}
}
// Component that loads all info for a single device
property Component deviceLoaderComponent: Component {
QtObject {
id: loader
property string deviceId: ""
property var deviceData: ({
id: deviceId,
name: "",
reachable: false,
paired: false,
pairRequested: false,
verificationKey: "",
charging: false,
battery: -1,
cellularNetworkType: "",
cellularNetworkStrength: -1,
notificationIds: []
})
function start() {
nameProc.running = true
}
property Process nameProc: Process {
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId, "org.kde.kdeconnect.device", "name")
stdout: StdioCollector {
onStreamFinished: {
loader.deviceData.name = busctlData(text);
reachableProc.running = true;
}
}
}
property Process reachableProc: Process {
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId, "org.kde.kdeconnect.device", "isReachable")
stdout: StdioCollector {
onStreamFinished: {
loader.deviceData.reachable = busctlData(text);
pairingRequestedProc.running = true;
}
}
}
property Process pairingRequestedProc: Process {
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId, "org.kde.kdeconnect.device", "isPairRequested")
stdout: StdioCollector {
onStreamFinished: {
loader.deviceData.pairRequested = busctlData(text);
verificationKeyProc.running = true;
}
}
}
property Process verificationKeyProc: Process {
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId, "org.kde.kdeconnect.device", "verificationKey")
stdout: StdioCollector {
onStreamFinished: {
loader.deviceData.verificationKey = busctlData(text);
pairedProc.running = true;
}
}
}
property Process pairedProc: Process {
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId, "org.kde.kdeconnect.device", "isPaired")
stdout: StdioCollector {
onStreamFinished: {
loader.deviceData.paired = busctlData(text);
if (loader.deviceData.paired)
activeNotificationsProc.running = true;
else
finalize()
}
}
}
property Process activeNotificationsProc: Process {
command: busctlCall("/modules/kdeconnect/devices/" + loader.deviceId + "/notifications", "org.kde.kdeconnect.device.notifications", "activeNotifications");
stdout: StdioCollector {
onStreamFinished: {
let ids = busctlData(text);
loader.deviceData.notificationIds = ids
cellularNetworkTypeProc.running = true;
}
}
}
property Process cellularNetworkTypeProc: Process {
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId + "/connectivity_report", "org.kde.kdeconnect.device.connectivity_report", "cellularNetworkType")
stdout: StdioCollector {
onStreamFinished: {
loader.deviceData.cellularNetworkType = busctlData(text);
cellularNetworkStrengthProc.running = true;
}
}
}
property Process cellularNetworkStrengthProc: Process {
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId + "/connectivity_report", "org.kde.kdeconnect.device.connectivity_report", "cellularNetworkStrength")
stdout: StdioCollector {
onStreamFinished: {
const strength = busctlData(text);
loader.deviceData.cellularNetworkStrength = strength;
isChargingProc.running = true;
}
}
}
property Process isChargingProc: Process {
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId + "/battery", "org.kde.kdeconnect.device.battery", "isCharging")
stdout: StdioCollector {
onStreamFinished: {
loader.deviceData.charging = busctlData(text);
batteryProc.running = true;
}
}
}
property Process batteryProc: Process {
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId + "/battery", "org.kde.kdeconnect.device.battery", "charge")
stdout: StdioCollector {
onStreamFinished: {
const charge = busctlData(text);
if (!isNaN(charge)) {
loader.deviceData.battery = charge;
}
finalize();
}
}
}
function finalize() {
root.pendingDevices = root.pendingDevices.concat([loader.deviceData]);
if (root.pendingDevices.length === root.pendingDeviceCount) {
let newDevices = root.pendingDevices
newDevices.sort((a, b) => a.name.localeCompare(b.name))
let prevMainDevice = root.devices.find((device) => device.id === root.mainDeviceId);
let newMainDevice = newDevices.find((device) => device.id === root.mainDeviceId);
let deviceNotReachableAnymore =
prevMainDevice === undefined ||
(
(prevMainDevice?.reachable ?? false) &&
!(newMainDevice?.reachable ?? false)
) ||
(
(prevMainDevice?.paired ?? false) &&
!(newMainDevice?.paired ?? false)
)
root.devices = newDevices
root.pendingDevices = []
updateMainDevice(deviceNotReachableAnymore);
}
loader.destroy();
}
}
}
// FindMyPhone component
property Component findMyPhoneComponent: Component {
Process {
id: proc
property string deviceId: ""
command: busctlCall("/modules/kdeconnect/devices/" + deviceId + "/findmyphone", "org.kde.kdeconnect.device.findmyphone", "ring")
stdout: StdioCollector {
onStreamFinished: proc.destroy()
}
}
}
// SFTP Browse component
property Component browseFilesComponent: Component {
Process {
id: mountProc
property string deviceId: ""
command: busctlCall("/modules/kdeconnect/devices/" + deviceId + "/sftp", "org.kde.kdeconnect.device.sftp", "mountAndWait")
stdout: StdioCollector {
onStreamFinished: rootDirProc.running = true
}
property Process rootDirProc: Process {
command: busctlCall("/modules/kdeconnect/devices/" + mountProc.deviceId + "/sftp", "org.kde.kdeconnect.device.sftp", "getDirectories")
stdout: StdioCollector {
onStreamFinished: {
const dirs = busctlData(text);
const path = Object.keys(dirs[0])[0];
if (!Qt.openUrlExternally("file://" + path)) {
Logger.e("KDEConnect", "Failed to open file manager for path:", path);
}
mountProc.destroy();
}
}
}
}
}
// Request Pairing Component
property Component requestPairingComponent: Component {
Process {
id: proc
property string deviceId: ""
command: busctlCall("/modules/kdeconnect/devices/" + deviceId, "org.kde.kdeconnect.device", "requestPairing")
stdout: StdioCollector {
onStreamFinished: proc.destroy()
}
}
}
// Unpairing Component
property Component unpairingComponent: Component {
Process {
id: proc
property string deviceId: ""
command: busctlCall("/modules/kdeconnect/devices/" + deviceId, "org.kde.kdeconnect.device", "unpair")
stdout: StdioCollector {
onStreamFinished: {
KDEConnect.refreshDevices()
proc.destroy()
}
}
}
}
// Wake up Device Component
property Component wakeUpDeviceComponent: Component {
Process {
id: proc
property string deviceId: ""
command: busctlCall("/modules/kdeconnect/devices/" + deviceId + "/remotecontrol", "org.kde.kdeconnect.device.remotecontrol", "sendCommand", [ "a{sv}", "1", "singleclick", "b", "true" ])
stdout: StdioCollector {
onStreamFinished: {
KDEConnect.refreshDevices()
proc.destroy()
}
}
}
}
// Share file component
property Component shareComponent: Component {
Process {
id: proc
property string deviceId: ""
property string filePath: ""
command: busctlCall("/modules/kdeconnect/devices/" + deviceId + "/share", "org.kde.kdeconnect.device.share", "shareUrl", [ "file://" + filePath ])
stdout: StdioCollector {
onStreamFinished: {
proc.destroy()
}
}
}
}
// Periodic refresh timer
property Timer refreshTimer: Timer {
interval: 5000
running: true
repeat: true
onTriggered: root.checkDaemon()
}
}