This commit is contained in:
2026-02-17 21:43:15 -06:00
parent e9f3ab00a7
commit 1432d75bb6
22 changed files with 1088 additions and 243 deletions

View File

@@ -2,10 +2,7 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Services.UI
import qs.Widgets
@@ -26,12 +23,15 @@ Item {
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)
property bool micActive: false
property bool camActive: false
property bool scrActive: false
property var micApps: []
property var camApps: []
property var scrApps: []
// 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 || ({})
@@ -63,156 +63,6 @@ Item {
visible: root.isVisible
opacity: root.isVisible ? 1.0 : 0.0
PwObjectTracker {
objects: Pipewire.ready ? Pipewire.nodes.values : []
}
Process {
id: cameraCheckProcess
running: false
command: ["sh", "-c", "for dev in /dev/video*; do [ -e \"$dev\" ] && [ -n \"$(find /proc/[0-9]*/fd/ -lname \"$dev\" 2>/dev/null | head -n1)\" ] && echo \"active\" && exit 0; done; exit 1"]
onExited: (code, status) => {
var isActive = code === 0;
root.camActive = isActive;
if (isActive) {
cameraAppsProcess.running = true;
} else {
root.camApps = [];
}
}
}
Process {
id: cameraAppsProcess
running: false
command: ["sh", "-c", "for dev in /dev/video*; do [ -e \"$dev\" ] && for fd in /proc/[0-9]*/fd/*; do [ -L \"$fd\" ] && [ \"$(readlink \"$fd\" 2>/dev/null)\" = \"$dev\" ] && ps -p \"$(echo \"$fd\" | cut -d/ -f3)\" -o comm= 2>/dev/null; done; done | sort -u | tr '\\n' ',' | sed 's/,$//'"]
onExited: (code, status) => {
if (stdout) {
var appsString = stdout.trim();
var apps = appsString.length > 0 ? appsString.split(',') : [];
root.camApps = apps;
} else {
root.camApps = [];
}
}
}
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() {
cameraCheckProcess.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);
}
function buildTooltip() {
var parts = [];
@@ -301,12 +151,14 @@ Item {
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
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);
}
}

View 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;
}
}

View File

@@ -0,0 +1,169 @@
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
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") || "Access History"
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") || "No recent access"
color: Qt.alpha(Color.mOnSurface, 0.5)
pointSize: Style.fontSizeM
Layout.topMargin: Style.marginL
}
Item { Layout.fillHeight: true } // spacer
}
}
}
}
}
}

View File

@@ -11,11 +11,36 @@
"removeMargins": {
"desc": "Alle äußeren Ränder des Widgets entfernen.",
"label": "Ränder entfernen"
},
"activeColor": {
"desc": "Farbe der Symbole, wenn sie aktiv sind.",
"label": "Aktive Farbsymbol"
},
"inactiveColor": {
"desc": "Farbe der Symbole, wenn sie inaktiv sind.",
"label": "Inaktive Farbsymbol"
}
},
"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"
},
"menu": {
"settings": "Widget-Einstellungen"
},
"history": {
"title": "Zugriffsverlauf",
"empty": "Kein kürzlicher Zugriff",
"clear": "Leeren",
"action": {
"started": "Gestartet",
"stopped": "Beendet"
}
}
}
}

View File

@@ -28,5 +28,19 @@
"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"
}
}
}
}

View File

@@ -11,11 +11,36 @@
"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"
}
}
}
}

View File

@@ -11,11 +11,36 @@
"removeMargins": {
"desc": "Supprime toutes les marges extérieures du widget.",
"label": "Supprimer les marges"
},
"activeColor": {
"desc": "Couleur des icônes lorsqu'elles sont actives.",
"label": "Couleur d'icône active"
},
"inactiveColor": {
"desc": "Couleur des icônes lorsqu'elles sont inactives.",
"label": "Couleur d'icône inactive"
}
},
"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"
},
"menu": {
"settings": "Paramètres du widget"
},
"history": {
"title": "Historique d'accès",
"empty": "Aucun accès récent",
"clear": "Effacer",
"action": {
"started": "Démarré",
"stopped": "Arrêté"
}
}
}
}

View File

@@ -11,11 +11,36 @@
"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"
}
}
}
}

View File

@@ -11,11 +11,36 @@
"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"
}
}
}
}

View File

@@ -11,11 +11,36 @@
"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": "停止"
}
}
}
}

View File

@@ -11,11 +11,36 @@
"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"
}
}
}
}

View File

@@ -11,11 +11,36 @@
"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"
}
}
}
}

View File

@@ -11,11 +11,36 @@
"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"
}
}
}
}

View File

@@ -11,11 +11,36 @@
"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"
}
}
}
}

View File

@@ -11,11 +11,36 @@
"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": "Остановлено"
}
}
}
}

View File

@@ -11,11 +11,36 @@
"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"
}
}
}
}

View File

@@ -11,11 +11,36 @@
"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": "Зупинено"
}
}
}
}

View File

@@ -11,11 +11,36 @@
"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": "已停止"
}
}
}
}

View File

@@ -11,11 +11,36 @@
"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": "已停止"
}
}
}
}

View File

@@ -1,16 +1,22 @@
{
"id": "privacy-indicator",
"name": "Privacy Indicator",
"version": "1.1.0",
"version": "1.2.4",
"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"],
"tags": [
"Bar",
"Privacy",
"Indicator"
],
"entryPoints": {
"main": "Main.qml",
"barWidget": "BarWidget.qml",
"panel": "Panel.qml",
"settings": "Settings.qml"
},
"dependencies": {
@@ -24,4 +30,4 @@
"inactiveColor": "none"
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 26 KiB