Add Control Center integration services
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user