update
This commit is contained in:
282
home/aiden/.config/noctalia/plugins/privacy-indicator/Main.qml
Normal file
282
home/aiden/.config/noctalia/plugins/privacy-indicator/Main.qml
Normal file
@@ -0,0 +1,282 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property var pluginApi: null
|
||||
|
||||
// --- Logic extracted from BarWidget.qml ---
|
||||
|
||||
property bool micActive: false
|
||||
property bool camActive: false
|
||||
property bool scrActive: false
|
||||
property var micApps: []
|
||||
property var camApps: []
|
||||
property var scrApps: []
|
||||
|
||||
property var accessHistory: []
|
||||
|
||||
// Previous states for history tracking
|
||||
property var _prevMicApps: []
|
||||
property var _prevCamApps: []
|
||||
property var _prevScrApps: []
|
||||
|
||||
PwObjectTracker {
|
||||
objects: Pipewire.ready ? Pipewire.nodes.values : []
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cameraDetectionProcess
|
||||
running: false
|
||||
command: ["sh", "-c", "for dev in /sys/class/video4linux/video*; do [ -e \"$dev/name\" ] && grep -qv 'Metadata' \"$dev/name\" && dev_name=$(basename \"$dev\") && find /proc/[0-9]*/fd -lname \"/dev/$dev_name\" 2>/dev/null; done | cut -d/ -f3 | xargs -r ps -o comm= -p | sort -u | tr '\\n' ',' | sed 's/,$//'"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var appsString = this.text.trim();
|
||||
var apps = appsString.length > 0 ? appsString.split(',') : [];
|
||||
root.camApps = apps;
|
||||
root.camActive = apps.length > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
repeat: true
|
||||
running: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: updatePrivacyState()
|
||||
}
|
||||
|
||||
function hasNodeLinks(node, links) {
|
||||
for (var i = 0; i < links.length; i++) {
|
||||
var link = links[i];
|
||||
if (link && (link.source === node || link.target === node)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getAppName(node) {
|
||||
return node.properties["application.name"] || node.nickname || node.name || "";
|
||||
}
|
||||
|
||||
function updateMicrophoneState(nodes, links) {
|
||||
var appNames = [];
|
||||
var isActive = false;
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
var node = nodes[i];
|
||||
if (!node || !node.isStream || !node.audio || node.isSink) continue;
|
||||
if (!hasNodeLinks(node, links) || !node.properties) continue;
|
||||
var mediaClass = node.properties["media.class"] || "";
|
||||
if (mediaClass === "Stream/Input/Audio") {
|
||||
if (node.properties["stream.capture.sink"] === "true") continue;
|
||||
isActive = true;
|
||||
var appName = getAppName(node);
|
||||
if (appName && appNames.indexOf(appName) === -1) appNames.push(appName);
|
||||
}
|
||||
}
|
||||
root.micActive = isActive;
|
||||
root.micApps = appNames;
|
||||
}
|
||||
|
||||
function updateCameraState() {
|
||||
cameraDetectionProcess.running = true;
|
||||
}
|
||||
|
||||
function isScreenShareNode(node) {
|
||||
if (!node.properties) return false;
|
||||
var mediaClass = node.properties["media.class"] || "";
|
||||
if (mediaClass.indexOf("Audio") >= 0) return false;
|
||||
if (mediaClass.indexOf("Video") === -1) return false;
|
||||
var mediaName = (node.properties["media.name"] || "").toLowerCase();
|
||||
if (mediaName.match(/^(xdph-streaming|gsr-default|game capture|screen|desktop|display|cast|webrtc|v4l2)/) ||
|
||||
mediaName === "gsr-default_output" ||
|
||||
mediaName.match(/screen-cast|screen-capture|desktop-capture|monitor-capture|window-capture|game-capture/i)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function updateScreenShareState(nodes, links) {
|
||||
var appNames = [];
|
||||
var isActive = false;
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
var node = nodes[i];
|
||||
if (!node || !hasNodeLinks(node, links) || !node.properties) continue;
|
||||
if (isScreenShareNode(node)) {
|
||||
isActive = true;
|
||||
var appName = getAppName(node);
|
||||
if (appName && appNames.indexOf(appName) === -1) appNames.push(appName);
|
||||
}
|
||||
}
|
||||
root.scrActive = isActive;
|
||||
root.scrApps = appNames;
|
||||
}
|
||||
|
||||
function updatePrivacyState() {
|
||||
if (!Pipewire.ready) return;
|
||||
var nodes = Pipewire.nodes.values || [];
|
||||
var links = Pipewire.links.values || [];
|
||||
updateMicrophoneState(nodes, links);
|
||||
updateCameraState();
|
||||
updateScreenShareState(nodes, links);
|
||||
}
|
||||
|
||||
// --- History Persistence ---
|
||||
|
||||
property string stateFile: ""
|
||||
property bool isLoaded: false
|
||||
|
||||
Component.onCompleted: {
|
||||
// Setup state file path
|
||||
Qt.callLater(() => {
|
||||
if (typeof Settings !== 'undefined' && Settings.cacheDir) {
|
||||
stateFile = Settings.cacheDir + "privacy-history.json";
|
||||
historyFileView.path = stateFile;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: historyFileView
|
||||
printErrors: false
|
||||
watchChanges: false
|
||||
|
||||
adapter: JsonAdapter {
|
||||
id: adapter
|
||||
property var history: []
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
root.isLoaded = true;
|
||||
if (adapter.history) {
|
||||
// Restore history
|
||||
root.accessHistory = adapter.history;
|
||||
}
|
||||
}
|
||||
|
||||
onLoadFailed: error => {
|
||||
// If file doesn't exist (error 2), we are ready to save new data
|
||||
if (error === 2) {
|
||||
root.isLoaded = true;
|
||||
} else {
|
||||
console.error("PrivacyIndicator: Failed to load history file:", error);
|
||||
root.isLoaded = true; // Try to continue anyway
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveHistory() {
|
||||
if (!stateFile || !isLoaded) return;
|
||||
|
||||
adapter.history = root.accessHistory;
|
||||
|
||||
// Ensure cache directory exists and save
|
||||
try {
|
||||
Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]);
|
||||
Qt.callLater(() => {
|
||||
try {
|
||||
historyFileView.writeAdapter();
|
||||
} catch (e) {
|
||||
console.error("PrivacyIndicator: Failed to save history", e);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("PrivacyIndicator: Failed to save history", e);
|
||||
}
|
||||
}
|
||||
|
||||
function addToHistory(app, type, icon, colorKey, action) {
|
||||
var time = new Date().toLocaleTimeString(Qt.locale(), Locale.ShortFormat);
|
||||
var entry = {
|
||||
"appName": app,
|
||||
"type": type,
|
||||
"icon": icon,
|
||||
"colorKey": colorKey,
|
||||
"time": time,
|
||||
"timestamp": Date.now(),
|
||||
"action": action // "started" or "stopped"
|
||||
};
|
||||
var newHistory = [entry].concat(accessHistory);
|
||||
if (newHistory.length > 50) newHistory = newHistory.slice(0, 50); // Increased limit as we have more entries now
|
||||
accessHistory = newHistory;
|
||||
saveHistory();
|
||||
}
|
||||
|
||||
function clearHistory() {
|
||||
accessHistory = [];
|
||||
saveHistory();
|
||||
}
|
||||
|
||||
function checkAppChanges(newApps, oldApps, type, icon, colorKey) {
|
||||
if (!newApps && !oldApps) return;
|
||||
|
||||
// Check for new apps (Started)
|
||||
if (newApps) {
|
||||
for (var i = 0; i < newApps.length; i++) {
|
||||
var app = newApps[i];
|
||||
if (!oldApps || oldApps.indexOf(app) === -1) {
|
||||
addToHistory(app, type, icon, colorKey, "started");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for removed apps (Stopped)
|
||||
if (oldApps) {
|
||||
for (var j = 0; j < oldApps.length; j++) {
|
||||
var oldApp = oldApps[j];
|
||||
if (!newApps || newApps.indexOf(oldApp) === -1) {
|
||||
addToHistory(oldApp, type, icon, colorKey, "stopped");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get active color from settings or default
|
||||
property var cfg: pluginApi?.pluginSettings || ({})
|
||||
property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({})
|
||||
property string activeColorKey: cfg.activeColor ?? defaults.activeColor ?? "primary"
|
||||
|
||||
onMicAppsChanged: {
|
||||
checkAppChanges(micApps, _prevMicApps, "Microphone", "microphone", activeColorKey);
|
||||
_prevMicApps = micApps;
|
||||
}
|
||||
// Helper to detect activation edge
|
||||
property bool oldMicActive: false
|
||||
onMicActiveChanged: {
|
||||
if (micActive && (!oldMicActive)) {
|
||||
ToastService.showNotice(pluginApi?.tr("toast.mic-on") || "Microphone is active", "", "microphone");
|
||||
}
|
||||
oldMicActive = micActive
|
||||
}
|
||||
|
||||
property bool oldCamActive: false
|
||||
onCamActiveChanged: {
|
||||
if (camActive && !oldCamActive) {
|
||||
ToastService.showNotice(pluginApi?.tr("toast.cam-on") || "Camera is active", "", "camera");
|
||||
}
|
||||
oldCamActive = camActive
|
||||
}
|
||||
onCamAppsChanged: {
|
||||
checkAppChanges(camApps, _prevCamApps, "Camera", "camera", activeColorKey);
|
||||
_prevCamApps = camApps;
|
||||
}
|
||||
|
||||
property bool oldScrActive: false
|
||||
onScrActiveChanged: {
|
||||
if (scrActive && !oldScrActive) {
|
||||
ToastService.showNotice(pluginApi?.tr("toast.screen-on") || "Screen sharing is active", "", "screen-share");
|
||||
}
|
||||
oldScrActive = scrActive
|
||||
}
|
||||
onScrAppsChanged: {
|
||||
checkAppChanges(scrApps, _prevScrApps, "Screen", "screen-share", activeColorKey);
|
||||
_prevScrApps = scrApps;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user