1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-07 22:15:38 -05:00

feat: Initial Privacy inidicators implementation

This commit is contained in:
purian23
2025-08-06 20:48:38 -04:00
parent 732f31d3c1
commit d036684842
4 changed files with 363 additions and 1 deletions

View File

@@ -60,6 +60,12 @@ ScrollView {
"description": "System notification area icons", "description": "System notification area icons",
"icon": "notifications", "icon": "notifications",
"enabled": true "enabled": true
}, {
"id": "privacyIndicator",
"text": "Privacy Indicator",
"description": "Shows when microphone, camera, or screen sharing is active",
"icon": "privacy_tip",
"enabled": true
}, { }, {
"id": "controlCenterButton", "id": "controlCenterButton",
"text": "Control Center", "text": "Control Center",
@@ -112,6 +118,9 @@ ScrollView {
"enabled": true "enabled": true
}] }]
property var defaultRightWidgets: [{ property var defaultRightWidgets: [{
"id": "privacyIndicator",
"enabled": true
}, {
"id": "systemTray", "id": "systemTray",
"enabled": true "enabled": true
}, { }, {
@@ -619,7 +628,7 @@ ScrollView {
border.width: 1 border.width: 1
visible: resetArea.containsMouse visible: resetArea.containsMouse
opacity: resetArea.containsMouse ? 1 : 0 opacity: resetArea.containsMouse ? 1 : 0
y: column.y + 48 // Position above the reset button in the header y: column.y + 48
x: parent.width - width - Theme.spacingM x: parent.width - width - Theme.spacingM
z: 100 z: 100

View File

@@ -0,0 +1,173 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property string section: "right"
property var popupTarget: null
property var parentScreen: null
readonly property bool hasActivePrivacy: PrivacyService.anyPrivacyActive
readonly property int activeCount: (PrivacyService.microphoneActive ? 1 : 0) + (PrivacyService.cameraActive ? 1 : 0) + (PrivacyService.screensharingActive ? 1 : 0)
width: hasActivePrivacy ? (activeCount > 1 ? 80 : 60) : 0
height: hasActivePrivacy ? 30 : 0
radius: Theme.cornerRadius
visible: hasActivePrivacy
opacity: hasActivePrivacy ? 1 : 0
color: {
const baseColor = privacyArea.containsMouse ? Theme.errorPressed : Theme.errorHover;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
MouseArea {
id: privacyArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
}
}
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
visible: hasActivePrivacy
Item {
width: 18
height: 18
visible: PrivacyService.microphoneActive
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "mic"
size: Theme.iconSizeSmall
color: Theme.error
filled: true
anchors.centerIn: parent
}
}
Item {
width: 18
height: 18
visible: PrivacyService.cameraActive
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "camera_video"
size: Theme.iconSizeSmall
color: Theme.surfaceText
filled: true
anchors.centerIn: parent
}
Rectangle {
width: 6
height: 6
radius: 3
color: Theme.error
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: -2
anchors.topMargin: -1
SequentialAnimation on opacity {
running: parent.visible
loops: Animation.Infinite
NumberAnimation {
to: 0.3
duration: Theme.longDuration
}
NumberAnimation {
to: 1.0
duration: Theme.longDuration
}
}
}
}
Item {
width: 18
height: 18
visible: PrivacyService.screensharingActive
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "screen_share"
size: Theme.iconSizeSmall
color: Theme.warning
filled: true
anchors.centerIn: parent
}
}
}
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on opacity {
enabled: !hasActivePrivacy
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
Rectangle {
id: tooltip
width: tooltipText.contentWidth + Theme.spacingM * 2
height: tooltipText.contentHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Theme.popupBackground()
border.color: Theme.outlineMedium
border.width: 1
visible: privacyArea.containsMouse && hasActivePrivacy
opacity: visible ? 1 : 0
z: 100
x: (parent.width - width) / 2
y: -height - Theme.spacingXS
StyledText {
id: tooltipText
anchors.centerIn: parent
text: PrivacyService.getPrivacySummary()
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Rectangle {
width: 8
height: 8
color: parent.color
border.color: parent.border.color
border.width: parent.border.width
rotation: 45
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.bottom
anchors.topMargin: -4
}
}
}

View File

@@ -176,6 +176,8 @@ PanelWindow {
return true; return true;
case "systemTray": case "systemTray":
return true; return true;
case "privacyIndicator":
return true;
case "clipboard": case "clipboard":
return true; return true;
case "systemResources": case "systemResources":
@@ -211,6 +213,8 @@ PanelWindow {
return weatherComponent; return weatherComponent;
case "systemTray": case "systemTray":
return systemTrayComponent; return systemTrayComponent;
case "privacyIndicator":
return privacyIndicatorComponent;
case "clipboard": case "clipboard":
return clipboardComponent; return clipboardComponent;
case "systemResources": case "systemResources":
@@ -552,6 +556,21 @@ PanelWindow {
} }
Component {
id: privacyIndicatorComponent
PrivacyIndicator {
section: {
if (parent && parent.parent === leftSection) return "left";
if (parent && parent.parent === rightSection) return "right";
if (parent && parent.parent === centerSection) return "center";
return "right";
}
parentScreen: root.screen
}
}
Component { Component {
id: clipboardComponent id: clipboardComponent

161
Services/PrivacyService.qml Normal file
View File

@@ -0,0 +1,161 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
Singleton {
id: root
property bool microphoneActive: false
property bool cameraActive: false
property bool screensharingActive: false
readonly property bool anyPrivacyActive: microphoneActive || cameraActive || screensharingActive
readonly property var micSource: AudioService.source
property var activeMicSources: []
property var activeCameraSources: []
property var activeScreenSources: []
function resetPrivacyStates() {
root.cameraActive = false;
root.screensharingActive = false;
root.microphoneActive = false;
}
Process {
id: portalMonitor
command: ["bash", "-c", `
screencast_count=0
camera_count=0
microphone_count=0
if command -v lsof >/dev/null 2>&1; then
video_device_users=$(lsof /dev/video* 2>/dev/null | wc -l || echo "0")
if [ "$video_device_users" -gt 0 ]; then
camera_count=1
fi
if command -v busctl >/dev/null 2>&1; then
mic_access_count=$(busctl --user call org.freedesktop.portal.Desktop /org/freedesktop/portal/desktop org.freedesktop.DBus.Properties Get ss "org.freedesktop.portal.Inhibit" "Inhibited" 2>/dev/null | grep -c "microphone" || echo "0")
if [ "$mic_access_count" -gt 0 ]; then
microphone_count=1
fi
fi
if [ "$microphone_count" -eq 0 ] && command -v pactl >/dev/null 2>&1; then
total_outputs=$(pactl list short source-outputs | wc -l || echo "0")
system_outputs=$(pactl list source-outputs | grep -c "media\\.name.*cava" || echo "0")
user_outputs=$((total_outputs - system_outputs))
if [ "$user_outputs" -gt 0 ]; then
microphone_count=1
fi
fi
fi
if command -v busctl >/dev/null 2>&1; then
screencast_sessions=$(busctl --user list | grep "org.freedesktop.portal.Session" | wc -l || echo "0")
if [ "$screencast_sessions" -gt 0 ]; then
screencast_count=1
fi
fi
if command -v pw-dump >/dev/null 2>&1; then
active_video_streams=$(pw-dump 2>/dev/null | grep -i "video" | grep -i "state.*running" | wc -l || echo "0")
if [ "$active_video_streams" -gt 0 ]; then
camera_count=1
fi
active_screen_streams=$(pw-dump 2>/dev/null | grep -i "screen" | grep -i "state.*running" | wc -l || echo "0")
if [ "$active_screen_streams" -gt 0 ]; then
screencast_count=1
fi
fi
echo "screencast:$screencast_count"
echo "camera:$camera_count"
echo "microphone:$microphone_count"
`]
stdout: StdioCollector {
onStreamFinished: {
root.resetPrivacyStates();
if (text && text.length > 0) {
const lines = text.trim().split('\n');
let foundScreencast = false;
let foundCamera = false;
let foundMicrophone = false;
for (const line of lines) {
if (line.startsWith('screencast:')) {
const count = parseInt(line.split(':')[1]) || 0;
foundScreencast = foundScreencast || (count > 0);
} else if (line.startsWith('camera:')) {
const count = parseInt(line.split(':')[1]) || 0;
foundCamera = foundCamera || (count > 0);
} else if (line.startsWith('microphone:')) {
const count = parseInt(line.split(':')[1]) || 0;
foundMicrophone = foundMicrophone || (count > 0);
}
}
root.screensharingActive = foundScreencast;
root.cameraActive = foundCamera;
root.microphoneActive = foundMicrophone;
} else {
root.screensharingActive = false;
root.cameraActive = false;
root.microphoneActive = false;
}
}
}
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
console.warn("PrivacyService: Portal monitor process failed with exit code:", exitCode);
}
}
}
Timer {
id: privacyMonitor
interval: 3000
running: true
repeat: true
onTriggered: {
if (!portalMonitor.running) {
portalMonitor.running = true;
}
}
}
Component.onCompleted: {
}
function getMicrophoneStatus() {
return microphoneActive ? "active" : "inactive";
}
function getCameraStatus() {
return cameraActive ? "active" : "inactive";
}
function getScreensharingStatus() {
return screensharingActive ? "active" : "inactive";
}
function getPrivacySummary() {
const active = [];
if (microphoneActive) active.push("microphone");
if (cameraActive) active.push("camera");
if (screensharingActive) active.push("screensharing");
return active.length > 0 ?
"Privacy active: " + active.join(", ") :
"No privacy concerns detected";
}
}