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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
173
Modules/TopBar/PrivacyIndicator.qml
Normal file
173
Modules/TopBar/PrivacyIndicator.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
161
Services/PrivacyService.qml
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user