Files
Panama/config/dot/quickshell/services/KdeConnect.qml
T

223 lines
7.1 KiB
QML

pragma Singleton
// Shared KDE Connect state. The Python helper owns all CLI and D-Bus parsing;
// this singleton exposes only normalized devices and user-initiated actions.
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-kdeconnect"
property bool fixtureMode: false
property bool available: false
property var devices: []
property string lastError: ""
property bool transferActive: false
property string transferFileName: ""
property string transferDeviceName: ""
property var recentExchange: null
property string actionKind: ""
property string actionPath: ""
readonly property var preferredPhone: {
const phones = root.devices.filter(device => device.type === "phone" && device.paired);
return phones.find(device => device.reachable) ?? phones[0] ?? null;
}
readonly property bool phoneReachable: root.preferredPhone?.reachable === true
readonly property int pairedCount: root.devices.filter(device => device.paired).length
readonly property var phoneActions: root.preferredPhone?.actions ?? []
function supports(action: string): bool {
return root.phoneActions.indexOf(action) >= 0;
}
function refresh(): void {
if (root.fixtureMode || statusProc.running)
return;
statusProc.running = true;
}
function consumeStatus(text: string): void {
if (root.fixtureMode)
return;
try {
const result = JSON.parse(text);
root.available = result.available === true;
root.devices = Array.isArray(result.devices) ? result.devices : [];
root.lastError = String(result.error ?? "");
} catch (error) {
root.available = false;
root.devices = [];
root.lastError = "invalid-response";
}
}
function sendFile(path: string): void {
if (!root.phoneReachable || !root.supports("share") || path === "" || actionProc.running)
return;
root.actionKind = "share";
root.actionPath = path;
root.transferActive = true;
root.transferFileName = path.split("/").pop();
root.transferDeviceName = root.preferredPhone.name;
actionProc.command = [root.helperPath, "send-file", root.preferredPhone.id, path];
actionProc.running = true;
}
function sendClipboard(): void {
root.runSimpleAction("clipboard", "send-clipboard");
}
function ring(): void {
root.runSimpleAction("ring", "ring");
}
function runSimpleAction(kind: string, command: string): void {
if (!root.phoneReachable || !root.supports(kind) || actionProc.running)
return;
root.actionKind = kind;
root.actionPath = "";
actionProc.command = [root.helperPath, command, root.preferredPhone.id];
actionProc.running = true;
}
function cancelTransfer(): void {
if (actionProc.running && root.actionKind === "share")
actionProc.signal(2);
root.actionKind = "";
root.actionPath = "";
root.transferActive = false;
root.transferFileName = "";
root.transferDeviceName = "";
}
function finishAction(text: string): void {
if (root.actionKind === "")
return;
const kind = root.actionKind;
const localPath = root.actionPath;
const fileName = root.transferFileName;
const deviceName = root.transferDeviceName || root.preferredPhone?.name || "Phone";
let result = null;
try {
result = JSON.parse(text);
} catch (error) {
result = { ok: false, error: "invalid-response" };
}
root.actionKind = "";
root.actionPath = "";
if (kind === "share") {
root.transferActive = false;
root.transferFileName = "";
root.transferDeviceName = "";
}
if (result.ok === true && kind === "share") {
root.recentExchange = {
direction: "sent",
fileName: String(result.fileName || fileName),
deviceName,
timestamp: Date.now()
};
StatusEvents.publish({
key: "phone-transfer",
glyph: "\u{F03F2}",
title: "Sent to " + deviceName,
detail: String(result.fileName || fileName),
tone: "ok",
priority: StatusEvents.importantPriority,
actionId: localPath !== "" ? "open-path" : "",
actionData: localPath
});
} else if (result.ok !== true) {
root.lastError = String(result.error || "action-failed");
StatusEvents.publish({
key: "phone-action",
glyph: "\u{F03F2}",
title: kind === "share" ? "File was not sent" : "Phone action failed",
detail: "Check that the phone is nearby and try again",
tone: "warn",
priority: StatusEvents.importantPriority
});
}
refreshDelay.restart();
}
function applyFixture(name: string): void {
if (["reachable", "offline", "transfer"].indexOf(name) < 0)
return;
root.fixtureMode = true;
root.available = true;
root.lastError = "";
root.recentExchange = null;
root.devices = [{
id: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
name: "Fixture iPhone",
type: "phone",
paired: true,
reachable: name !== "offline",
actions: ["clipboard", "ping", "ring", "share"]
}];
root.transferActive = name === "transfer";
root.transferFileName = name === "transfer" ? "Fixture document.pdf" : "";
root.transferDeviceName = name === "transfer" ? "Fixture iPhone" : "";
root.actionKind = name === "transfer" ? "share" : "";
root.actionPath = "";
}
function clearFixture(): void {
root.fixtureMode = false;
root.available = false;
root.devices = [];
root.lastError = "";
root.transferActive = false;
root.transferFileName = "";
root.transferDeviceName = "";
root.actionKind = "";
root.actionPath = "";
root.recentExchange = null;
root.refresh();
}
Process {
id: statusProc
command: [root.helperPath, "status"]
stdout: StdioCollector {
onStreamFinished: root.consumeStatus(this.text)
}
}
Process {
id: actionProc
stdout: StdioCollector {
onStreamFinished: root.finishAction(this.text)
}
onExited: (code, status) => {
if (root.actionKind !== "")
root.finishAction("");
}
}
Timer {
interval: 30000
repeat: true
running: !root.fixtureMode
triggeredOnStart: true
onTriggered: root.refresh()
}
Timer {
id: refreshDelay
interval: 500
onTriggered: root.refresh()
}
}