mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-02 03:28:28 -04:00
Compare commits
6 Commits
c67b185076
...
alpha
| Author | SHA1 | Date | |
|---|---|---|---|
| 52068fb61b | |||
| 64461c534f | |||
| 73da4879f6 | |||
| 8594a414ec | |||
| df396bfa43 | |||
| 33677150b1 |
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -34,7 +34,7 @@ Scope {
|
||||
}
|
||||
|
||||
function exclusionSizeForEdge(edge) {
|
||||
return root.barEdges.includes(edge) ? SettingsData.frameBarSize : SettingsData.frameThickness;
|
||||
return SettingsData.frameEdgeReservation(root.screen, edge);
|
||||
}
|
||||
|
||||
Loader {
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user