summaryrefslogtreecommitdiff
path: root/noctalia/.config/plugins/privacy-indicator
diff options
context:
space:
mode:
Diffstat (limited to 'noctalia/.config/plugins/privacy-indicator')
-rw-r--r--noctalia/.config/plugins/privacy-indicator/BarWidget.qml175
-rw-r--r--noctalia/.config/plugins/privacy-indicator/Main.qml316
-rw-r--r--noctalia/.config/plugins/privacy-indicator/Panel.qml170
-rw-r--r--noctalia/.config/plugins/privacy-indicator/README.md47
-rw-r--r--noctalia/.config/plugins/privacy-indicator/Settings.qml142
-rw-r--r--noctalia/.config/plugins/privacy-indicator/i18n/de.json58
-rw-r--r--noctalia/.config/plugins/privacy-indicator/i18n/en.json58
-rw-r--r--noctalia/.config/plugins/privacy-indicator/i18n/es.json46
-rw-r--r--noctalia/.config/plugins/privacy-indicator/i18n/fr.json58
-rw-r--r--noctalia/.config/plugins/privacy-indicator/i18n/hu.json46
-rw-r--r--noctalia/.config/plugins/privacy-indicator/i18n/it.json46
-rw-r--r--noctalia/.config/plugins/privacy-indicator/i18n/ja.json46
-rw-r--r--noctalia/.config/plugins/privacy-indicator/i18n/ko.json58
-rw-r--r--noctalia/.config/plugins/privacy-indicator/i18n/ku.json46
-rw-r--r--noctalia/.config/plugins/privacy-indicator/i18n/nl.json46
-rw-r--r--noctalia/.config/plugins/privacy-indicator/i18n/pl.json46
-rw-r--r--noctalia/.config/plugins/privacy-indicator/i18n/pt.json46
-rw-r--r--noctalia/.config/plugins/privacy-indicator/i18n/ru.json46
-rw-r--r--noctalia/.config/plugins/privacy-indicator/i18n/tr.json46
-rw-r--r--noctalia/.config/plugins/privacy-indicator/i18n/uk-UA.json46
-rw-r--r--noctalia/.config/plugins/privacy-indicator/i18n/vi.json54
-rw-r--r--noctalia/.config/plugins/privacy-indicator/i18n/zh-CN.json46
-rw-r--r--noctalia/.config/plugins/privacy-indicator/i18n/zh-TW.json46
-rw-r--r--noctalia/.config/plugins/privacy-indicator/manifest.json37
-rw-r--r--noctalia/.config/plugins/privacy-indicator/preview.pngbin0 -> 26449 bytes
25 files changed, 1771 insertions, 0 deletions
diff --git a/noctalia/.config/plugins/privacy-indicator/BarWidget.qml b/noctalia/.config/plugins/privacy-indicator/BarWidget.qml
new file mode 100644
index 0000000..bdebd41
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/BarWidget.qml
@@ -0,0 +1,175 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import qs.Commons
+import qs.Services.UI
+import qs.Widgets
+
+Item {
+ id: root
+
+ property var pluginApi: null
+
+ property ShellScreen screen
+ property string widgetId: ""
+ property string section: ""
+ property int sectionWidgetIndex: -1
+ property int sectionWidgetsCount: 0
+
+ // Bar positioning properties
+ readonly property string screenName: screen ? screen.name : ""
+ readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
+ readonly property bool isVertical: barPosition === "left" || barPosition === "right"
+ readonly property real barHeight: Style.getBarHeightForScreen(screenName)
+ readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
+ readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)
+
+ // Access main instance for state
+ readonly property var mainInstance: pluginApi?.mainInstance
+
+ property bool micActive: mainInstance ? mainInstance.micActive : false
+ property bool camActive: mainInstance ? mainInstance.camActive : false
+ property bool scrActive: mainInstance ? mainInstance.scrActive : false
+ property var micApps: mainInstance ? mainInstance.micApps : []
+ property var camApps: mainInstance ? mainInstance.camApps : []
+ property var scrApps: mainInstance ? mainInstance.scrApps : []
+
+ property var cfg: pluginApi?.pluginSettings || ({})
+ property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({})
+
+ property bool hideInactive: cfg.hideInactive ?? defaults.hideInactive ?? false
+ property bool enableToast: cfg.enableToast ?? defaults.enableToast ?? true
+ property bool removeMargins: cfg.removeMargins ?? defaults.removeMargins ?? false
+ property int iconSpacing: cfg.iconSpacing ?? defaults.iconSpacing ?? 4
+ property string activeColorKey: cfg.activeColor ?? defaults.activeColor ?? "primary"
+ property string inactiveColorKey: cfg.inactiveColor ?? defaults.inactiveColor ?? "none"
+
+ readonly property color activeColor: Color.resolveColorKey(activeColorKey)
+ readonly property color inactiveColor: inactiveColorKey === "none" ? Qt.alpha(Color.mOnSurfaceVariant, 0.3) : Color.resolveColorKey(inactiveColorKey)
+ readonly property color micColor: micActive ? activeColor : inactiveColor
+ readonly property color camColor: camActive ? activeColor : inactiveColor
+ readonly property color scrColor: scrActive ? activeColor : inactiveColor
+
+ readonly property bool isVisible: !hideInactive || micActive || camActive || scrActive
+
+ property real margins: removeMargins ? 0 : Style.marginM * 2
+
+ readonly property real contentWidth: isVertical ? Style.capsuleHeight : Math.round(layout.implicitWidth + margins)
+ readonly property real contentHeight: isVertical ? Math.round(layout.implicitHeight + margins) : Style.capsuleHeight
+
+ implicitWidth: contentWidth
+ implicitHeight: contentHeight
+
+ Layout.alignment: Qt.AlignVCenter
+ visible: root.isVisible
+ opacity: root.isVisible ? 1.0 : 0.0
+
+ function buildTooltip() {
+ var parts = [];
+
+ if (micActive && micApps.length > 0) {
+ parts.push("Mic: " + micApps.join(", "));
+ }
+
+ if (camActive && camApps.length > 0) {
+ parts.push("Cam: " + camApps.join(", "));
+ }
+
+ if (scrActive && scrApps.length > 0) {
+ parts.push("Screen sharing: " + scrApps.join(", "));
+ }
+
+ return parts.length > 0 ? parts.join("\n") : "";
+ }
+
+ Rectangle {
+ id: visualCapsule
+ x: Style.pixelAlignCenter(parent.width, width)
+ y: Style.pixelAlignCenter(parent.height, height)
+ width: root.contentWidth
+ height: root.contentHeight
+ radius: Style.radiusM
+ color: Style.capsuleColor
+ border.color: Style.capsuleBorderColor
+ border.width: Style.capsuleBorderWidth
+
+ Item {
+ id: layout
+
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ implicitWidth: iconsLayout.implicitWidth
+ implicitHeight: iconsLayout.implicitHeight
+
+ GridLayout {
+ id: iconsLayout
+
+ columns: root.isVertical ? 1 : 3
+ rows: root.isVertical ? 3 : 1
+
+ rowSpacing: root.iconSpacing
+ columnSpacing: root.iconSpacing
+
+ NIcon {
+ visible: micActive || !root.hideInactive
+ icon: micActive ? "microphone" : "microphone-off"
+ color: root.micColor
+ }
+ NIcon {
+ visible: camActive || !root.hideInactive
+ icon: camActive ? "camera" : "camera-off"
+ color: root.camColor
+ }
+ NIcon {
+ visible: scrActive || !root.hideInactive
+ icon: scrActive ? "screen-share" : "screen-share-off"
+ color: root.scrColor
+ }
+ }
+ }
+ }
+
+ NPopupContextMenu {
+ id: contextMenu
+
+ model: [
+ {
+ "label": pluginApi?.tr("menu.settings"),
+ "action": "settings",
+ "icon": "settings"
+ },
+ ]
+
+ onTriggered: function (action) {
+ contextMenu.close();
+ PanelService.closeContextMenu(screen);
+ if (action === "settings") {
+ BarService.openPluginSettings(root.screen, pluginApi.manifest);
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ acceptedButtons: Qt.RightButton | Qt.LeftButton
+ hoverEnabled: true
+
+ onClicked: function (mouse) {
+ if (mouse.button === Qt.RightButton) {
+ PanelService.showContextMenu(contextMenu, root, screen);
+ } else if (mouse.button === Qt.LeftButton) {
+ if (pluginApi) pluginApi.openPanel(root.screen, root);
+ }
+ }
+
+ onEntered: {
+ var tooltipText = buildTooltip();
+ if (tooltipText) {
+ TooltipService.show(root, tooltipText, BarService.getTooltipDirection());
+ }
+ }
+ onExited: TooltipService.hide()
+ }
+}
diff --git a/noctalia/.config/plugins/privacy-indicator/Main.qml b/noctalia/.config/plugins/privacy-indicator/Main.qml
new file mode 100644
index 0000000..29d94a0
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/Main.qml
@@ -0,0 +1,316 @@
+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: []
+
+ // Get active color from settings or default
+ property var cfg: pluginApi?.pluginSettings || ({})
+ property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({})
+ property bool enableToast: cfg.enableToast ?? defaults.enableToast ?? true
+ property string activeColorKey: cfg.activeColor ?? defaults.activeColor ?? "primary"
+ property string micFilterRegex: cfg.micFilterRegex ?? defaults.micFilterRegex ?? ""
+ property string camFilterRegex: cfg.camFilterRegex ?? defaults.camFilterRegex ?? ""
+
+ 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(',') : [];
+
+ var filterRegex = null;
+ if (root.camFilterRegex && root.camFilterRegex.length > 0) {
+ try {
+ filterRegex = new RegExp(root.camFilterRegex);
+ } catch (e) {
+ Logger.w("PrivacyIndicator: Invalid camFilterRegex:", root.camFilterRegex);
+ }
+ }
+
+ var appNames = [];
+ for (var i = 0; i < apps.length; i++) {
+ var appName = apps[i];
+ if (filterRegex && appName && filterRegex.test(appName)) continue;
+ if (appName && appNames.indexOf(appName) === -1) appNames.push(appName);
+ }
+
+ root.camApps = appNames;
+ root.camActive = appNames.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;
+
+ var filterRegex = null;
+ if (root.micFilterRegex && root.micFilterRegex.length > 0) {
+ try {
+ filterRegex = new RegExp(root.micFilterRegex);
+ } catch (e) {
+ Logger.w("PrivacyIndicator: Invalid micFilterRegex:", root.micFilterRegex);
+ }
+ }
+
+ 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;
+
+ var appName = getAppName(node);
+ if (filterRegex && appName && filterRegex.test(appName)) continue;
+
+ isActive = true;
+ 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");
+ }
+ }
+ }
+ }
+
+
+ onMicAppsChanged: {
+ checkAppChanges(micApps, _prevMicApps, "Microphone", "microphone", activeColorKey);
+ _prevMicApps = micApps;
+ }
+ // Helper to detect activation edge
+ property bool oldMicActive: false
+ onMicActiveChanged: {
+ if (enableToast && micActive && !oldMicActive) {
+ ToastService.showNotice(pluginApi?.tr("toast.mic-on"), "", "microphone");
+ }
+ oldMicActive = micActive
+ }
+
+ property bool oldCamActive: false
+ onCamActiveChanged: {
+ if (enableToast && camActive && !oldCamActive) {
+ ToastService.showNotice(pluginApi?.tr("toast.cam-on"), "", "camera");
+ }
+ oldCamActive = camActive
+ }
+ onCamAppsChanged: {
+ checkAppChanges(camApps, _prevCamApps, "Camera", "camera", activeColorKey);
+ _prevCamApps = camApps;
+ }
+
+ property bool oldScrActive: false
+ onScrActiveChanged: {
+ if (enableToast && scrActive && !oldScrActive) {
+ ToastService.showNotice(pluginApi?.tr("toast.screen-on"), "", "screen-share");
+ }
+ oldScrActive = scrActive
+ }
+ onScrAppsChanged: {
+ checkAppChanges(scrApps, _prevScrApps, "Screen", "screen-share", activeColorKey);
+ _prevScrApps = scrApps;
+ }
+
+
+}
diff --git a/noctalia/.config/plugins/privacy-indicator/Panel.qml b/noctalia/.config/plugins/privacy-indicator/Panel.qml
new file mode 100644
index 0000000..0cfcc22
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/Panel.qml
@@ -0,0 +1,170 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import qs.Commons
+import qs.Widgets
+
+Item {
+ id: root
+
+ property var pluginApi: null
+
+ // Standard panel properties
+ readonly property var geometryPlaceholder: panelContainer
+ property real contentPreferredWidth: 320 * Style.uiScaleRatio
+ property real contentPreferredHeight: 450 * Style.uiScaleRatio
+
+ readonly property var mainInstance: pluginApi?.mainInstance
+ readonly property bool allowAttach: true
+
+ Rectangle {
+ id: panelContainer
+ anchors.fill: parent
+ color: "transparent"
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: Style.marginM
+ spacing: Style.marginM
+
+ // Header Box
+ NBox {
+ Layout.fillWidth: true
+ Layout.preferredHeight: headerRow.implicitHeight + Style.marginM * 2
+
+ RowLayout {
+ id: headerRow
+ anchors.fill: parent
+ anchors.margins: Style.marginM
+ spacing: Style.marginS
+
+ NIcon {
+ icon: "shield-check"
+ color: Color.mPrimary
+ pointSize: Style.fontSizeL
+ }
+
+ NText {
+ Layout.fillWidth: true
+ text: pluginApi?.tr("history.title")
+ font.weight: Style.fontWeightBold
+ pointSize: Style.fontSizeL
+ color: Color.mOnSurface
+ }
+
+ NIconButton {
+ icon: "trash"
+ baseSize: Style.baseWidgetSize * 0.8
+ onClicked: {
+ if (mainInstance) mainInstance.clearHistory();
+ }
+ }
+ }
+ }
+
+
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ NScrollView {
+ id: scrollView
+ anchors.fill: parent
+ horizontalPolicy: ScrollBar.AlwaysOff
+ verticalPolicy: ScrollBar.AsNeeded
+
+ ColumnLayout {
+ width: scrollView.availableWidth
+ spacing: Style.marginS
+
+ Repeater {
+ model: mainInstance ? mainInstance.accessHistory : []
+
+ delegate: Rectangle {
+ Layout.fillWidth: true
+ implicitHeight: 56 * Style.uiScaleRatio
+ radius: Style.radiusM
+ color: Color.mSurfaceVariant
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: Style.marginM
+ spacing: Style.marginM
+
+ Rectangle {
+ width: 32 * Style.uiScaleRatio
+ height: 32 * Style.uiScaleRatio
+ radius: width/2
+ color: Qt.alpha(iconColor, 0.1)
+
+ readonly property color iconColor: Color.resolveColorKey(modelData.colorKey || "primary")
+
+ NIcon {
+ anchors.centerIn: parent
+ icon: modelData.icon
+ color: parent.iconColor
+ pointSize: Style.fontSizeM
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 0
+
+ NText {
+ Layout.fillWidth: true
+ text: modelData.appName
+ elide: Text.ElideRight
+ font.weight: Style.fontWeightBold
+ pointSize: Style.fontSizeM
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginS
+
+ NText {
+ text: modelData.time
+ color: Qt.alpha(Color.mOnSurface, 0.7)
+ pointSize: Style.fontSizeS
+ }
+
+ NText {
+ text: "•"
+ color: Qt.alpha(Color.mOnSurface, 0.3)
+ pointSize: Style.fontSizeS
+ }
+
+ NText {
+ text: {
+ const action = modelData.action || "started";
+ return pluginApi?.tr("history.action." + action) || action;
+ }
+ color: (modelData.action || "started") === "stopped" ? Color.resolveColorKey("error") : Color.resolveColorKey("primary")
+ font.weight: Style.fontWeightBold
+ pointSize: Style.fontSizeS
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Empty state
+ NText {
+ Layout.alignment: Qt.AlignHCenter
+ visible: (!mainInstance || mainInstance.accessHistory.length === 0)
+ text: pluginApi?.tr("history.empty")
+ color: Qt.alpha(Color.mOnSurface, 0.5)
+ pointSize: Style.fontSizeM
+ Layout.topMargin: Style.marginL
+ }
+
+ Item { Layout.fillHeight: true } // spacer
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/noctalia/.config/plugins/privacy-indicator/README.md b/noctalia/.config/plugins/privacy-indicator/README.md
new file mode 100644
index 0000000..d192040
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/README.md
@@ -0,0 +1,47 @@
+# Privacy Indicator Plugin
+
+A privacy indicator widget that monitors and displays when microphone, camera, or screen sharing is active on your system.
+
+## Features
+
+- **Microphone Monitoring**: Detects active microphone usage via Pipewire
+- **Camera Monitoring**: Detects active camera usage by checking `/dev/video*` devices
+- **Screen Sharing Detection**: Monitors screen sharing sessions via Pipewire
+- **Visual Indicators**: Shows icons that change color based on active state
+ - Active: Primary color
+ - Inactive: Semi-transparent variant color
+- **App Information**: Tooltip displays which applications are using each resource
+- **Adaptive Layout**: Automatically adjusts layout for horizontal or vertical bar positions
+
+## Configuration
+
+Access the plugin settings in Noctalia to configure the following options:
+
+- **Hide Inactive States**: If enabled, microphone, camera, and screen icons are hidden whenever they are inactive. Only active states are shown.
+- **Remove Margins**: If enabled, removes all outer margins of the widget.
+- **Icon Spacing**: Controls the horizontal/vertical spacing between the icons.
+- **Active/Inactive Icon Color**: Customize the colors for active and inactive states.
+- **Microphone Filter Regex**: Regex pattern to filter out specific microphone applications. Matching apps are completely excluded from detection (they won't trigger the indicator or appear in tooltips). Use `|` to specify multiple patterns, e.g., `effect_input.rnnoise|easyeffects`.
+
+
+## Usage
+
+The widget displays three icons in the bar:
+- **Microphone**: Shows when any app is using the microphone
+- **Camera**: Shows when any app is accessing the camera
+- **Screen Share**: Shows when screen sharing is active
+
+Hover over the widget to see a tooltip listing which applications are using each resource.
+
+## Requirements
+
+- Noctalia Shell 3.6.0 or higher
+- Pipewire (for microphone and screen sharing detection)
+- Access to `/dev/video*` devices (for camera detection)
+
+## Technical Details
+
+- Updates privacy state every second
+- Uses Pipewire API to monitor audio/video streams
+- Checks `/proc/[0-9]*/fd/` for camera device access
+- Detects screen sharing by analyzing Pipewire node properties and media class
diff --git a/noctalia/.config/plugins/privacy-indicator/Settings.qml b/noctalia/.config/plugins/privacy-indicator/Settings.qml
new file mode 100644
index 0000000..a6acde9
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/Settings.qml
@@ -0,0 +1,142 @@
+import QtQuick
+import QtQuick.Layouts
+import qs.Commons
+import qs.Widgets
+
+ColumnLayout {
+ id: root
+
+ property var pluginApi: null
+
+ property var cfg: pluginApi?.pluginSettings || ({})
+ property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({})
+
+ property bool hideInactive: cfg.hideInactive ?? defaults.hideInactive ?? false
+ property bool enableToast: cfg.enableToast ?? defaults.enableToast ?? true
+ property bool removeMargins: cfg.removeMargins ?? defaults.removeMargins ?? false
+ property int iconSpacing: cfg.iconSpacing ?? defaults.iconSpacing ?? 4
+ property string activeColor: cfg.activeColor ?? defaults.activeColor ?? "primary"
+ property string inactiveColor: cfg.inactiveColor ?? defaults.inactiveColor ?? "none"
+ property string micFilterRegex: cfg.micFilterRegex ?? defaults.micFilterRegex
+ property string camFilterRegex: cfg.camFilterRegex ?? defaults.camFilterRegex
+
+ spacing: Style.marginL
+
+ Component.onCompleted: {
+ Logger.i("PrivacyIndicator", "Settings UI loaded");
+ }
+
+ ColumnLayout {
+ spacing: Style.marginM
+ Layout.fillWidth: true
+
+ NToggle {
+ label: pluginApi?.tr("settings.hideInactive.label")
+ description: pluginApi?.tr("settings.hideInactive.desc")
+
+ checked: root.hideInactive
+ onToggled: checked => {
+ root.hideInactive = checked;
+ }
+ }
+
+ NToggle {
+ label: pluginApi?.tr("settings.enableToast.label")
+ description: pluginApi?.tr("settings.enableToast.desc")
+
+ checked: root.enableToast
+ onToggled: checked => {
+ root.enableToast = checked;
+ }
+ }
+
+ NToggle {
+ label: pluginApi?.tr("settings.removeMargins.label")
+ description: pluginApi?.tr("settings.removeMargins.desc")
+
+ checked: root.removeMargins
+ onToggled: checked => {
+ root.removeMargins = checked;
+ }
+ }
+
+ NColorChoice {
+ label: pluginApi?.tr("settings.activeColor.label")
+ description: pluginApi?.tr("settings.activeColor.desc")
+ currentKey: root.activeColor
+ onSelected: key => root.activeColor = key
+ }
+
+ NColorChoice {
+ label: pluginApi?.tr("settings.inactiveColor.label")
+ description: pluginApi?.tr("settings.inactiveColor.desc")
+ currentKey: root.inactiveColor
+ onSelected: key => root.inactiveColor = key
+ noneColor: Qt.alpha(Color.mOnSurfaceVariant, 0.3)
+ noneOnColor: Qt.alpha(Color.mOnSurface, 0.7)
+ }
+
+ NComboBox {
+ label: pluginApi?.tr("settings.iconSpacing.label")
+ description: pluginApi?.tr("settings.iconSpacing.desc")
+
+ model: {
+ const labels = ["XXS", "XS", "S", "M", "L", "XL"];
+ const values = [Style.marginXXS, Style.marginXS, Style.marginS, Style.marginM, Style.marginL, Style.marginXL];
+
+ const result = [];
+ for (var i = 0; i < labels.length; ++i) {
+ const v = values[i];
+ result.push({
+ key: v.toFixed(0),
+ name: `${labels[i]} (${v}px)`
+ });
+ }
+ return result;
+ }
+
+ // INFO: From my understanding, the toFixed(0) shouldn't be needed here and there, but without the
+ // current key does not show when opening the settings window.
+ currentKey: root.iconSpacing.toFixed(0)
+ onSelected: key => root.iconSpacing = key
+ }
+
+ NTextInput {
+ Layout.fillWidth: true
+ label: pluginApi?.tr("settings.micFilterRegex.label")
+ description: pluginApi?.tr("settings.micFilterRegex.desc")
+ placeholderText: "effect_input.rnnoise|easyeffects"
+ text: root.micFilterRegex
+ onTextChanged: root.micFilterRegex = text
+ }
+
+ NTextInput {
+ Layout.fillWidth: true
+ label: pluginApi?.tr("settings.camFilterRegex.label")
+ description: pluginApi?.tr("settings.camFilterRegex.desc")
+ placeholderText: "droidcam"
+ text: root.camFilterRegex
+ onTextChanged: root.camFilterRegex = text
+ }
+ }
+
+ function saveSettings() {
+ if (!pluginApi) {
+ Logger.e("PrivacyIndicator", "Cannot save settings: pluginApi is null");
+ return;
+ }
+
+ pluginApi.pluginSettings.hideInactive = root.hideInactive;
+ pluginApi.pluginSettings.enableToast = root.enableToast;
+ pluginApi.pluginSettings.iconSpacing = root.iconSpacing;
+ pluginApi.pluginSettings.removeMargins = root.removeMargins;
+ pluginApi.pluginSettings.activeColor = root.activeColor;
+ pluginApi.pluginSettings.inactiveColor = root.inactiveColor;
+ pluginApi.pluginSettings.micFilterRegex = root.micFilterRegex;
+ pluginApi.pluginSettings.camFilterRegex = root.camFilterRegex;
+
+ pluginApi.saveSettings();
+
+ Logger.i("PrivacyIndicator", "Settings saved successfully");
+ }
+}
diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/de.json b/noctalia/.config/plugins/privacy-indicator/i18n/de.json
new file mode 100644
index 0000000..5817042
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/i18n/de.json
@@ -0,0 +1,58 @@
+{
+ "menu": {
+ "settings": "Widget Einstellungen"
+ },
+ "settings": {
+ "activeColor": {
+ "desc": "Farbe der Symbole, wenn sie aktiv sind.",
+ "label": "Aktive Farbsymbol"
+ },
+ "hideInactive": {
+ "desc": "Mikrofon-, Kamera- und Bildschirmsymbole ausblenden, wenn sie inaktiv sind.",
+ "label": "Inaktive Zustände ausblenden"
+ },
+ "enableToast": {
+ "desc": "Zeige Toast Mitteilungen an, wenn sich einer der Zustände ändert.",
+ "label": "Aktiviere Toast Mitteilungen"
+ },
+ "inactiveColor": {
+ "desc": "Farbe der Symbole, wenn sie inaktiv sind.",
+ "label": "Inaktive Farbsymbol"
+ },
+ "iconSpacing": {
+ "desc": "Den Abstand zwischen den Symbolen festlegen.",
+ "label": "Symbolabstand"
+ },
+ "removeMargins": {
+ "desc": "Alle äußeren Ränder des Widgets entfernen.",
+ "label": "Ränder entfernen"
+ },
+ "micFilterRegex": {
+ "desc": "Regex Muster zum Herausfiltern von Mikrofon-Apps. Entsprechende Apps werden vollständig von der Erkennung ausgeschlossen.",
+ "label": "Regex für Mikrofonfilter"
+ },
+ "camFilterRegex": {
+ "desc": "Regex-Muster zum Herausfiltern von Kamera-Apps. Entsprechende Apps werden vollständig von der Erkennung ausgeschlossen.",
+ "label": "Regex für Kamerafilter"
+ }
+ },
+ "tooltip": {
+ "cam-on": "Kamera: {apps}",
+ "mic-on": "Mikrofon: {apps}",
+ "screen-on": "Bildschirmfreigabe: {apps}"
+ },
+ "toast": {
+ "cam-on": "Kamera ist aktiv",
+ "mic-on": "Mikrofon ist aktiv",
+ "screen-on": "Bildschirmfreigabe ist aktiv"
+ },
+ "history": {
+ "title": "Zugriffsverlauf",
+ "empty": "Kein kürzlicher Zugriff",
+ "clear": "Leeren",
+ "action": {
+ "started": "Gestartet",
+ "stopped": "Beendet"
+ }
+ }
+}
diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/en.json b/noctalia/.config/plugins/privacy-indicator/i18n/en.json
new file mode 100644
index 0000000..fa65418
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/i18n/en.json
@@ -0,0 +1,58 @@
+{
+ "menu": {
+ "settings": "Widget settings"
+ },
+ "settings": {
+ "activeColor": {
+ "desc": "Color of the icons when active.",
+ "label": "Active icon color"
+ },
+ "hideInactive": {
+ "desc": "Hide microphone, camera, and screen icons when there are inactive.",
+ "label": "Hide inactive states"
+ },
+ "enableToast": {
+ "desc": "Show toast messages when one of the states changes.",
+ "label": "Enable toast notifications"
+ },
+ "inactiveColor": {
+ "desc": "Color of the icons when inactive.",
+ "label": "Inactive icon color"
+ },
+ "iconSpacing": {
+ "desc": "Set the spacing between the icons.",
+ "label": "Icon spacing"
+ },
+ "removeMargins": {
+ "desc": "Remove all outer margins of the widget.",
+ "label": "Remove margins"
+ },
+ "micFilterRegex": {
+ "desc": "Regex pattern to filter out microphone applications. Matching apps are completely excluded from detection.",
+ "label": "Microphone filter regex"
+ },
+ "camFilterRegex": {
+ "desc": "Regex pattern to filter out camera applications. Matching apps are completely excluded from detection.",
+ "label": "Camera filter regex"
+ }
+ },
+ "tooltip": {
+ "cam-on": "Camera: {apps}",
+ "mic-on": "Microphone: {apps}",
+ "screen-on": "Screen sharing: {apps}"
+ },
+ "toast": {
+ "cam-on": "Camera is active",
+ "mic-on": "Microphone is active",
+ "screen-on": "Screen sharing is active"
+ },
+ "history": {
+ "title": "Access History",
+ "empty": "No recent access",
+ "clear": "Clear",
+ "action": {
+ "started": "Started",
+ "stopped": "Stopped"
+ }
+ }
+}
diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/es.json b/noctalia/.config/plugins/privacy-indicator/i18n/es.json
new file mode 100644
index 0000000..93a48a7
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/i18n/es.json
@@ -0,0 +1,46 @@
+{
+ "settings": {
+ "hideInactive": {
+ "desc": "Ocultar los iconos de micrófono, cámara y pantalla cuando estén inactivos.",
+ "label": "Ocultar estados inactivos"
+ },
+ "iconSpacing": {
+ "desc": "Configurar el espacio entre los iconos.",
+ "label": "Espaciado de iconos"
+ },
+ "removeMargins": {
+ "desc": "Quita todos los márgenes externos del widget.",
+ "label": "Quitar márgenes"
+ },
+ "activeColor": {
+ "desc": "Color de los iconos cuando están activos.",
+ "label": "Color de icono activo"
+ },
+ "inactiveColor": {
+ "desc": "Color de los iconos cuando están inactivos.",
+ "label": "Color de icono inactivo"
+ }
+ },
+ "tooltip": {
+ "cam-on": "Cámara: {apps}",
+ "mic-on": "Micrófono: {apps}",
+ "screen-on": "Compartir pantalla: {apps}"
+ },
+ "toast": {
+ "cam-on": "La cámara está activa",
+ "mic-on": "El micrófono está activo",
+ "screen-on": "Compartir pantalla está activo"
+ },
+ "menu": {
+ "settings": "Configuración del widget"
+ },
+ "history": {
+ "title": "Historial de acceso",
+ "empty": "Sin accesos recientes",
+ "clear": "Borrar",
+ "action": {
+ "started": "Iniciado",
+ "stopped": "Detenido"
+ }
+ }
+} \ No newline at end of file
diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/fr.json b/noctalia/.config/plugins/privacy-indicator/i18n/fr.json
new file mode 100644
index 0000000..814c07d
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/i18n/fr.json
@@ -0,0 +1,58 @@
+{
+ "menu": {
+ "settings": "Paramètres du widget"
+ },
+ "settings": {
+ "activeColor": {
+ "desc": "Couleur des icônes lorsqu'elles sont actives.",
+ "label": "Couleur d'icône active"
+ },
+ "hideInactive": {
+ "desc": "Masquer les icônes du micro, de la caméra et de l’écran lorsqu’ils sont inactifs.",
+ "label": "Masquer les états inactifs"
+ },
+ "enableToast": {
+ "desc": "Afficher des messages toast lorsque l'un des états change.",
+ "label": "Activer les notifications toast"
+ },
+ "inactiveColor": {
+ "desc": "Couleur des icônes lorsqu'elles sont inactives.",
+ "label": "Couleur d'icône inactive"
+ },
+ "iconSpacing": {
+ "desc": "Définir l’espacement entre les icônes.",
+ "label": "Espacement des icônes"
+ },
+ "removeMargins": {
+ "desc": "Supprime toutes les marges extérieures du widget.",
+ "label": "Supprimer les marges"
+ },
+ "micFilterRegex": {
+ "desc": "Motif Regex pour filtrer les applications de microphone. Les applications correspondantes sont complètement exclues de la détection.",
+ "label": "Regex de filtrage du microphone"
+ },
+ "camFilterRegex": {
+ "desc": "Motif Regex pour filtrer les applications de caméra. Les applications correspondantes sont complètement exclues de la détection.",
+ "label": "Regex de filtrage de la caméra"
+ }
+ },
+ "tooltip": {
+ "cam-on": "Caméra: {apps}",
+ "mic-on": "Microphone: {apps}",
+ "screen-on": "Partage d'écran: {apps}"
+ },
+ "toast": {
+ "cam-on": "La caméra est active",
+ "mic-on": "Le microphone est actif",
+ "screen-on": "Le partage d'écran est actif"
+ },
+ "history": {
+ "title": "Historique d'accès",
+ "empty": "Aucun accès récent",
+ "clear": "Effacer",
+ "action": {
+ "started": "Démarré",
+ "stopped": "Arrêté"
+ }
+ }
+}
diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/hu.json b/noctalia/.config/plugins/privacy-indicator/i18n/hu.json
new file mode 100644
index 0000000..4a7ba8c
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/i18n/hu.json
@@ -0,0 +1,46 @@
+{
+ "settings": {
+ "hideInactive": {
+ "desc": "Mikrofon, kamera és képernyő ikonok elrejtése, ha éppen nincsenek használatban.",
+ "label": "Inaktív állapotok elrejtése"
+ },
+ "iconSpacing": {
+ "desc": "Állítsa be az ikonok közötti távolságot.",
+ "label": "Ikon távolság"
+ },
+ "removeMargins": {
+ "desc": "Távolítsd el a widget összes külső margóját.",
+ "label": "Margók eltávolítása"
+ },
+ "activeColor": {
+ "desc": "Az ikonok színe, amikor aktívak.",
+ "label": "Aktív ikon színe"
+ },
+ "inactiveColor": {
+ "desc": "Az ikonok színe, amikor inaktívak.",
+ "label": "Inaktív ikon színe"
+ }
+ },
+ "tooltip": {
+ "cam-on": "Kamera: {apps}",
+ "mic-on": "Mikrofon: {apps}",
+ "screen-on": "Képernyőmegosztás: {apps}"
+ },
+ "toast": {
+ "cam-on": "A kamera aktív",
+ "mic-on": "A mikrofon aktív",
+ "screen-on": "Képernyőmegosztás aktív"
+ },
+ "menu": {
+ "settings": "Widget beállítások"
+ },
+ "history": {
+ "title": "Hozzáférési előzmények",
+ "empty": "Nincs nemrégiben történt hozzáférés",
+ "clear": "Törlés",
+ "action": {
+ "started": "Elindítva",
+ "stopped": "Leállítva"
+ }
+ }
+} \ No newline at end of file
diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/it.json b/noctalia/.config/plugins/privacy-indicator/i18n/it.json
new file mode 100644
index 0000000..d0eee79
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/i18n/it.json
@@ -0,0 +1,46 @@
+{
+ "settings": {
+ "hideInactive": {
+ "desc": "Nascondi le icone di microfono, fotocamera e schermo quando sono inattive.",
+ "label": "Nascondi stati inattivi"
+ },
+ "iconSpacing": {
+ "desc": "Imposta la distanza tra le icone.",
+ "label": "Spaziatura delle icone"
+ },
+ "removeMargins": {
+ "desc": "Rimuove tutti i margini esterni del widget.",
+ "label": "Rimuovi margini"
+ },
+ "activeColor": {
+ "desc": "Colore delle icone quando sono attive.",
+ "label": "Colore icona attiva"
+ },
+ "inactiveColor": {
+ "desc": "Colore delle icone quando sono inattive.",
+ "label": "Colore icona inattiva"
+ }
+ },
+ "tooltip": {
+ "cam-on": "Kamera: {apps}",
+ "mic-on": "Mikrofonoa: {apps}",
+ "screen-on": "Ekran-partaĝado: {apps}"
+ },
+ "toast": {
+ "cam-on": "La fotocamera è attiva",
+ "mic-on": "Il microfono è attivo",
+ "screen-on": "La condivisione dello schermo è attiva"
+ },
+ "menu": {
+ "settings": "Impostazioni widget"
+ },
+ "history": {
+ "title": "Cronologia accessi",
+ "empty": "Nessun accesso recente",
+ "clear": "Pulisci",
+ "action": {
+ "started": "Iniziato",
+ "stopped": "Terminato"
+ }
+ }
+} \ No newline at end of file
diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/ja.json b/noctalia/.config/plugins/privacy-indicator/i18n/ja.json
new file mode 100644
index 0000000..ba61aeb
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/i18n/ja.json
@@ -0,0 +1,46 @@
+{
+ "settings": {
+ "hideInactive": {
+ "desc": "マイク・カメラ・画面共有のアイコンを、非アクティブなときは非表示にします。",
+ "label": "非アクティブ状態を非表示"
+ },
+ "iconSpacing": {
+ "desc": "アイコン同士の間隔を設定します。",
+ "label": "アイコン間隔"
+ },
+ "removeMargins": {
+ "desc": "ウィジェットの外側の余白をすべて削除します。",
+ "label": "余白を削除"
+ },
+ "activeColor": {
+ "desc": "アクティブ時のアイコンの色。",
+ "label": "アクティブ時の色"
+ },
+ "inactiveColor": {
+ "desc": "非アクティブ時のアイコンの色。",
+ "label": "非アクティブ時の色"
+ }
+ },
+ "tooltip": {
+ "cam-on": "カメラ: {apps}",
+ "mic-on": "マイク: {apps}",
+ "screen-on": "画面共有: {apps}"
+ },
+ "toast": {
+ "cam-on": "カメラがアクティブです",
+ "mic-on": "マイクがアクティブです",
+ "screen-on": "画面共有がアクティブです"
+ },
+ "menu": {
+ "settings": "ウィジェット設定"
+ },
+ "history": {
+ "title": "アクセス履歴",
+ "empty": "最近のアクセスはありません",
+ "clear": "クリア",
+ "action": {
+ "started": "開始",
+ "stopped": "停止"
+ }
+ }
+} \ No newline at end of file
diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/ko.json b/noctalia/.config/plugins/privacy-indicator/i18n/ko.json
new file mode 100644
index 0000000..e104202
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/i18n/ko.json
@@ -0,0 +1,58 @@
+{
+ "menu": {
+ "settings": "위젯 설정"
+ },
+ "settings": {
+ "activeColor": {
+ "desc": "활성 상태일 때 아이콘에 적용되는 색상입니다.",
+ "label": "활성 아이콘 색상"
+ },
+ "hideInactive": {
+ "desc": "비활성화된 상태의 마이크, 카메라, 화면 아이콘을 숨깁니다.",
+ "label": "비활성 상태 숨기기"
+ },
+ "enableToast": {
+ "desc": "상태가 변경될 때 토스트 알림을 표시합니다.",
+ "label": "토스트 알림 활성화"
+ },
+ "inactiveColor": {
+ "desc": "비활성 상태일 때 아이콘에 적용되는 색상입니다.",
+ "label": "비활성 아이콘 색상"
+ },
+ "iconSpacing": {
+ "desc": "아이콘 사이의 간격을 설정합니다.",
+ "label": "아이콘 간격"
+ },
+ "removeMargins": {
+ "desc": "위젯의 모든 외곽 여백을 제거합니다.",
+ "label": "여백 제거"
+ },
+ "micFilterRegex": {
+ "desc": "마이크를 사용하는 애플리케이션을 필터링하기 위한 정규식을 지정합니다. 일치하는 애플리케이션은 상태 감지에서 완전히 제외됩니다.",
+ "label": "마이크 필터 정규식"
+ },
+ "camFilterRegex": {
+ "desc": "카메라를 사용하는 애플리케이션을 필터링하기 위한 정규식을 지정합니다. 일치하는 애플리케이션은 상태 감지에서 완전히 제외됩니다.",
+ "label": "카메라 필터 정규식"
+ }
+ },
+ "tooltip": {
+ "cam-on": "카메라: {apps}",
+ "mic-on": "마이크: {apps}",
+ "screen-on": "화면 공유: {apps}"
+ },
+ "toast": {
+ "cam-on": "카메라가 활성화되었습니다.",
+ "mic-on": "마이크가 활성화되었습니다.",
+ "screen-on": "화면 공유가 활성화되었습니다."
+ },
+ "history": {
+ "title": "접근 기록",
+ "empty": "최근에 접근한 기록 없음",
+ "clear": "비우기",
+ "action": {
+ "started": "시작됨",
+ "stopped": "정지됨"
+ }
+ }
+}
diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/ku.json b/noctalia/.config/plugins/privacy-indicator/i18n/ku.json
new file mode 100644
index 0000000..6a58f84
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/i18n/ku.json
@@ -0,0 +1,46 @@
+{
+ "settings": {
+ "hideInactive": {
+ "desc": "Îkonên mîkrofon, kamera û ekranê dema ku neçalak bin veşêre.",
+ "label": "Rewşa neçalak veşêre"
+ },
+ "iconSpacing": {
+ "desc": "Cihê di navbera îkonan de diyar bike.",
+ "label": "Dûrahiya îkonan"
+ },
+ "removeMargins": {
+ "desc": "Hemû marjînalên derveyî yên widgetê rake.",
+ "label": "Derdestên derxînin"
+ },
+ "activeColor": {
+ "desc": "Color of the icons when active.",
+ "label": "Active icon color"
+ },
+ "inactiveColor": {
+ "desc": "Color of the icons when inactive.",
+ "label": "Inactive icon color"
+ }
+ },
+ "tooltip": {
+ "cam-on": "Kamera: {apps}",
+ "mic-on": "Mîkrofon: {apps}",
+ "screen-on": "Parvekirina ekranê: {apps}"
+ },
+ "toast": {
+ "cam-on": "Kamera çalak e",
+ "mic-on": "Mîkrofon çalak e",
+ "screen-on": "Parvekirina ekranê çalak e"
+ },
+ "menu": {
+ "settings": "Widget settings"
+ },
+ "history": {
+ "title": "Access History",
+ "empty": "No recent access",
+ "clear": "Clear",
+ "action": {
+ "started": "Started",
+ "stopped": "Stopped"
+ }
+ }
+} \ No newline at end of file
diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/nl.json b/noctalia/.config/plugins/privacy-indicator/i18n/nl.json
new file mode 100644
index 0000000..d0b031d
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/i18n/nl.json
@@ -0,0 +1,46 @@
+{
+ "settings": {
+ "hideInactive": {
+ "desc": "Verberg de pictogrammen voor microfoon, camera en scherm wanneer ze inactief zijn.",
+ "label": "Inactieve status verbergen"
+ },
+ "iconSpacing": {
+ "desc": "Stel de afstand tussen de pictogrammen in.",
+ "label": "Pictogramafstand"
+ },
+ "removeMargins": {
+ "desc": "Verwijdert alle buitenste marges van de widget.",
+ "label": "Marges verwijderen"
+ },
+ "activeColor": {
+ "desc": "Kleur van de pictogrammen wanneer ze actief zijn.",
+ "label": "Actieve pictogramkleur"
+ },
+ "inactiveColor": {
+ "desc": "Kleur van de pictogrammen wanneer ze inactief zijn.",
+ "label": "Inactieve pictogramkleur"
+ }
+ },
+ "tooltip": {
+ "cam-on": "Camera: {apps}",
+ "mic-on": "Microfoon: {apps}",
+ "screen-on": "Schermdeling: {apps}"
+ },
+ "toast": {
+ "cam-on": "Camera is actief",
+ "mic-on": "Microfoon is actief",
+ "screen-on": "Scherm delen is actief"
+ },
+ "menu": {
+ "settings": "Widget instellingen"
+ },
+ "history": {
+ "title": "Toegangsgeschiedenis",
+ "empty": "Geen recente toegang",
+ "clear": "Wissen",
+ "action": {
+ "started": "Gestart",
+ "stopped": "Gestopt"
+ }
+ }
+} \ No newline at end of file
diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/pl.json b/noctalia/.config/plugins/privacy-indicator/i18n/pl.json
new file mode 100644
index 0000000..9e05d3a
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/i18n/pl.json
@@ -0,0 +1,46 @@
+{
+ "settings": {
+ "hideInactive": {
+ "desc": "Ukryj ikony mikrofonu, kamery i ekranu, gdy są nieaktywne.",
+ "label": "Ukryj nieaktywne stany"
+ },
+ "iconSpacing": {
+ "desc": "Ustaw odstęp między ikonami.",
+ "label": "Odstępy ikon"
+ },
+ "removeMargins": {
+ "desc": "Usuń wszystkie zewnętrzne marginesy widżetu.",
+ "label": "Usuń marginesy"
+ },
+ "activeColor": {
+ "desc": "Kolor ikon, gdy są aktywne.",
+ "label": "Kolor aktywnej ikony"
+ },
+ "inactiveColor": {
+ "desc": "Kolor ikon, gdy są nieaktywne.",
+ "label": "Kolor nieaktywnej ikony"
+ }
+ },
+ "tooltip": {
+ "cam-on": "Kamera: {apps}",
+ "mic-on": "Mikrofon: {apps}",
+ "screen-on": "Udostępnianie ekranu: {apps}"
+ },
+ "toast": {
+ "cam-on": "Kamera jest aktywna",
+ "mic-on": "Mikrofon jest aktywny",
+ "screen-on": "Udostępnianie ekranu jest aktywne"
+ },
+ "menu": {
+ "settings": "Ustawienia widżetu"
+ },
+ "history": {
+ "title": "Historia dostępu",
+ "empty": "Brak ostatnich dostępów",
+ "clear": "Wyczyść",
+ "action": {
+ "started": "Rozpoczęto",
+ "stopped": "Zatrzymano"
+ }
+ }
+} \ No newline at end of file
diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/pt.json b/noctalia/.config/plugins/privacy-indicator/i18n/pt.json
new file mode 100644
index 0000000..068a528
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/i18n/pt.json
@@ -0,0 +1,46 @@
+{
+ "settings": {
+ "hideInactive": {
+ "desc": "Oculta os ícones de microfone, câmera e tela quando estiverem inativos.",
+ "label": "Ocultar estados inativos"
+ },
+ "iconSpacing": {
+ "desc": "Define o espaçamento entre os ícones.",
+ "label": "Espaçamento dos ícones"
+ },
+ "removeMargins": {
+ "desc": "Remove todas as margens externas do widget.",
+ "label": "Remover margens"
+ },
+ "activeColor": {
+ "desc": "Cor dos ícones quando ativos.",
+ "label": "Cor do ícone ativo"
+ },
+ "inactiveColor": {
+ "desc": "Cor dos ícones quando inativos.",
+ "label": "Cor do ícone inativo"
+ }
+ },
+ "tooltip": {
+ "cam-on": "Câmera: {apps}",
+ "mic-on": "Microfone: {apps}",
+ "screen-on": "Compartilhamento de tela: {apps}"
+ },
+ "toast": {
+ "cam-on": "A câmera está ativa",
+ "mic-on": "O microfone está ativo",
+ "screen-on": "O compartilhamento de tela está ativo"
+ },
+ "menu": {
+ "settings": "Configurações do widget"
+ },
+ "history": {
+ "title": "Histórico de acesso",
+ "empty": "Sem acessos recentes",
+ "clear": "Limpar",
+ "action": {
+ "started": "Iniciado",
+ "stopped": "Interrompido"
+ }
+ }
+} \ No newline at end of file
diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/ru.json b/noctalia/.config/plugins/privacy-indicator/i18n/ru.json
new file mode 100644
index 0000000..152e7d8
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/i18n/ru.json
@@ -0,0 +1,46 @@
+{
+ "settings": {
+ "hideInactive": {
+ "desc": "Скрывать значки микрофона, камеры и экрана, когда они неактивны.",
+ "label": "Скрывать неактивные состояния"
+ },
+ "iconSpacing": {
+ "desc": "Задать расстояние между значками.",
+ "label": "Интервал между значками"
+ },
+ "removeMargins": {
+ "desc": "Удаляет все внешние отступы виджета.",
+ "label": "Убрать отступы"
+ },
+ "activeColor": {
+ "desc": "Цвет иконок при активации.",
+ "label": "Цвет активной иконки"
+ },
+ "inactiveColor": {
+ "desc": "Цвет иконок в спокойном состоянии.",
+ "label": "Цвет неактивной иконки"
+ }
+ },
+ "tooltip": {
+ "cam-on": "Камера: {apps}",
+ "mic-on": "Микрофон: {apps}",
+ "screen-on": "Демонстрация экрана: {apps}"
+ },
+ "toast": {
+ "cam-on": "Камера активна",
+ "mic-on": "Микрофон активен",
+ "screen-on": "Демонстрация экрана активна"
+ },
+ "menu": {
+ "settings": "Настройки виджета"
+ },
+ "history": {
+ "title": "История доступа",
+ "empty": "Нет недавних доступов",
+ "clear": "Очистить",
+ "action": {
+ "started": "Начато",
+ "stopped": "Остановлено"
+ }
+ }
+} \ No newline at end of file
diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/tr.json b/noctalia/.config/plugins/privacy-indicator/i18n/tr.json
new file mode 100644
index 0000000..016f1be
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/i18n/tr.json
@@ -0,0 +1,46 @@
+{
+ "settings": {
+ "hideInactive": {
+ "desc": "Mikrofon, kamera ve ekran simgelerini pasif olduklarında gizle.",
+ "label": "Pasif durumları gizle"
+ },
+ "iconSpacing": {
+ "desc": "Simgeler arasındaki boşluğu ayarla.",
+ "label": "Simge aralığı"
+ },
+ "removeMargins": {
+ "desc": "Widget’ın tüm dış kenar boşluklarını kaldırır.",
+ "label": "Kenarlıkları kaldır"
+ },
+ "activeColor": {
+ "desc": "Aktif olduğunda simgelerin rengi.",
+ "label": "Aktif simge rengi"
+ },
+ "inactiveColor": {
+ "desc": "Devre dışı olduğunda simgelerin rengi.",
+ "label": "Pasif simge rengi"
+ }
+ },
+ "tooltip": {
+ "cam-on": "Kamera: {apps}",
+ "mic-on": "Mikrofon: {apps}",
+ "screen-on": "Ekran paylaşımı: {apps}"
+ },
+ "toast": {
+ "cam-on": "Kamera aktif",
+ "mic-on": "Mikrofon aktif",
+ "screen-on": "Ekran paylaşımı aktif"
+ },
+ "menu": {
+ "settings": "Bileşen ayarları"
+ },
+ "history": {
+ "title": "Erişim Geçmişi",
+ "empty": "Yakın zamanda erişim yok",
+ "clear": "Temizle",
+ "action": {
+ "started": "Başlatıldı",
+ "stopped": "Durduruldu"
+ }
+ }
+} \ No newline at end of file
diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/uk-UA.json b/noctalia/.config/plugins/privacy-indicator/i18n/uk-UA.json
new file mode 100644
index 0000000..5b872a7
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/i18n/uk-UA.json
@@ -0,0 +1,46 @@
+{
+ "settings": {
+ "hideInactive": {
+ "desc": "Приховувати значки мікрофона, камери та екрана, коли вони неактивні.",
+ "label": "Приховувати неактивні стани"
+ },
+ "iconSpacing": {
+ "desc": "Встановити відстань між значками.",
+ "label": "Інтервал між значками"
+ },
+ "removeMargins": {
+ "desc": "Видаляє всі зовнішні відступи віджета.",
+ "label": "Прибрати відступи"
+ },
+ "activeColor": {
+ "desc": "Колір іконок при активації.",
+ "label": "Колір активної іконки"
+ },
+ "inactiveColor": {
+ "desc": "Колір іконок у спокійному стані.",
+ "label": "Колір неактивної іконки"
+ }
+ },
+ "tooltip": {
+ "cam-on": "Камера: {apps}",
+ "mic-on": "Мікрофон: {apps}",
+ "screen-on": "Демонстрація екрана: {apps}"
+ },
+ "toast": {
+ "cam-on": "Камера активна",
+ "mic-on": "Мікрофон активний",
+ "screen-on": "Демонстрація екрана активна"
+ },
+ "menu": {
+ "settings": "Налаштування віджета"
+ },
+ "history": {
+ "title": "Історія доступу",
+ "empty": "Немає недавніх доступів",
+ "clear": "Очистити",
+ "action": {
+ "started": "Розпочато",
+ "stopped": "Зупинено"
+ }
+ }
+} \ No newline at end of file
diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/vi.json b/noctalia/.config/plugins/privacy-indicator/i18n/vi.json
new file mode 100644
index 0000000..6cd6d76
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/i18n/vi.json
@@ -0,0 +1,54 @@
+{
+ "menu": {
+ "settings": "Cài đặt tiện ích"
+ },
+ "settings": {
+ "activeColor": {
+ "desc": "Màu của biểu tượng khi đang hoạt động.",
+ "label": "Màu biểu tượng khi hoạt động"
+ },
+ "hideInactive": {
+ "desc": "Ẩn biểu tượng micro, camera và màn hình khi không hoạt động.",
+ "label": "Ẩn trạng thái không hoạt động"
+ },
+ "enableToast": {
+ "desc": "Hiển thị thông báo khi một trạng thái thay đổi.",
+ "label": "Bật thông báo"
+ },
+ "inactiveColor": {
+ "desc": "Màu của biểu tượng khi không hoạt động.",
+ "label": "Màu biểu tượng khi không hoạt động"
+ },
+ "iconSpacing": {
+ "desc": "Thiết lập khoảng cách giữa các biểu tượng.",
+ "label": "Khoảng cách biểu tượng"
+ },
+ "removeMargins": {
+ "desc": "Loại bỏ toàn bộ lề ngoài của widget.",
+ "label": "Xóa lề"
+ },
+ "micFilterRegex": {
+ "desc": "Biểu thức chính quy để lọc các ứng dụng sử dụng micro. Các ứng dụng khớp sẽ bị loại khỏi việc phát hiện.",
+ "label": "Regex lọc micro"
+ }
+ },
+ "tooltip": {
+ "cam-on": "Camera: {apps}",
+ "mic-on": "Micro: {apps}",
+ "screen-on": "Chia sẻ màn hình: {apps}"
+ },
+ "toast": {
+ "cam-on": "Camera đang hoạt động",
+ "mic-on": "Micro đang hoạt động",
+ "screen-on": "Chia sẻ màn hình đang hoạt động"
+ },
+ "history": {
+ "title": "Lịch sử truy cập",
+ "empty": "Không có truy cập gần đây",
+ "clear": "Xóa",
+ "action": {
+ "started": "Bắt đầu",
+ "stopped": "Dừng"
+ }
+ }
+}
diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/zh-CN.json b/noctalia/.config/plugins/privacy-indicator/i18n/zh-CN.json
new file mode 100644
index 0000000..5665d78
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/i18n/zh-CN.json
@@ -0,0 +1,46 @@
+{
+ "settings": {
+ "hideInactive": {
+ "desc": "在麦克风、摄像头和屏幕图标处于非活动状态时将其隐藏。",
+ "label": "隐藏非活动状态"
+ },
+ "iconSpacing": {
+ "desc": "设置图标之间的间距。",
+ "label": "图标间距"
+ },
+ "removeMargins": {
+ "desc": "移除小部件所有外部边距。",
+ "label": "移除边距"
+ },
+ "activeColor": {
+ "desc": "激活图标的颜色。",
+ "label": "激活图标颜色"
+ },
+ "inactiveColor": {
+ "desc": "未激活图标的颜色。",
+ "label": "未激活图标颜色"
+ }
+ },
+ "tooltip": {
+ "cam-on": "摄像头: {apps}",
+ "mic-on": "麦克风: {apps}",
+ "screen-on": "屏幕共享: {apps}"
+ },
+ "toast": {
+ "cam-on": "摄像头已激活",
+ "mic-on": "麦克风已激活",
+ "screen-on": "屏幕共享已激活"
+ },
+ "menu": {
+ "settings": "小部件设置"
+ },
+ "history": {
+ "title": "访问历史",
+ "empty": "无最近访问",
+ "clear": "清除",
+ "action": {
+ "started": "已开始",
+ "stopped": "已停止"
+ }
+ }
+} \ No newline at end of file
diff --git a/noctalia/.config/plugins/privacy-indicator/i18n/zh-TW.json b/noctalia/.config/plugins/privacy-indicator/i18n/zh-TW.json
new file mode 100644
index 0000000..febeadc
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/i18n/zh-TW.json
@@ -0,0 +1,46 @@
+{
+ "settings": {
+ "hideInactive": {
+ "desc": "當麥克風, 攝影機及螢幕分享沒有啟動時就直接隱藏",
+ "label": "隱藏未啟動的狀態"
+ },
+ "iconSpacing": {
+ "desc": "設定圖示之間的留空",
+ "label": "圖示間距"
+ },
+ "removeMargins": {
+ "desc": "移除小工具外面的所有邊距",
+ "label": "移除邊距"
+ },
+ "activeColor": {
+ "desc": "圖示啟用時的顏色。",
+ "label": "啟用圖示顏色"
+ },
+ "inactiveColor": {
+ "desc": "圖示未啟用時的顏色。",
+ "label": "未啟用圖示顏色"
+ }
+ },
+ "tooltip": {
+ "cam-on": "攝影機: {apps}",
+ "mic-on": "麥克風: {apps}",
+ "screen-on": "螢幕分享: {apps}"
+ },
+ "toast": {
+ "cam-on": "攝影機已啟用",
+ "mic-on": "麥克風已啟用",
+ "screen-on": "螢幕分享已啟用"
+ },
+ "menu": {
+ "settings": "小工具設定"
+ },
+ "history": {
+ "title": "存取紀錄",
+ "empty": "無最近存取",
+ "clear": "清除",
+ "action": {
+ "started": "已開始",
+ "stopped": "已停止"
+ }
+ }
+} \ No newline at end of file
diff --git a/noctalia/.config/plugins/privacy-indicator/manifest.json b/noctalia/.config/plugins/privacy-indicator/manifest.json
new file mode 100644
index 0000000..4618fc4
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/manifest.json
@@ -0,0 +1,37 @@
+{
+ "id": "privacy-indicator",
+ "name": "Privacy Indicator",
+ "version": "1.2.13",
+ "minNoctaliaVersion": "3.6.0",
+ "author": "Noctalia Team",
+ "official": true,
+ "license": "MIT",
+ "repository": "https://github.com/noctalia-dev/noctalia-plugins",
+ "description": "A privacy indicator widget that shows when microphone, camera or screen sharing is active.",
+ "tags": [
+ "Bar",
+ "Privacy",
+ "Indicator"
+ ],
+ "entryPoints": {
+ "main": "Main.qml",
+ "barWidget": "BarWidget.qml",
+ "panel": "Panel.qml",
+ "settings": "Settings.qml"
+ },
+ "dependencies": {
+ "plugins": []
+ },
+ "metadata": {
+ "defaultSettings": {
+ "hideInactive": false,
+ "enableToast": true,
+ "removeMargins": false,
+ "iconSpacing": 4,
+ "activeColor": "primary",
+ "inactiveColor": "none",
+ "micFilterRegex": "",
+ "camFilterRegex": ""
+ }
+ }
+}
diff --git a/noctalia/.config/plugins/privacy-indicator/preview.png b/noctalia/.config/plugins/privacy-indicator/preview.png
new file mode 100644
index 0000000..0db7b7c
--- /dev/null
+++ b/noctalia/.config/plugins/privacy-indicator/preview.png
Binary files differ