diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 0f8ae3e9..15dc8837 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -456,6 +456,8 @@ "enable-bluetooth": "Bluetooth aktivieren", "enable-dnd": "Nicht stören aktivieren", "enable-wifi": "WLAN aktivieren", + "connect-vpn": "Mit {name} verbinden", + "disconnect-vpn": "Verbindung zu {name} trennen", "next": "Nächste/r/s", "open-calendar": "Kalender öffnen", "open-display-settings": "Anzeigeeinstellungen", @@ -2074,6 +2076,10 @@ "disabled": "Deaktiviert", "disconnected": "Getrennt von '{ssid}'", "enabled": "Aktiviert" + }, + "vpn": { + "connected": "Verbunden mit '{name}'", + "disconnected": "Verbindung zu '{name}' getrennt" } }, "tooltips": { @@ -2097,6 +2103,7 @@ "keep-awake": "Wach halten", "keyboard-layout": "{layout} Tastaturlayout", "manage-wifi": "WLAN verwalten", + "manage-vpn": "VPN-Verbindungen verwalten", "microphone-volume-at": "Mikrofonlautstärke bei {volume}%.\nLinksklick für Einstellungen. Rechtsklick zum Stummschalten.\nScrollen zum Ändern der Lautstärke.", "move-to-center-section": "Zur mittleren Sektion verschieben", "move-to-left-section": "Zur linken Sektion verschieben", diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 6d8bb459..f315c8f8 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -456,6 +456,8 @@ "enable-bluetooth": "Enable Bluetooth", "enable-dnd": "Enable Do Not Disturb", "enable-wifi": "Enable Wi-Fi", + "connect-vpn": "Connect to {name}", + "disconnect-vpn": "Disconnect {name}", "next": "Next", "open-calendar": "Open calendar", "open-display-settings": "Display settings", @@ -2078,6 +2080,10 @@ "disabled": "Disabled", "disconnected": "Disconnected from '{ssid}'", "enabled": "Enabled" + }, + "vpn": { + "connected": "Connected to '{name}'", + "disconnected": "Disconnected from '{name}'" } }, "tooltips": { @@ -2101,6 +2107,7 @@ "keep-awake": "Keep awake", "keyboard-layout": "{layout} keyboard layout", "manage-wifi": "Manage Wi-Fi", + "manage-vpn": "Manage VPN connections", "microphone-volume-at": "Microphone volume at {volume}%\nScroll to modify volume", "move-to-center-section": "Move to center section", "move-to-left-section": "Move to left section", diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 52ffc128..6ad05e3e 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -456,6 +456,8 @@ "enable-bluetooth": "Activar Bluetooth", "enable-dnd": "Activar No molestar", "enable-wifi": "Activar Wi-Fi", + "connect-vpn": "Conectarse a {name}", + "disconnect-vpn": "Desconectar {name}", "next": "Siguiente", "open-calendar": "Abrir calendario", "open-display-settings": "Configuración de pantalla", @@ -2074,6 +2076,10 @@ "disabled": "Desactivado", "disconnected": "Desconectado de '{ssid}'", "enabled": "Activado" + }, + "vpn": { + "connected": "Conectado a '{name}'", + "disconnected": "Desconectado de '{name}'" } }, "tooltips": { @@ -2097,6 +2103,7 @@ "keep-awake": "Mantener despierto", "keyboard-layout": "Distribución de teclado {layout}", "manage-wifi": "Gestionar Wi-Fi", + "manage-vpn": "Administrar conexiones VPN", "microphone-volume-at": "Volumen del micrófono al {volume}%.\nClic izquierdo para ajustes. Clic derecho para activar/desactivar el silencio.\nDesplázate para modificar el volumen.", "move-to-center-section": "Mover a la sección central", "move-to-left-section": "Mover a la sección izquierda", diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index c8525648..c37c4782 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -456,6 +456,8 @@ "enable-bluetooth": "Activer le Bluetooth", "enable-dnd": "Activer le mode Ne pas déranger", "enable-wifi": "Activer le Wi-Fi", + "connect-vpn": "Se connecter à {name}", + "disconnect-vpn": "Se déconnecter de {name}", "next": "Suivant", "open-calendar": "Ouvrir le calendrier", "open-display-settings": "Paramètres d'affichage", @@ -2074,6 +2076,10 @@ "disabled": "Désactivé", "disconnected": "Déconnecté de '{ssid}'", "enabled": "Activé" + }, + "vpn": { + "connected": "Connecté à '{name}'", + "disconnected": "Déconnecté de '{name}'" } }, "tooltips": { @@ -2097,6 +2103,7 @@ "keep-awake": "Rester éveillé", "keyboard-layout": "Disposition du clavier {layout}", "manage-wifi": "Gérer le Wi-Fi", + "manage-vpn": "Gérer les connexions VPN", "microphone-volume-at": "Volume du microphone à {volume}%.\nClic gauche pour les paramètres. Clic droit pour activer/désactiver le mode muet.\nFaites défiler pour modifier le volume.", "move-to-center-section": "Déplacer vers la section centrale", "move-to-left-section": "Déplacer vers la section de gauche", diff --git a/Assets/Translations/nl.json b/Assets/Translations/nl.json index 43369044..d9d2bf18 100644 --- a/Assets/Translations/nl.json +++ b/Assets/Translations/nl.json @@ -456,6 +456,8 @@ "enable-bluetooth": "Bluetooth inschakelen", "enable-dnd": "Niet Storen inschakelen", "enable-wifi": "Wi-Fi inschakelen", + "connect-vpn": "Verbinding maken met {name}", + "disconnect-vpn": "Verbinding met {name} verbreken", "next": "Volgende", "open-calendar": "Open agenda", "open-display-settings": "Beeldscherminstellingen", @@ -2074,6 +2076,10 @@ "disabled": "Wi-Fi uitgeschakeld", "disconnected": "Verbinding met '{ssid}' verbroken", "enabled": "Wi-Fi ingeschakeld" + }, + "vpn": { + "connected": "Verbonden met '{name}'", + "disconnected": "Verbinding met '{name}' verbroken" } }, "tooltips": { @@ -2097,6 +2103,7 @@ "keep-awake": "Wakker houden", "keyboard-layout": "{layout}-toetsenbordindeling", "manage-wifi": "Wi-Fi beheren", + "manage-vpn": "VPN-verbindingen beheren", "microphone-volume-at": "Microfoonvolume {volume}%\nLinks klikken voor instellingen. Rechts klikken om te dempen.\nScroll om het volume aan te passen.", "move-to-center-section": "Verplaatsen naar middelste sectie", "move-to-left-section": "Verplaatsen naar linker sectie", diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index d4f24ea0..48e66034 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -456,6 +456,8 @@ "enable-bluetooth": "Ativar Bluetooth", "enable-dnd": "Ativar Não Perturbe", "enable-wifi": "Ativar Wi-Fi", + "connect-vpn": "Conectar-se a {name}", + "disconnect-vpn": "Desconectar {name}", "next": "Próximo(a)", "open-calendar": "Abrir calendário", "open-display-settings": "Configurações de exibição", @@ -2074,6 +2076,10 @@ "disabled": "Desativado", "disconnected": "Desconectado de '{ssid}'", "enabled": "Ativado" + }, + "vpn": { + "connected": "Conectado a '{name}'", + "disconnected": "Desconectado de '{name}'" } }, "tooltips": { @@ -2097,6 +2103,7 @@ "keep-awake": "Manter acordado", "keyboard-layout": "Layout de teclado {layout}", "manage-wifi": "Gerenciar Wi-Fi", + "manage-vpn": "Gerenciar conexões VPN", "microphone-volume-at": "Volume do microfone em {volume}%.\nClique esquerdo para configurações. Clique direito para ativar/desativar o mudo.\nRole para modificar o volume.", "move-to-center-section": "Mover para a seção central", "move-to-left-section": "Mover para a seção esquerda", diff --git a/Assets/Translations/ru.json b/Assets/Translations/ru.json index 722a1da0..5425db53 100644 --- a/Assets/Translations/ru.json +++ b/Assets/Translations/ru.json @@ -456,6 +456,8 @@ "enable-bluetooth": "Включить Bluetooth", "enable-dnd": "Не беспокоить", "enable-wifi": "Включить Wi-Fi", + "connect-vpn": "Подключиться к {name}", + "disconnect-vpn": "Отключить {name}", "next": "Следующий", "open-calendar": "Открыть календарь", "open-display-settings": "Настройки экрана", @@ -2074,6 +2076,10 @@ "disabled": "Отключен", "disconnected": "Отключено от '{ssid}'", "enabled": "Включен" + }, + "vpn": { + "connected": "Подключено к '{name}'", + "disconnected": "Отключено от '{name}'" } }, "tooltips": { @@ -2097,6 +2103,7 @@ "keep-awake": "Не засыпать", "keyboard-layout": "Раскладка клавиатуры {layout}", "manage-wifi": "Управление Wi-Fi", + "manage-vpn": "Управлять VPN-подключениями", "microphone-volume-at": "Громкость микрофона {volume}%\nЛевый клик для настроек. Правый клик для переключения заглушения.\nПрокрутка для изменения громкости.", "move-to-center-section": "Переместить в центральную секцию", "move-to-left-section": "Переместить в левую секцию", diff --git a/Assets/Translations/tr.json b/Assets/Translations/tr.json index 9ea3d368..44b48e41 100644 --- a/Assets/Translations/tr.json +++ b/Assets/Translations/tr.json @@ -456,6 +456,8 @@ "enable-bluetooth": "Bluetooth'u etkinleştir", "enable-dnd": "Rahatsız Etmeyin'i Etkinleştir", "enable-wifi": "Wi-Fi'ı etkinleştir", + "connect-vpn": "{name} bağlantısına bağlan", + "disconnect-vpn": "{name} bağlantısını kes", "next": "Sonraki", "open-calendar": "Takvimi aç", "open-display-settings": "Ekran ayarları", @@ -2074,6 +2076,10 @@ "disabled": "Devre dışı", "disconnected": "'{ssid}' bağlantısı kesildi", "enabled": "Etkin" + }, + "vpn": { + "connected": "'{name}' ile bağlantı kuruldu", + "disconnected": "'{name}' bağlantısı kesildi" } }, "tooltips": { @@ -2097,6 +2103,7 @@ "keep-awake": "Uyanık kal", "keyboard-layout": "{layout} klavye düzeni", "manage-wifi": "Wi-Fi yönet", + "manage-vpn": "VPN bağlantılarını yönet", "microphone-volume-at": "Mikrofon sesi %{volume}\nAyarlar için sol tık. Sessize almak için sağ tık.\nSesi değiştirmek için kaydırın.", "move-to-center-section": "Orta bölüme taşı", "move-to-left-section": "Sol bölüme taşı", diff --git a/Assets/Translations/uk-UA.json b/Assets/Translations/uk-UA.json index 369cdbbf..1bd3db7a 100644 --- a/Assets/Translations/uk-UA.json +++ b/Assets/Translations/uk-UA.json @@ -456,6 +456,8 @@ "enable-bluetooth": "Увімкнути Bluetooth", "enable-dnd": "Увімкнути режим \"Не турбувати\"", "enable-wifi": "Увімкнути Wi-Fi", + "connect-vpn": "Підключитися до {name}", + "disconnect-vpn": "Відключити {name}", "next": "Наступний", "open-calendar": "Відкрити календар", "open-display-settings": "Параметри дисплея", @@ -2074,6 +2076,10 @@ "disabled": "Вимкнено", "disconnected": "Відключено від '{ssid}'", "enabled": "Увімкнено" + }, + "vpn": { + "connected": "Підключено до '{name}'", + "disconnected": "Відключено від '{name}'" } }, "tooltips": { @@ -2097,6 +2103,7 @@ "keep-awake": "Не спати", "keyboard-layout": "Розкладка клавіатури {layout}", "manage-wifi": "Керувати Wi-Fi", + "manage-vpn": "Керувати підключеннями VPN", "microphone-volume-at": "Гучність мікрофона на {volume}%\nЛівий клік для налаштувань. Правий клік для вимкнення звуку.\nПрокрутка для зміни гучності.", "move-to-center-section": "Перемістити в центральну секцію", "move-to-left-section": "Перемістити в ліву секцію", diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index d09fb837..df272fbb 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -457,6 +457,8 @@ "enable-dnd": "启用勿扰模式", "enable-wifi": "启用 Wi-Fi", "next": "下一首", + "disconnect-vpn": "断开 {name}", + "connect-vpn": "连接 {name}", "open-calendar": "打开日历", "open-display-settings": "显示设置", "open-launcher": "打开启动器", @@ -2074,6 +2076,10 @@ "disabled": "已禁用", "disconnected": "已断开与 '{ssid}' 的连接", "enabled": "已启用" + }, + "vpn": { + "connected": "已连接到“{name}”", + "disconnected": "已断开与“{name}”的连接" } }, "tooltips": { @@ -2097,6 +2103,7 @@ "keep-awake": "保持唤醒", "keyboard-layout": "{layout} 键盘布局", "manage-wifi": "管理 Wi-Fi", + "manage-vpn": "管理 VPN 连接", "microphone-volume-at": "麦克风音量 {volume}%\n左键点击进入设置。右键点击切换静音。\n滚动滚轮调节音量。", "move-to-center-section": "移动到中央部分", "move-to-left-section": "移动到左侧部分", diff --git a/Modules/Bar/Widgets/VPN.qml b/Modules/Bar/Widgets/VPN.qml new file mode 100644 index 00000000..87a1c307 --- /dev/null +++ b/Modules/Bar/Widgets/VPN.qml @@ -0,0 +1,143 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Services.Networking +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right" + readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode + + implicitWidth: pill.width + implicitHeight: pill.height + + NPopupContextMenu { + id: contextMenu + + model: { + const items = []; + const active = VPNService.activeConnections; + for (let i = 0; i < active.length; ++i) { + const conn = active[i]; + items.push({ + "label": I18n.tr("context-menu.disconnect-vpn", { + "name": conn.name + }), + "action": "disconnect:" + conn.uuid, + "icon": "shield-off" + }); + } + const inactive = VPNService.inactiveConnections; + for (let i = 0; i < inactive.length; ++i) { + const conn = inactive[i]; + items.push({ + "label": I18n.tr("context-menu.connect-vpn", { + "name": conn.name + }), + "action": "connect:" + conn.uuid, + "icon": "shield-lock" + }); + } + items.push({ + "label": I18n.tr("context-menu.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }); + return items; + } + + onTriggered: action => { + var popupMenuWindow = PanelService.getPopupMenuWindow(screen); + if (popupMenuWindow) { + popupMenuWindow.close(); + } + if (!action) { + return; + } + if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + return; + } + if (action.startsWith("connect:")) { + const uuid = action.substring("connect:".length); + VPNService.connect(uuid); + return; + } + if (action.startsWith("disconnect:")) { + const uuid = action.substring("disconnect:".length); + VPNService.disconnect(uuid); + } + } + } + + BarPill { + id: pill + + screen: root.screen + density: Settings.data.bar.density + oppositeDirection: BarService.getPillDirection(root) + icon: VPNService.hasActiveConnection ? "shield-lock" : "shield" + text: { + if (VPNService.activeConnections.length > 0) { + return VPNService.activeConnections[0].name; + } + if (VPNService.connectingUuid) { + const pending = VPNService.connections[VPNService.connectingUuid]; + if (pending) { + return pending.name; + } + } + return ""; + } + suffix: { + if (VPNService.activeConnections.length > 1) { + return ` + ${VPNService.activeConnections.length - 1}`; + } + return ""; + } + autoHide: false + forceOpen: !isBarVertical && root.displayMode === "alwaysShow" + forceClose: isBarVertical || root.displayMode === "alwaysHide" || !pill.text + onClicked: PanelService.getPanel("vpnPanel", screen)?.toggle(this) + onRightClicked: { + var popupMenuWindow = PanelService.getPopupMenuWindow(screen); + if (popupMenuWindow) { + const pos = BarService.getContextMenuPosition(pill, contextMenu.implicitWidth, contextMenu.implicitHeight); + contextMenu.openAtItem(pill, pos.x, pos.y); + popupMenuWindow.showContextMenu(contextMenu); + } + } + tooltipText: { + if (pill.text !== "") { + return pill.text; + } + return I18n.tr("tooltips.manage-vpn"); + } + } +} + + + diff --git a/Modules/Panels/Settings/Bar/WidgetSettings/VPNSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/VPNSettings.qml new file mode 100644 index 00000000..55302b7e --- /dev/null +++ b/Modules/Panels/Settings/Bar/WidgetSettings/VPNSettings.qml @@ -0,0 +1,43 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + property var widgetData: null + property var widgetMetadata: null + + property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.displayMode = valueDisplayMode; + return settings; + } + + NComboBox { + label: I18n.tr("bar.widget-settings.battery.display-mode.label") + description: I18n.tr("bar.widget-settings.battery.display-mode.description") + minimumWidth: 134 + model: [ + { + "key": "onhover", + "name": I18n.tr("options.display-mode.on-hover") + }, + { + "key": "alwaysShow", + "name": I18n.tr("options.display-mode.always-show") + }, + { + "key": "alwaysHide", + "name": I18n.tr("options.display-mode.always-hide") + } + ] + currentKey: root.valueDisplayMode + onSelected: key => root.valueDisplayMode = key + } +} + diff --git a/Services/Networking/VPNService.qml b/Services/Networking/VPNService.qml new file mode 100644 index 00000000..9cfe8ce1 --- /dev/null +++ b/Services/Networking/VPNService.qml @@ -0,0 +1,290 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.UI + +Singleton { + id: root + + property var connections: ({}) + property bool refreshing: false + property bool connecting: false + property bool disconnecting: false + property string connectingUuid: "" + property string disconnectingUuid: "" + property string lastError: "" + property bool refreshPending: false + + readonly property var activeConnections: { + const result = []; + const map = connections; + for (const key in map) { + const conn = map[key]; + if (conn && conn.active) { + result.push(conn); + } + } + return result; + } + + readonly property var inactiveConnections: { + const result = []; + const map = connections; + for (const key in map) { + const conn = map[key]; + if (conn && !conn.active) { + result.push(conn); + } + } + return result; + } + + readonly property bool hasActiveConnection: activeConnections.length > 0 + + Timer { + id: refreshTimer + interval: 5000 + running: true + repeat: true + onTriggered: refresh() + } + + Timer { + id: delayedRefreshTimer + interval: 1000 + repeat: false + onTriggered: refresh() + } + + Component.onCompleted: { + Logger.i("VPN", "Service started"); + refresh(); + } + + function refresh() { + if (refreshing) { + refreshPending = true; + return; + } + refreshing = true; + lastError = ""; + refreshProcess.running = true; + } + + function connect(uuid) { + if (connecting || !uuid) { + return; + } + const conn = connections[uuid]; + if (!conn) { + return; + } + connecting = true; + connectingUuid = uuid; + lastError = ""; + connectProcess.uuid = uuid; + connectProcess.name = conn.name; + connectProcess.running = true; + } + + function disconnect(uuid) { + if (disconnecting || !uuid) { + return; + } + const conn = connections[uuid]; + if (!conn) { + return; + } + disconnecting = true; + disconnectingUuid = uuid; + lastError = ""; + disconnectProcess.uuid = uuid; + disconnectProcess.name = conn.name; + disconnectProcess.running = true; + } + + function toggle(uuid) { + const conn = connections[uuid]; + if (!conn) { + return; + } + if (conn.active) { + disconnect(uuid); + } else { + connect(uuid); + } + } + + function setConnection(uuid, data) { + if (!uuid) { + return; + } + const map = Object.assign({}, connections); + if (map[uuid]) { + map[uuid] = Object.assign({}, map[uuid], data); + connections = map; + } + } + + function scheduleRefresh(interval) { + delayedRefreshTimer.interval = interval; + delayedRefreshTimer.restart(); + } + + Process { + id: refreshProcess + running: false + command: ["nmcli", "-t", "-f", "NAME,UUID,TYPE,DEVICE", "connection", "show"] + + stdout: StdioCollector { + onStreamFinished: { + const lines = text.split("\n"); + const map = {}; + for (let i = 0; i < lines.length; ++i) { + const line = lines[i].trim(); + if (!line) { + continue; + } + const lastColonIdx = line.lastIndexOf(":"); + if (lastColonIdx === -1) { + continue; + } + const device = line.substring(lastColonIdx + 1); + const remaining = line.substring(0, lastColonIdx); + const secondLastColonIdx = remaining.lastIndexOf(":"); + if (secondLastColonIdx === -1) { + continue; + } + const type = remaining.substring(secondLastColonIdx + 1); + if (type !== "vpn") { + continue; + } + const remaining2 = remaining.substring(0, secondLastColonIdx); + const thirdLastColonIdx = remaining2.lastIndexOf(":"); + if (thirdLastColonIdx === -1) { + continue; + } + const uuid = remaining2.substring(thirdLastColonIdx + 1); + const name = remaining2.substring(0, thirdLastColonIdx); + if (!uuid || !name) { + continue; + } + const active = device && device !== "--"; + map[uuid] = { + "uuid": uuid, + "name": name, + "device": device, + "active": active + }; + } + connections = map; + const pending = refreshPending; + refreshing = false; + refreshPending = false; + if (pending) { + scheduleRefresh(200); + } + } + } + + stderr: StdioCollector { + onStreamFinished: { + const pending = refreshPending; + refreshing = false; + refreshPending = false; + if (text.trim()) { + lastError = text.split("\n")[0].trim(); + Logger.w("VPN", "Refresh error: " + text); + } + if (pending) { + scheduleRefresh(2000); + } + } + } + } + + Process { + id: connectProcess + property string uuid: "" + property string name: "" + running: false + command: ["nmcli", "connection", "up", "uuid", uuid] + + stdout: StdioCollector { + onStreamFinished: { + const output = text.trim(); + if (!output || (!output.includes("successfully activated") && !output.includes("Connection successfully"))) { + return; + } + setConnection(connectProcess.uuid, { + "active": true + }); + connecting = false; + connectingUuid = ""; + lastError = ""; + Logger.i("VPN", "Connected to " + connectProcess.name); + ToastService.showNotice(connectProcess.name, I18n.tr("toast.vpn.connected", { + "name": connectProcess.name + }), "shield-lock"); + scheduleRefresh(1000); + } + } + + stderr: StdioCollector { + onStreamFinished: { + const trimmed = text.trim(); + if (trimmed) { + lastError = trimmed.split("\n")[0].trim(); + Logger.w("VPN", "Connect error: " + trimmed); + ToastService.showWarning(connectProcess.name, lastError); + } + connecting = false; + connectingUuid = ""; + } + } + } + + Process { + id: disconnectProcess + property string uuid: "" + property string name: "" + running: false + command: ["nmcli", "connection", "down", "uuid", uuid] + + stdout: StdioCollector { + onStreamFinished: { + Logger.i("VPN", "Disconnected from " + disconnectProcess.name); + setConnection(disconnectProcess.uuid, { + "active": false, + "device": "" + }); + disconnecting = false; + disconnectingUuid = ""; + lastError = ""; + ToastService.showNotice(disconnectProcess.name, I18n.tr("toast.vpn.disconnected", { + "name": disconnectProcess.name + }), "shield-off"); + scheduleRefresh(1000); + } + } + + stderr: StdioCollector { + onStreamFinished: { + const trimmed = text.trim(); + if (trimmed) { + lastError = trimmed.split("\n")[0].trim(); + Logger.w("VPN", "Disconnect error: " + trimmed); + ToastService.showWarning(disconnectProcess.name, lastError); + } + disconnecting = false; + disconnectingUuid = ""; + } + } + } +} + + + diff --git a/Services/UI/BarWidgetRegistry.qml b/Services/UI/BarWidgetRegistry.qml index 52fd336a..b34e9795 100644 --- a/Services/UI/BarWidgetRegistry.qml +++ b/Services/UI/BarWidgetRegistry.qml @@ -36,6 +36,7 @@ Singleton { "TaskbarGrouped": taskbarGroupedComponent, "Tray": trayComponent, "Volume": volumeComponent, + "VPN": vpnComponent, "WiFi": wiFiComponent, "WallpaperSelector": wallpaperSelectorComponent, "Workspace": workspaceComponent @@ -62,6 +63,7 @@ Singleton { "TaskbarGrouped": "WidgetSettings/TaskbarGroupedSettings.qml", "Tray": "WidgetSettings/TraySettings.qml", "Volume": "WidgetSettings/VolumeSettings.qml", + "VPN": "WidgetSettings/VPNSettings.qml", "WiFi": "WidgetSettings/WiFiSettings.qml", "Workspace": "WidgetSettings/WorkspaceSettings.qml" }) @@ -210,6 +212,10 @@ Singleton { "pinned": [], "drawerEnabled": true }, + "VPN": { + "allowUserSettings": true, + "displayMode": "onhover" + }, "WiFi": { "allowUserSettings": true, "displayMode": "onhover" @@ -299,6 +305,9 @@ Singleton { property Component volumeComponent: Component { Volume {} } + property Component vpnComponent: Component { + VPN {} + } property Component wiFiComponent: Component { WiFi {} }