1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-02 03:28:28 -04:00

Compare commits

...

6 Commits

Author SHA1 Message Date
purian23 52068fb61b feat(dankdash): add native DankConnect integration 2026-07-28 18:20:23 -04:00
bbedward 64461c534f qs/socket: improve resilience of socket detection and connection
related #2369
port 1.5
2026-07-27 13:40:34 -04:00
bbedward 73da4879f6 frame: consolidate and simplify inset logic 2026-07-27 13:10:47 -04:00
euletheia 8594a414ec fix(BatteryService): fix typo in notification check (#2944) 2026-07-27 11:33:04 -04:00
euletheia df396bfa43 fix(BatteryService): fix unresolved icons for notifications and dismiss alerts when plugged in (#2943)
* feat(BatteryService): dismiss sticky critical battery notifications

If critical notifications do not timeout, dismiss the battery-related
ones automatically when battery is plugged in.

* fix(BatteryService): use material icons for battery alerts

Since the previously specified icons are not resolved, instead reuse
icons from DMS first-party plugin for critical battery alerts.
2026-07-27 11:13:47 -04:00
euletheia 33677150b1 feat(ipc): add 'dismiss' function to notifications ipc (#2942)
Allow to dismiss the last visible notification only instead of resorting
directly to dismissAllPopups.
2026-07-27 11:13:07 -04:00
17 changed files with 786 additions and 81 deletions
+12 -4
View File
@@ -2556,11 +2556,19 @@ Singleton {
return edges;
}
function frameEdgeInsetForSide(screen, side) {
if (!frameEnabled || !screen)
readonly property real frameBarContentGap: frameBarInsetPadding < 0 ? frameThickness : frameBarInsetPadding
readonly property real frameBarContentGapExtra: Math.max(0, frameBarContentGap - frameThickness)
function frameEdgeReservation(screen, edge) {
if (!screen)
return 0;
const edges = getActiveBarEdgesForScreen(screen);
return edges.includes(side) ? frameBarSize : frameThickness;
return getActiveBarEdgesForScreen(screen).includes(edge) ? frameBarSize : frameThickness;
}
function frameEdgeInsetForSide(screen, side) {
if (!frameEnabled)
return 0;
return frameEdgeReservation(screen, side);
}
function setMatugenScheme(scheme) {
+2
View File
@@ -31,6 +31,8 @@ Item {
id: root
readonly property var log: Log.scoped("DMSShell")
readonly property var _sessionsServiceRef: SessionsService
// Keep the optional integration service alive
readonly property var _dankConnectServiceRef: DankConnectService
property var core: null
+9
View File
@@ -64,6 +64,10 @@ DankModal {
NotificationService.dismissAllPopups();
}
function dismissLastNotification() {
NotificationService.dismissLastNotification();
}
modalWidth: Math.min(500, screenWidth - 48)
modalHeight: Math.min(700, screenHeight * 0.85)
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
@@ -190,6 +194,11 @@ DankModal {
return "NOTIFICATION_MODAL_DISMISS_ALL_POPUPS_SUCCESS";
}
function dismiss(): string {
notificationModal.dismissLastNotification();
return "NOTIFICATION_DISMISS_SUCCESS";
}
target: "notifications"
}
@@ -38,11 +38,10 @@ Item {
readonly property real _barInsetPaddingRaw: SettingsData.barInsetPaddingSyncAll ? SettingsData.barInsetPaddingShared : (barConfig?.barInsetPadding ?? -1)
readonly property real _barInsetPaddingAuto: _barIsVertical ? Theme.spacingXS : _edgeBaseMargin
readonly property real _barInsetPadding: _barInsetPaddingRaw < 0 ? _barInsetPaddingAuto : _barInsetPaddingRaw
// Connected-frame Bar Inset Padding: absolute free-end gap the hosted bar spans full-width into
// (auto < 0 = frameThickness so widgets align with the interior cutout, 0 = edge-to-edge). The extra
// beyond frameThickness is what an adjacent bar end adds on top of its corner alignment.
readonly property real _frameInsetResolved: SettingsData.frameBarInsetPadding < 0 ? SettingsData.frameThickness : SettingsData.frameBarInsetPadding
readonly property real _frameInsetExtra: Math.max(0, _frameInsetResolved - SettingsData.frameThickness)
// Hosted bars span their edge fully; frameBarContentGap is the free-end gap measured from the
// screen edge (auto = frameThickness, aligning widgets with the interior cutout).
readonly property real _frameInsetResolved: SettingsData.frameBarContentGap
readonly property real _frameInsetExtra: SettingsData.frameBarContentGapExtra
// Horizontal bars span the full width and own the corners; the perpendicular vertical bar
// tucks in below/above. Where they meet, inset the corner widget so it centres in the
@@ -0,0 +1,209 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Card {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
readonly property var device: DankConnectService.primaryDevice
readonly property var pairingRequest: DankConnectService.pairingRequest
readonly property bool hasBattery: !!device && typeof device.batteryCharge === "number" && device.batteryCharge >= 0
function statusText() {
if (!DankConnectService.connected)
return I18n.tr("Disconnected");
if (!device)
return I18n.tr("No devices");
const connection = device.isReachable ? I18n.tr("Connected") : I18n.tr("Disconnected");
const pairing = device.isPaired ? I18n.tr("Paired") : I18n.tr("Not paired");
return connection + " • " + pairing;
}
function batteryText() {
if (!hasBattery)
return "";
const charge = Math.round(device.batteryCharge) + "%";
return device.batteryCharging ? charge + " • " + I18n.tr("Charging") : charge;
}
function reportError(title, response) {
if (response?.error)
ToastService.showError(title, response.error);
}
Row {
id: contentRow
anchors.fill: parent
spacing: Theme.spacingL
Item {
width: Math.max(180, contentRow.width - actionRow.width - trailingPanel.width - contentRow.spacing * 2)
height: parent.height
Row {
anchors.fill: parent
spacing: Theme.spacingM
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: "smartphone"
size: 36
color: root.device?.isReachable ? Theme.primary : Theme.surfaceTextSecondary
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 36 - parent.spacing
spacing: Theme.spacingXS
StyledText {
width: parent.width
text: root.device?.name || "DankConnect"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
maximumLineCount: 1
}
StyledText {
width: parent.width
text: root.statusText()
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
elide: Text.ElideRight
maximumLineCount: 1
}
StyledText {
width: parent.width
visible: root.hasBattery
text: root.batteryText()
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
elide: Text.ElideRight
maximumLineCount: 1
}
}
}
}
Row {
id: actionRow
anchors.verticalCenter: parent.verticalCenter
width: implicitWidth
spacing: Theme.spacingXS
DankActionButton {
buttonSize: 34
iconSize: 17
iconName: "ring_volume"
iconColor: Theme.primary
tooltipText: I18n.tr("Ring")
enabled: DankConnectService.canRing(root.device)
onClicked: DankConnectService.ringDevice(root.device.id, response => root.reportError(I18n.tr("Ring"), response))
}
DankActionButton {
buttonSize: 34
iconSize: 17
iconName: "network_ping"
iconColor: Theme.primary
tooltipText: I18n.tr("Ping")
enabled: DankConnectService.canPing(root.device)
onClicked: DankConnectService.pingDevice(root.device.id, response => root.reportError(I18n.tr("Ping"), response))
}
DankActionButton {
buttonSize: 34
iconSize: 17
iconName: "content_paste_go"
iconColor: Theme.primary
tooltipText: I18n.tr("Send Clipboard")
enabled: DankConnectService.canSendClipboard(root.device)
onClicked: DankConnectService.sendClipboard(root.device.id, response => root.reportError(I18n.tr("Send Clipboard"), response))
}
DankActionButton {
buttonSize: 34
iconSize: 17
iconName: "open_in_new"
iconColor: Theme.primary
tooltipText: I18n.tr("Open App")
onClicked: DankConnectService.openApp()
}
}
Item {
id: trailingPanel
width: root.pairingRequest ? 270 : 52
height: parent.height
DankIcon {
anchors.centerIn: parent
visible: !root.pairingRequest && root.hasBattery
name: root.hasBattery ? Theme.getBatteryIcon(root.device.batteryCharge, root.device.batteryCharging, true) : "battery_std"
size: 28
color: Theme.primary
}
Column {
anchors.centerIn: parent
width: parent.width
spacing: Theme.spacingXS
visible: root.pairingRequest !== null
StyledText {
width: parent.width
text: I18n.tr("Pairing request from %1").arg(root.pairingRequest?.name || I18n.tr("Device"))
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
maximumLineCount: 1
}
StyledText {
width: parent.width
text: root.pairingRequest?.verificationKey || I18n.tr("Unknown")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.primary
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
maximumLineCount: 1
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingS
DankButton {
text: I18n.tr("Reject")
buttonHeight: 30
horizontalPadding: Theme.spacingM
onClicked: DankConnectService.rejectPairing(root.pairingRequest.deviceId, response => root.reportError(I18n.tr("Failed to reject pairing"), response))
}
DankButton {
text: I18n.tr("Accept")
buttonHeight: 30
horizontalPadding: Theme.spacingM
backgroundColor: Theme.primary
textColor: Theme.primaryText
onClicked: DankConnectService.acceptPairing(root.pairingRequest.deviceId, response => root.reportError(I18n.tr("Failed to accept pairing"), response))
}
}
}
}
}
}
+11 -1
View File
@@ -9,8 +9,11 @@ Item {
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
readonly property int overviewGridHeight: 410
readonly property int dankConnectCardHeight: 124
implicitWidth: SettingsData.showWeekNumber ? 736 : 700
implicitHeight: 410
implicitHeight: overviewGridHeight + Theme.spacingM + dankConnectCardHeight
signal switchToWeatherTab
signal switchToMediaTab
@@ -89,5 +92,12 @@ Item {
onClicked: root.switchToMediaTab()
}
DankConnectOverviewCard {
x: 0
y: root.overviewGridHeight + Theme.spacingM
width: parent.width
height: root.dankConnectCardHeight
}
}
}
+1 -2
View File
@@ -21,11 +21,10 @@ QtObject {
readonly property bool frameExclusionActive: CompositorService.frameWindowVisibleForScreen(screen)
readonly property bool usesConnectedFrameChrome: CompositorService.usesConnectedFrameChromeForScreen(screen)
readonly property bool connectedBarActiveOnEdge: usesConnectedFrameChrome && !!screen && SettingsData.getActiveBarEdgesForScreen(screen).includes(edge)
readonly property real connectedJoinInset: {
if (usesConnectedFrameChrome)
return connectedBarActiveOnEdge ? SettingsData.frameBarSize : SettingsData.frameThickness;
return SettingsData.frameEdgeReservation(screen, edge);
if (frameExclusionActive)
return SettingsData.frameEdgeInsetForSide(screen, edge);
return 0;
+1 -1
View File
@@ -34,7 +34,7 @@ Scope {
}
function exclusionSizeForEdge(edge) {
return root.barEdges.includes(edge) ? SettingsData.frameBarSize : SettingsData.frameThickness;
return SettingsData.frameEdgeReservation(root.screen, edge);
}
Loader {
+16 -4
View File
@@ -262,10 +262,22 @@ PanelWindow {
return false;
}
readonly property int cutoutTopInset: win._regionInt(barEdges.includes("top") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutBottomInset: win._regionInt(barEdges.includes("bottom") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutLeftInset: win._regionInt(barEdges.includes("left") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutRightInset: win._regionInt(barEdges.includes("right") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutTopInset: {
SettingsData.barConfigs;
return win._regionInt(SettingsData.frameEdgeReservation(win.targetScreen, "top"));
}
readonly property int cutoutBottomInset: {
SettingsData.barConfigs;
return win._regionInt(SettingsData.frameEdgeReservation(win.targetScreen, "bottom"));
}
readonly property int cutoutLeftInset: {
SettingsData.barConfigs;
return win._regionInt(SettingsData.frameEdgeReservation(win.targetScreen, "left"));
}
readonly property int cutoutRightInset: {
SettingsData.barConfigs;
return win._regionInt(SettingsData.frameEdgeReservation(win.targetScreen, "right"));
}
readonly property int cutoutWidth: Math.max(0, win._windowRegionWidth - win.cutoutLeftInset - win.cutoutRightInset)
readonly property int cutoutHeight: Math.max(0, win._windowRegionHeight - win.cutoutTopInset - win.cutoutBottomInset)
readonly property int cutoutRadius: {
@@ -360,8 +360,7 @@ QtObject {
function _frameEdgeInset(side) {
if (!manager.modelData)
return 0;
const edges = SettingsData.getActiveBarEdgesForScreen(manager.modelData);
const raw = edges.includes(side) ? SettingsData.frameBarSize : SettingsData.frameThickness;
const raw = SettingsData.frameEdgeReservation(manager.modelData, side);
const dpr = CompositorService.getScreenScale(manager.modelData);
return Math.max(0, Math.round(Theme.px(raw, dpr)));
}
+1 -1
View File
@@ -13,7 +13,7 @@ Item {
LayoutMirroring.childrenInherit: true
// Bar Inset Padding: resolve the "auto" sentinel (stored < 0) to the frame thickness for the slider display.
readonly property int frameInsetPaddingDisplay: SettingsData.frameBarInsetPadding < 0 ? Math.round(SettingsData.frameThickness) : SettingsData.frameBarInsetPadding
readonly property int frameInsetPaddingDisplay: Math.round(SettingsData.frameBarContentGap)
DankFlickable {
anchors.fill: parent
+25 -4
View File
@@ -136,7 +136,7 @@ Singleton {
function sendAlert(title, message, isWarning, category, notificationType) {
if (notificationType === 1) {
Quickshell.execDetached(["notify-send", "-u", isWarning ? "critical" : "normal", "-a", "DMS", "-i", isWarning ? "battery-caution" : "battery-charging", title, message]);
Quickshell.execDetached(["notify-send", "-u", isWarning ? "critical" : "normal", "-a", "DMS", "-i", isWarning ? "material:battery_alert" : "material:battery_charging_full", title, message]);
} else {
if (isWarning) {
ToastService.showWarning(title, message, "", category);
@@ -150,7 +150,7 @@ Singleton {
if (isCharging && batteryLevel >= SettingsData.batteryChargeLimit) {
if (!_hasNotifiedChargeLimit && SettingsData.batteryNotifyChargeLimit) {
_hasNotifiedChargeLimit = true;
sendAlert(I18n.tr("Charge Limit Reached"), I18n.tr("Battery has charged to your set limit of %1%").arg(SettingsData.batteryChargeLimit), false, "battery-charge-limit", SettingsData.batteryChargeLimitNotificationType);
sendAlert(I18n.tr("Charge Limit Reached"), I18n.tr("Battery has charged to your set limit of %1%").arg(SettingsData.batteryChargeLimit), false, "material:battery_profile", SettingsData.batteryChargeLimitNotificationType);
}
} else if (!isCharging || batteryLevel < SettingsData.batteryChargeLimit - 2) {
_hasNotifiedChargeLimit = false;
@@ -166,7 +166,7 @@ Singleton {
if (isCriticalBattery) {
if (!_hasNotifiedCriticalBattery && SettingsData.batteryNotifyCritical) {
_hasNotifiedCriticalBattery = true;
sendAlert(I18n.tr("Critical Battery"), I18n.tr("Battery is at %1% - Connect charger immediately!").arg(batteryLevel), true, "battery-critical", SettingsData.batteryCriticalNotificationType);
sendAlert(I18n.tr("Critical Battery"), I18n.tr("Battery is at %1% - Connect charger immediately!").arg(batteryLevel), true, "material:battery_alert", SettingsData.batteryCriticalNotificationType);
}
return;
}
@@ -179,7 +179,7 @@ Singleton {
if (isLowBattery) {
if (!_hasNotifiedLowBattery && SettingsData.batteryNotifyLow) {
_hasNotifiedLowBattery = true;
sendAlert(I18n.tr("Low Battery"), I18n.tr("Battery is at %1% - Consider charging soon").arg(batteryLevel), true, "battery-low", SettingsData.batteryLowNotificationType);
sendAlert(I18n.tr("Low Battery"), I18n.tr("Battery is at %1% - Consider charging soon").arg(batteryLevel), true, "material:battery_0_bar", SettingsData.batteryLowNotificationType);
}
if (SettingsData.batteryAutoPowerSaver && PowerProfileWatcher.available) {
@@ -223,6 +223,27 @@ Singleton {
applyPowerProfile();
if (isPluggedIn) {
const dismissLow = SettingsData.batteryLowNotificationType === 1 && SettingsData.notificationTimeoutNormal === 0;
const dismissCritical = SettingsData.batteryCriticalNotificationType === 1 && SettingsData.notificationTimeoutCritical === 0;
if (dismissLow || dismissCritical) {
const lowSummary = I18n.tr("Low Battery");
const criticalSummary = I18n.tr("Critical Battery");
for (const w of NotificationService.visibleNotifications) {
if (!w || !w.notification)
continue;
const summary = w.notification.summary;
if ((dismissLow && summary === lowSummary) || (dismissCritical && summary === criticalSummary)) {
NotificationService.dismissNotification(w);
}
}
}
}
previousPluggedState = isPluggedIn;
}
+30 -53
View File
@@ -11,7 +11,7 @@ Singleton {
id: root
readonly property var log: Log.scoped("DMSService")
property bool dmsAvailable: false
readonly property bool dmsAvailable: isConnected
property var capabilities: []
property int apiVersion: 0
property string cliVersion: ""
@@ -21,7 +21,7 @@ Singleton {
property var availableThemes: []
property var installedThemes: []
property bool isConnected: false
property bool isConnecting: false
readonly property bool isConnecting: requestSocket.connected && !requestSocket.linkUp
property bool subscribeConnected: false
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
@@ -72,9 +72,10 @@ Singleton {
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "wallpaper", "bluetooth", "bluetooth.pairing", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard", "sysupdate"]
Component.onCompleted: {
if (socketPath && socketPath.length > 0) {
detectUpdateCommand();
}
if (!socketPath || socketPath.length === 0)
return;
detectUpdateCommand();
requestSocket.connected = true;
}
function detectUpdateCommand() {
@@ -82,12 +83,6 @@ Singleton {
checkAurHelper.running = true;
}
function startSocketConnection() {
if (socketPath && socketPath.length > 0) {
testProcess.running = true;
}
}
Process {
id: checkAurHelper
command: ["sh", "-c", "command -v paru || command -v yay"]
@@ -105,7 +100,6 @@ Singleton {
} else {
updateCommand = "dms update";
checkingUpdateCommand = false;
startSocketConnection();
}
}
}
@@ -114,7 +108,6 @@ Singleton {
if (exitCode !== 0) {
updateCommand = "dms update";
checkingUpdateCommand = false;
startSocketConnection();
}
}
}
@@ -135,7 +128,6 @@ Singleton {
updateCommand = "dms update";
}
checkingUpdateCommand = false;
startSocketConnection();
}
}
@@ -143,52 +135,27 @@ Singleton {
if (exitCode !== 0) {
updateCommand = "dms update";
checkingUpdateCommand = false;
startSocketConnection();
}
}
}
Process {
id: testProcess
command: ["test", "-S", root.socketPath]
onExited: exitCode => {
if (exitCode === 0) {
root.dmsAvailable = true;
connectSocket();
} else {
root.dmsAvailable = false;
}
}
}
function connectSocket() {
if (!dmsAvailable || isConnected || isConnecting) {
return;
}
isConnecting = true;
requestSocket.connected = true;
}
DankSocket {
id: requestSocket
path: root.socketPath
connected: false
onConnectionStateChanged: {
if (connected) {
if (linkUp) {
root.isConnected = true;
root.isConnecting = false;
root.connectionStateChanged();
subscribeSocket.connected = true;
} else {
root.isConnected = false;
root.isConnecting = false;
root.apiVersion = 0;
root.capabilities = [];
root.connectionStateChanged();
return;
}
root.isConnected = false;
root.apiVersion = 0;
root.capabilities = [];
root.failPendingRequests();
root.connectionStateChanged();
}
parser: SplitParser {
@@ -219,10 +186,10 @@ Singleton {
connected: false
onConnectionStateChanged: {
root.subscribeConnected = connected;
if (connected) {
sendSubscribeRequest();
}
root.subscribeConnected = linkUp;
if (!linkUp)
return;
sendSubscribeRequest();
}
parser: SplitParser {
@@ -432,10 +399,20 @@ Singleton {
function handleResponse(response) {
const callback = pendingRequests[response.id];
if (!callback)
return;
delete pendingRequests[response.id];
callback(response);
}
if (callback) {
delete pendingRequests[response.id];
callback(response);
function failPendingRequests() {
const pending = pendingRequests;
pendingRequests = {};
clipboardRequestIds = {};
for (const id in pending) {
pending[id]({
"error": "not connected to DMS socket"
});
}
}
+284
View File
@@ -0,0 +1,284 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
// Canonical DankConnect IPC client. Targets daemon apiVersion 1.
Item {
id: root
// dankgo omits zero-valued response IDs, so the subscription handshake
// must use a non-zero ID that can be matched in the response.
readonly property int subscriptionRequestId: 1
property string socketPath: {
const override = Quickshell.env("DANKCONNECT_SOCKET");
if (override && override.length > 0)
return override;
const runtimeDir = Quickshell.env("XDG_RUNTIME_DIR");
return runtimeDir ? runtimeDir + "/dankconnect.sock" : "";
}
readonly property bool connected: requestSocket.linkUp && ready
property bool ready: false
property bool initialized: false
property bool subscribed: false
property int apiVersion: 0
property var capabilities: []
property var subscribedTopics: []
property var devices: []
property var devicesById: ({})
property string lastError: ""
property bool launchPending: false
property int _requestId: 0
property var _pendingRequests: ({})
signal eventReceived(string topic, var data)
function _nextId() {
_requestId = _requestId >= 2147483647 ? 1 : _requestId + 1;
return _requestId;
}
function _parse(line) {
if (!line || line.length === 0)
return null;
try {
return JSON.parse(line);
} catch (error) {
return null;
}
}
function _isHello(message) {
return message && message.id === undefined && message.apiVersion !== undefined && Array.isArray(message.capabilities);
}
function _acceptHello(message) {
apiVersion = Number(message.apiVersion) || 0;
capabilities = message.capabilities.slice();
ready = apiVersion === 1;
if (!ready) {
lastError = "Unsupported DankConnect API version " + apiVersion;
return;
}
lastError = "";
launchDelayTimer.stop();
launchPending = false;
refreshDevices();
}
function _flushPending(message) {
const callbacks = _pendingRequests;
_pendingRequests = {};
for (const id in callbacks) {
const callback = callbacks[id];
if (!callback)
continue;
callback({
"id": Number(id),
"error": message
});
}
}
function _applyDevices(snapshots) {
const nextDevices = [];
const nextById = {};
for (const device of (snapshots || [])) {
if (!device?.id)
continue;
nextDevices.push(device);
nextById[device.id] = device;
}
devicesById = nextById;
devices = nextDevices;
initialized = true;
}
function _resolveDevice(deviceOrId) {
if (typeof deviceOrId === "string")
return devicesById[deviceOrId] || null;
return deviceOrId || null;
}
function getDevice(deviceId) {
return devicesById[deviceId] || null;
}
// supportedPlugins is the peer's incoming view: actions this client sends.
function supports(deviceOrId, packetType) {
const device = _resolveDevice(deviceOrId);
return !!device && (device.supportedPlugins || []).includes(packetType);
}
// outgoingCapabilities is the peer's producing view: data this client consumes.
function produces(deviceOrId, packetType) {
const device = _resolveDevice(deviceOrId);
return !!device && (device.outgoingCapabilities || []).includes(packetType);
}
function hasCapability(family) {
return capabilities.includes(family);
}
function request(method, params, callback) {
if (!connected) {
callback?.({
"error": "daemon not connected"
});
return -1;
}
const family = method.split(".")[0];
if (!hasCapability(family)) {
callback?.({
"error": "daemon does not support " + family
});
return -1;
}
const id = _nextId();
const message = {
"id": id,
"method": method,
"params": params || {}
};
if (callback)
_pendingRequests[id] = callback;
requestSocket.send(message);
return id;
}
function refreshDevices(callback) {
return request("device.list", {}, response => {
if (!response.error)
_applyDevices(response.result || []);
callback?.(response);
});
}
function launch() {
if (connected || requestSocket.linkUp || launchPending)
return;
launchPending = true;
launchDelayTimer.restart();
}
function _handleRequestLine(line) {
const message = _parse(line);
if (!message)
return;
if (_isHello(message)) {
_acceptHello(message);
return;
}
if (message.id === undefined)
return;
const callback = _pendingRequests[message.id];
if (!callback)
return;
delete _pendingRequests[message.id];
callback(message);
}
function _handleSubscriptionLine(line) {
const message = _parse(line);
if (!message)
return;
if (_isHello(message)) {
if (Number(message.apiVersion) !== 1) {
lastError = "Unsupported DankConnect subscription API version " + message.apiVersion;
return;
}
subscriptionSocket.send({
"id": root.subscriptionRequestId,
"method": "subscribe",
"params": {}
});
return;
}
if (message.id === root.subscriptionRequestId) {
subscribed = !message.error;
subscribedTopics = message.result?.topics || [];
if (message.error)
lastError = message.error;
return;
}
const topic = message.event || message.topic || "";
if (!topic)
return;
if (topic === "devices")
_applyDevices(message.data || []);
eventReceived(topic, message.data);
}
DankSocket {
id: requestSocket
path: root.socketPath
connected: root.enabled && root.socketPath.length > 0
reconnectBaseMs: 3000
reconnectMaxMs: 3000
onConnectionStateChanged: {
if (linkUp)
return;
root.ready = false;
root.initialized = false;
root.subscribed = false;
root.subscribedTopics = [];
root.apiVersion = 0;
root.capabilities = [];
root._flushPending("daemon disconnected");
}
parser: SplitParser {
onRead: line => root._handleRequestLine(line)
}
}
DankSocket {
id: subscriptionSocket
path: root.socketPath
connected: root.enabled && root.ready
reconnectBaseMs: 3000
reconnectMaxMs: 3000
onConnectionStateChanged: {
root.subscribed = false;
root.subscribedTopics = [];
}
parser: SplitParser {
onRead: line => root._handleSubscriptionLine(line)
}
}
Timer {
id: launchDelayTimer
interval: 1000
repeat: false
onTriggered: {
if (root.connected || requestSocket.linkUp) {
root.launchPending = false;
return;
}
Quickshell.execDetached(["sh", "-c", "if command -v dankconnect >/dev/null 2>&1; then exec dankconnect daemon; elif command -v flatpak >/dev/null 2>&1 && flatpak info com.danklinux.dankconnect >/dev/null 2>&1; then exec flatpak run --command=dankconnect com.danklinux.dankconnect daemon; fi"]);
launchResetTimer.restart();
}
}
Timer {
id: launchResetTimer
interval: 5000
repeat: false
onTriggered: root.launchPending = false
}
}
+172
View File
@@ -0,0 +1,172 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
Singleton {
id: root
readonly property bool connected: backend.connected && backend.subscribed
readonly property bool initialized: backend.initialized
readonly property int apiVersion: backend.apiVersion
readonly property var capabilities: backend.capabilities
readonly property var devices: backend.devices
readonly property var devicesById: backend.devicesById
readonly property string lastError: backend.lastError
readonly property var primaryDevice: _selectPrimaryDevice()
property var pairingRequest: null
property bool _openAppPending: false
Component.onCompleted: backend.launch()
onConnectedChanged: {
if (!connected) {
pairingRequest = null;
return;
}
if (_openAppPending) {
_openAppPending = false;
_launchAttachedApp();
}
}
function _selectPrimaryDevice() {
let selected = null;
let selectedScore = -1;
for (const device of devices) {
const score = (device.isPaired ? 2 : 0) + (device.isReachable ? 1 : 0);
if (score <= selectedScore)
continue;
selected = device;
selectedScore = score;
}
return selected;
}
function _syncPairingRequest() {
for (const device of devices) {
if (!device.isPairRequestedByPeer)
continue;
pairingRequest = {
"deviceId": device.id,
"name": device.name || "",
"verificationKey": device.verificationKey || ""
};
return;
}
if (initialized)
pairingRequest = null;
}
function _handleEvent(topic, data) {
if (topic !== "pairing" || !data?.deviceId)
return;
pairingRequest = {
"deviceId": data.deviceId,
"name": data.name || "",
"verificationKey": data.verificationKey || ""
};
}
function supports(deviceOrId, packetType) {
return backend.supports(deviceOrId, packetType);
}
function canRing(deviceOrId) {
const device = typeof deviceOrId === "string" ? backend.getDevice(deviceOrId) : deviceOrId;
return connected && !!device?.isPaired && !!device?.isReachable && backend.hasCapability("device") && supports(device, "kdeconnect.findmyphone.request");
}
function canPing(deviceOrId) {
const device = typeof deviceOrId === "string" ? backend.getDevice(deviceOrId) : deviceOrId;
return connected && !!device?.isPaired && !!device?.isReachable && backend.hasCapability("ping") && supports(device, "kdeconnect.ping");
}
function canSendClipboard(deviceOrId) {
const device = typeof deviceOrId === "string" ? backend.getDevice(deviceOrId) : deviceOrId;
return connected && !!device?.isPaired && !!device?.isReachable && backend.hasCapability("clipboard") && supports(device, "kdeconnect.clipboard");
}
function ringDevice(deviceId, callback) {
if (!canRing(deviceId)) {
callback?.({
"error": "ring unavailable"
});
return -1;
}
return backend.request("device.ring", {
"deviceId": deviceId
}, callback);
}
function pingDevice(deviceId, callback) {
if (!canPing(deviceId)) {
callback?.({
"error": "ping unavailable"
});
return -1;
}
return backend.request("ping.send", {
"deviceId": deviceId
}, callback);
}
function sendClipboard(deviceId, callback) {
if (!canSendClipboard(deviceId)) {
callback?.({
"error": "clipboard unavailable"
});
return -1;
}
return backend.request("clipboard.send", {
"deviceId": deviceId,
"content": ""
}, callback);
}
function acceptPairing(deviceId, callback) {
return _respondToPairing("pairing.accept", deviceId, callback);
}
function rejectPairing(deviceId, callback) {
return _respondToPairing("pairing.reject", deviceId, callback);
}
function openApp() {
if (!connected) {
_openAppPending = true;
backend.launch();
return;
}
_launchAttachedApp();
}
function _launchAttachedApp() {
Quickshell.execDetached(["sh", "-c", "if command -v dankconnect >/dev/null 2>&1; then exec dankconnect app; elif command -v flatpak >/dev/null 2>&1 && flatpak info com.danklinux.dankconnect >/dev/null 2>&1; then exec flatpak run --command=dankconnect com.danklinux.dankconnect app; fi"]);
}
function _respondToPairing(method, deviceId, callback) {
if (!connected || !backend.hasCapability("pairing") || !deviceId) {
callback?.({
"error": "pairing unavailable"
});
return -1;
}
return backend.request(method, {
"deviceId": deviceId
}, response => {
if (!response.error && pairingRequest?.deviceId === deviceId)
pairingRequest = null;
callback?.(response);
});
}
DankConnectBackend {
id: backend
enabled: true
onDevicesChanged: root._syncPairingRequest()
onEventReceived: (topic, data) => root._handleEvent(topic, data)
}
}
@@ -889,6 +889,12 @@ Singleton {
NotifWrapper {}
}
function dismissLastNotification() {
const w = visibleNotifications[visibleNotifications.length - 1];
if (w)
dismissNotification(w);
}
function dismissAllPopups() {
for (const w of visibleNotifications) {
if (w) {
+2 -4
View File
@@ -584,11 +584,9 @@ Item {
}
function _frameEdgeInset(side) {
if (!root.frameOwnsConnectedChrome || !root.screen)
if (!root.frameOwnsConnectedChrome)
return 0;
const edges = SettingsData.getActiveBarEdgesForScreen(root.screen);
const raw = edges.includes(side) ? SettingsData.frameBarSize : SettingsData.frameThickness;
return Math.max(0, raw);
return Math.max(0, SettingsData.frameEdgeReservation(root.screen, side));
}
function _edgeGapFor(side, popupGap) {