223 lines
7.1 KiB
QML
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()
|
|
}
|
|
}
|