mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-07 05:55:37 -05:00
feat: Initial Privacy inidicators implementation
This commit is contained in:
@@ -60,6 +60,12 @@ ScrollView {
|
||||
"description": "System notification area icons",
|
||||
"icon": "notifications",
|
||||
"enabled": true
|
||||
}, {
|
||||
"id": "privacyIndicator",
|
||||
"text": "Privacy Indicator",
|
||||
"description": "Shows when microphone, camera, or screen sharing is active",
|
||||
"icon": "privacy_tip",
|
||||
"enabled": true
|
||||
}, {
|
||||
"id": "controlCenterButton",
|
||||
"text": "Control Center",
|
||||
@@ -112,6 +118,9 @@ ScrollView {
|
||||
"enabled": true
|
||||
}]
|
||||
property var defaultRightWidgets: [{
|
||||
"id": "privacyIndicator",
|
||||
"enabled": true
|
||||
}, {
|
||||
"id": "systemTray",
|
||||
"enabled": true
|
||||
}, {
|
||||
@@ -619,7 +628,7 @@ ScrollView {
|
||||
border.width: 1
|
||||
visible: resetArea.containsMouse
|
||||
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
|
||||
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;
|
||||
case "systemTray":
|
||||
return true;
|
||||
case "privacyIndicator":
|
||||
return true;
|
||||
case "clipboard":
|
||||
return true;
|
||||
case "systemResources":
|
||||
@@ -211,6 +213,8 @@ PanelWindow {
|
||||
return weatherComponent;
|
||||
case "systemTray":
|
||||
return systemTrayComponent;
|
||||
case "privacyIndicator":
|
||||
return privacyIndicatorComponent;
|
||||
case "clipboard":
|
||||
return clipboardComponent;
|
||||
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 {
|
||||
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