noctalia
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
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
|
||||
readonly property var pluginSettings: pluginApi?.pluginSettings ?? ({})
|
||||
readonly property var main: pluginApi?.mainInstance ?? ({})
|
||||
|
||||
readonly property string screenName: screen?.name ?? ""
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
|
||||
readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)
|
||||
readonly property string displayMode: root.pluginSettings.displayMode ?? "onhover"
|
||||
readonly property string connectedColor: root.pluginSettings.connectedColor
|
||||
readonly property string disconnectedColor: root.pluginSettings.disconnectedColor
|
||||
readonly property var vpnList: root.main.vpnList ?? []
|
||||
readonly property real connectedCount: root.main.connectedCount ?? 0
|
||||
readonly property bool isLoading: root.main.isLoading ?? false
|
||||
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
Component.onCompleted: {
|
||||
Logger.i("NetworkManagerVPN", "Bar widget loaded");
|
||||
}
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [{
|
||||
"label": pluginApi?.tr("settings.pluginSettings"),
|
||||
"action": "plugin-settings",
|
||||
"icon": "settings"
|
||||
}]
|
||||
onTriggered: (action) => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
if (action === "plugin-settings")
|
||||
BarService.openPluginSettings(screen, pluginApi.manifest);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
BarPill {
|
||||
id: pill
|
||||
|
||||
screen: root.screen
|
||||
oppositeDirection: BarService.getPillDirection(root)
|
||||
autoHide: false
|
||||
text: root.connectedCount > 0 ? pluginApi?.tr("common.connected") : pluginApi?.tr("common.disconnected")
|
||||
icon: root.isLoading ? "reload" : root.connectedCount > 0 ? "shield-lock" : "shield"
|
||||
onClicked: {
|
||||
if (pluginApi)
|
||||
pluginApi.openPanel(root.screen, root);
|
||||
|
||||
}
|
||||
onRightClicked: {
|
||||
if (pluginApi)
|
||||
pluginApi.closePanel(root.screen);
|
||||
PanelService.showContextMenu(contextMenu, pill, screen);
|
||||
}
|
||||
customIconColor: Color.resolveColorKeyOptional(root.connectedCount > 0 ? root.connectedColor : root.disconnectedColor)
|
||||
customTextColor: Color.resolveColorKeyOptional(root.connectedCount > 0 ? root.connectedColor : root.disconnectedColor)
|
||||
forceOpen: root.displayMode === "alwaysShow"
|
||||
forceClose: root.displayMode === "alwaysHide"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Widgets
|
||||
import qs.Commons
|
||||
|
||||
NIconButtonHot {
|
||||
property ShellScreen screen
|
||||
property var pluginApi: null
|
||||
|
||||
readonly property var main: pluginApi?.mainInstance ?? ({})
|
||||
readonly property real connectedCount: main.connectedCount ?? 0
|
||||
readonly property bool isLoading: main.isLoading ?? false
|
||||
readonly property var pluginSettings: pluginApi?.pluginSettings ?? ({})
|
||||
readonly property string connectedColor: pluginSettings.connectedColor
|
||||
readonly property string disconnectedColor: pluginSettings.disconnectedColor
|
||||
|
||||
icon: isLoading ? "reload" : connectedCount > 0 ? "shield-lock" : "shield"
|
||||
tooltipText: connectedCount > 0
|
||||
? pluginApi?.tr("common.connected")
|
||||
: pluginApi?.tr("common.disconnected")
|
||||
|
||||
colorFg: {
|
||||
const key = connectedCount > 0 ? connectedColor : disconnectedColor;
|
||||
if (!key || key === "none") return Color.mPrimary;
|
||||
return Color.resolveColorKeyOptional(key) ?? Color.mPrimary;
|
||||
}
|
||||
|
||||
onClicked: pluginApi?.togglePanel(screen, this)
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
readonly property var pluginSettings: pluginApi?.pluginSettings ?? ({})
|
||||
|
||||
readonly property var toast: root.pluginSettings?.disableToastNotifications ? null : ToastService
|
||||
|
||||
property var pluginApi: null
|
||||
|
||||
property var vpnList: []
|
||||
property real connectedCount: 0
|
||||
readonly property bool isLoading: Object.keys(root._pending).length > 0
|
||||
|
||||
property var _pending: ({})
|
||||
|
||||
// Needed only to detect disconnection not initiated by the user
|
||||
property var _pollTimer: Timer {
|
||||
interval: 5000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
property var _lines: []
|
||||
|
||||
property var _listProc: Process {
|
||||
command: ["nmcli", "-t", "-f", "NAME,TYPE,STATE,UUID", "connection", "show"]
|
||||
running: true
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: (line) => {
|
||||
if (line.trim() !== "")
|
||||
root._lines.push(line)
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
const parsed = []
|
||||
const newPending = Object.assign({}, root._pending)
|
||||
for (const line of root._lines) {
|
||||
const parts = line.split(":")
|
||||
if (parts.length >= 4) {
|
||||
const name = parts[0]
|
||||
const type = parts[1]
|
||||
const state = parts[2]
|
||||
const uuid = parts[3]
|
||||
if (type === "vpn" || type === "wireguard") {
|
||||
if (newPending[uuid]) {
|
||||
const wasConnecting = newPending[uuid] === "connect"
|
||||
if (wasConnecting && state === "activated")
|
||||
delete newPending[uuid]
|
||||
else if (!wasConnecting && state !== "activated")
|
||||
delete newPending[uuid]
|
||||
}
|
||||
parsed.push({
|
||||
name,
|
||||
type,
|
||||
connected: state === "activated",
|
||||
isLoading: !!newPending[uuid],
|
||||
uuid
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
root._pending = newPending
|
||||
root.vpnList = parsed
|
||||
root.connectedCount = parsed.filter(v => v.connected).length
|
||||
}
|
||||
root._lines = []
|
||||
}
|
||||
}
|
||||
|
||||
property var _connectProc: Process {
|
||||
property string targetName: ""
|
||||
property string targetUuid: ""
|
||||
command: ["nmcli", "connection", "up", "uuid", targetUuid]
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0)
|
||||
toast?.showNotice(t("toast.connectedTo", { name: targetName }))
|
||||
else {
|
||||
root.stopLoading(targetUuid)
|
||||
toast?.showError(t("toast.connectionError", { name: targetName }))
|
||||
}
|
||||
root.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
property var _disconnectProc: Process {
|
||||
property string targetName: ""
|
||||
property string targetUuid: ""
|
||||
command: ["nmcli", "connection", "down", "uuid", targetUuid]
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0)
|
||||
toast?.showNotice(t("toast.disconnectedFrom", { name: targetName }))
|
||||
else {
|
||||
root.stopLoading(targetUuid)
|
||||
toast?.showError(t("toast.disconnectionError", { name: targetName }))
|
||||
}
|
||||
root.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
property var _addProc: Process {
|
||||
property string targetType: ""
|
||||
|
||||
command: ["nm-connection-editor", "--create", "--type", targetType]
|
||||
onExited: (exitCode) => {
|
||||
root.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
property var _editProc: Process {
|
||||
property string targetName: ""
|
||||
property string targetUuid: ""
|
||||
|
||||
command: ["nm-connection-editor", "--edit", targetUuid]
|
||||
onExited: (exitCode) => {
|
||||
root.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
property var _removeProc: Process {
|
||||
property string targetName: ""
|
||||
property string targetUuid: ""
|
||||
|
||||
command: ["nmcli", "connection", "delete", "uuid", targetUuid]
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0)
|
||||
toast?.showNotice(t("toast.vpnRemoved", { "name": targetName }));
|
||||
else
|
||||
toast?.showError(t("toast.vpnRemoveError", { "name": targetName }));
|
||||
root.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
function t(key: string, data) {
|
||||
if (!pluginApi)
|
||||
return null;
|
||||
|
||||
return pluginApi.tr(key, data);
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
_listProc.running = true
|
||||
}
|
||||
|
||||
function stopLoading(uuid) {
|
||||
if (uuid && _pending[uuid]) {
|
||||
const p = Object.assign({}, _pending)
|
||||
delete p[uuid]
|
||||
_pending = p
|
||||
}
|
||||
|
||||
vpnList = vpnList.map(v => {
|
||||
if (v.uuid !== uuid) {
|
||||
return v
|
||||
}
|
||||
|
||||
return Object.assign({}, v, { isLoading: false })
|
||||
})
|
||||
}
|
||||
|
||||
function connectTo(uuid) {
|
||||
const p = Object.assign({}, _pending)
|
||||
p[uuid] = "connect"
|
||||
_pending = p
|
||||
let name = ""
|
||||
vpnList = vpnList.map(v => {
|
||||
if (v.uuid !== uuid) {
|
||||
return v
|
||||
}
|
||||
|
||||
name = v.name
|
||||
return Object.assign({}, v, { isLoading: true })
|
||||
})
|
||||
_connectProc.targetName = name
|
||||
_connectProc.targetUuid = uuid
|
||||
_connectProc.running = true
|
||||
}
|
||||
|
||||
function disconnectFrom(uuid) {
|
||||
const p = Object.assign({}, _pending)
|
||||
p[uuid] = "disconnect"
|
||||
_pending = p
|
||||
let name = ""
|
||||
vpnList = vpnList.map(v => {
|
||||
if (v.uuid !== uuid) {
|
||||
return v
|
||||
}
|
||||
|
||||
name = v.name
|
||||
return Object.assign({}, v, { isLoading: true })
|
||||
})
|
||||
_disconnectProc.targetName = name
|
||||
_disconnectProc.targetUuid = uuid
|
||||
_disconnectProc.running = true
|
||||
}
|
||||
|
||||
function addConnection(type) {
|
||||
_addProc.targetType = type || "vpn";
|
||||
_addProc.running = true;
|
||||
}
|
||||
|
||||
function editConnection(uuid) {
|
||||
const vpn = vpnList.find((v) => {
|
||||
return v.uuid === uuid;
|
||||
});
|
||||
_editProc.targetName = vpn ? vpn.name : uuid;
|
||||
_editProc.targetUuid = uuid;
|
||||
_editProc.running = true;
|
||||
}
|
||||
|
||||
function removeConnection(uuid) {
|
||||
const vpn = vpnList.find((v) => {
|
||||
return v.uuid === uuid;
|
||||
});
|
||||
_removeProc.targetName = vpn ? vpn.name : uuid;
|
||||
_removeProc.targetUuid = uuid;
|
||||
_removeProc.running = true;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.i("NetworkManagerVPN", "Started")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services.UI
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var pluginApi: null
|
||||
property ShellScreen screen
|
||||
readonly property var geometryPlaceholder: panelContainer
|
||||
readonly property bool allowAttach: true
|
||||
readonly property var main: pluginApi?.mainInstance ?? null
|
||||
readonly property var vpnList: main?.vpnList ?? []
|
||||
readonly property var activeList: vpnList.filter(v => v.connected || v.isLoading)
|
||||
readonly property var inactiveList: vpnList.filter(v => !v.connected && !v.isLoading)
|
||||
property real contentPreferredWidth: Math.round(500 * Style.uiScaleRatio)
|
||||
property real contentPreferredHeight: Math.min(500, mainColumn.implicitHeight + Style.marginL * 2)
|
||||
|
||||
Component.onCompleted: {
|
||||
if (main) main.refresh()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: panelContainer
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
|
||||
ColumnLayout {
|
||||
id: mainColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginM
|
||||
|
||||
// HEADER
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.round(header.implicitHeight + Style.marginM * 2 + 1)
|
||||
|
||||
ColumnLayout {
|
||||
id: header
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
RowLayout {
|
||||
NIcon {
|
||||
icon: "shield"
|
||||
pointSize: Style.fontSizeXXL
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NLabel {
|
||||
label: pluginApi?.tr("common.vpn")
|
||||
}
|
||||
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "refresh"
|
||||
tooltipText: pluginApi?.tr("common.refresh")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
enabled: true
|
||||
onClicked: {
|
||||
if (main) {
|
||||
main.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: pluginApi?.tr("common.close")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: pluginApi.closePanel(pluginApi.panelOpenScreen)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NScrollView {
|
||||
id: scrollView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
reserveScrollbarSpace: false
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Style.marginM
|
||||
|
||||
// CONNECTED
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.round(networksListActive.implicitHeight + Style.marginXL)
|
||||
visible: activeList.length > 0
|
||||
|
||||
ColumnLayout {
|
||||
id: networksListActive
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.marginS
|
||||
spacing: Style.marginS
|
||||
|
||||
NLabel {
|
||||
label: pluginApi?.tr("common.connected")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: activeList
|
||||
|
||||
VpnListItem {
|
||||
name: modelData.name
|
||||
type: modelData.type
|
||||
isConnected: true
|
||||
isLoading: modelData.isLoading
|
||||
onButtonClicked: {
|
||||
if (!main) return
|
||||
main.disconnectFrom(modelData.uuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DISCONNECTED
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.round(networksListInactive.implicitHeight + Style.marginXL)
|
||||
visible: inactiveList.length > 0
|
||||
|
||||
ColumnLayout {
|
||||
id: networksListInactive
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.marginS
|
||||
spacing: Style.marginS
|
||||
|
||||
NLabel {
|
||||
label: pluginApi?.tr("common.disconnected")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: inactiveList
|
||||
|
||||
VpnListItem {
|
||||
name: modelData.name
|
||||
type: modelData.type
|
||||
isConnected: false
|
||||
isLoading: modelData.isLoading
|
||||
onButtonClicked: {
|
||||
if (!main) return
|
||||
main.connectTo(modelData.uuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EMPTY
|
||||
NBox {
|
||||
id: emptyBox
|
||||
visible: vpnList.length < 1
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.round(emptyColumn.implicitHeight + Style.marginM * 2 + 1)
|
||||
|
||||
ColumnLayout {
|
||||
id: emptyColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginL
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "search"
|
||||
pointSize: 48
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("panel.emptyTitle")
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("panel.emptyDescription")
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: pluginApi?.tr("common.refresh")
|
||||
icon: "refresh"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: {
|
||||
if (main) {
|
||||
main.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Network Manager VPN
|
||||
|
||||

|
||||
|
||||
Plugin to connect to your VPN connections from the bar and Control Center. Supports OpenVPN and WireGuard connections managed by NetworkManager. Editing and creation are achieved using
|
||||
nm-connection-editor
|
||||
|
||||
## Usage
|
||||
|
||||
Install the plugin and add it to your bar:
|
||||
|
||||
- **Bar widget** — A shield icon that shows if there is an active connection. Click it to open the VPN panel.
|
||||
- **VPN panel** — Lists all configured VPN connections.
|
||||
|
||||
The panel refreshes automatically every 5 seconds. You can also trigger a manual refresh with the refresh button.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Noctalia Shell** ≥ 3.6.0
|
||||
- **NetworkManager** with `nmcli`
|
||||
- **nm-connection-editor** to edit and create connections
|
||||
@@ -0,0 +1,256 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property var pluginApi: null
|
||||
readonly property var pluginSettings: pluginApi?.pluginSettings ?? pluginApi?.manifest?.metadata?.defaultSettings ?? ({})
|
||||
|
||||
readonly property var main: pluginApi?.mainInstance ?? null
|
||||
readonly property var vpnList: main ? main.vpnList : []
|
||||
// Local state
|
||||
property string editDisplayMode: root.pluginSettings.displayMode ?? ""
|
||||
property string editConnectedColor: root.pluginSettings.connectedColor ?? ""
|
||||
property string editDisconnectedColor: root.pluginSettings.disconnectedColor ?? ""
|
||||
property bool disableToastNotifications: root.pluginSettings?.disableToastNotifications ?? false
|
||||
|
||||
property string pendingDeleteUuid: ""
|
||||
property string pendingDeleteName: ""
|
||||
|
||||
readonly property var displayModeModel: [{
|
||||
"key": "onhover",
|
||||
"name": I18n.tr("display-modes.on-hover")
|
||||
}, {
|
||||
"key": "alwaysShow",
|
||||
"name": I18n.tr("display-modes.always-show")
|
||||
}, {
|
||||
"key": "alwaysHide",
|
||||
"name": I18n.tr("display-modes.always-hide")
|
||||
}]
|
||||
|
||||
function saveSettings() {
|
||||
pluginApi.pluginSettings.displayMode = root.editDisplayMode;
|
||||
pluginApi.pluginSettings.connectedColor = root.editConnectedColor;
|
||||
pluginApi.pluginSettings.disconnectedColor = root.editDisconnectedColor;
|
||||
pluginApi.pluginSettings.disableToastNotifications = root.disableToastNotifications;
|
||||
pluginApi.saveSettings();
|
||||
|
||||
Logger.i("NetworkManagerVpn", "Settings saved successfully");
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("common.display-mode")
|
||||
description: I18n.tr("bar.volume.display-mode-description")
|
||||
minimumWidth: 200
|
||||
model: root.displayModeModel
|
||||
currentKey: root.editDisplayMode
|
||||
onSelected: (key) => {
|
||||
root.editDisplayMode = key;
|
||||
}
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: pluginApi?.tr("settings.connectedColor")
|
||||
description: pluginApi?.tr("settings.connectedColorDescription")
|
||||
currentKey: root.editConnectedColor
|
||||
onSelected: (key) => {
|
||||
root.editConnectedColor = key;
|
||||
}
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: pluginApi?.tr("settings.disconnectedColor")
|
||||
description: pluginApi?.tr("settings.disconnectedColorDescription")
|
||||
currentKey: root.editDisconnectedColor
|
||||
onSelected: (key) => {
|
||||
root.editDisconnectedColor = key;
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.disableToastNotifications")
|
||||
description: pluginApi?.tr("settings.disableToastNotificationsDescription")
|
||||
checked: root.disableToastNotifications
|
||||
onToggled: checked => {
|
||||
root.disableToastNotifications = checked;
|
||||
}
|
||||
}
|
||||
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginM
|
||||
Layout.preferredHeight: Math.round(vpnSection.implicitHeight + Style.marginL * 2)
|
||||
|
||||
ColumnLayout {
|
||||
id: vpnSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
NLabel {
|
||||
label: pluginApi?.tr("settings.vpnConnections")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: root.vpnList.length === 0
|
||||
text: pluginApi?.tr("settings.noVpnConnections")
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.leftMargin: Style.marginXS
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.vpnList
|
||||
|
||||
NBox {
|
||||
id: vpnRow
|
||||
|
||||
readonly property bool confirmingDelete: root.pendingDeleteUuid === modelData.uuid
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.marginXS
|
||||
Layout.rightMargin: Style.marginXS
|
||||
implicitHeight: Math.round(rowContent.implicitHeight + Style.marginL)
|
||||
color: vpnRow.confirmingDelete ? Qt.alpha(Color.mError, 0.12) : Color.mSurface
|
||||
|
||||
ColumnLayout {
|
||||
id: rowContent
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.marginM
|
||||
anchors.rightMargin: Style.marginM
|
||||
anchors.topMargin: Style.marginS
|
||||
anchors.bottomMargin: Style.marginS
|
||||
spacing: Style.marginS
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NIcon {
|
||||
icon: "router"
|
||||
pointSize: Style.fontSizeXXL
|
||||
color: modelData.connected ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
|
||||
NText {
|
||||
text: modelData.name
|
||||
pointSize: Style.fontSizeM
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSurface
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NText {
|
||||
text: modelData.type || "vpn"
|
||||
pointSize: Style.fontSizeXXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
visible: !vpnRow.confirmingDelete
|
||||
icon: "edit"
|
||||
tooltipText: pluginApi?.tr("common.edit")
|
||||
baseSize: Style.baseWidgetSize * 0.75
|
||||
onClicked: {
|
||||
main.editConnection(modelData.uuid);
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
visible: !vpnRow.confirmingDelete
|
||||
icon: "trash-x"
|
||||
tooltipText: pluginApi?.tr("common.delete")
|
||||
baseSize: Style.baseWidgetSize * 0.75
|
||||
onClicked: {
|
||||
root.pendingDeleteUuid = modelData.uuid;
|
||||
root.pendingDeleteName = modelData.name;
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
visible: vpnRow.confirmingDelete
|
||||
text: pluginApi?.tr("common.delete")
|
||||
fontSize: Style.fontSizeXS
|
||||
backgroundColor: Color.mError
|
||||
outlined: false
|
||||
onClicked: {
|
||||
main.removeConnection(root.pendingDeleteUuid);
|
||||
root.pendingDeleteUuid = "";
|
||||
root.pendingDeleteName = "";
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
visible: vpnRow.confirmingDelete
|
||||
text: pluginApi?.tr("common.cancel")
|
||||
fontSize: Style.fontSizeXS
|
||||
outlined: true
|
||||
onClicked: {
|
||||
root.pendingDeleteUuid = "";
|
||||
root.pendingDeleteName = "";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginXS
|
||||
spacing: Style.marginS
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: pluginApi?.tr("settings.addVpn")
|
||||
icon: "add"
|
||||
fontSize: Style.fontSizeS
|
||||
outlined: true
|
||||
onClicked: {
|
||||
main.addConnection("vpn");
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: pluginApi?.tr("settings.addWireguard")
|
||||
icon: "add"
|
||||
fontSize: Style.fontSizeS
|
||||
outlined: true
|
||||
onClicked: {
|
||||
main.addConnection("wireguard");
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services.UI
|
||||
|
||||
NBox {
|
||||
id: root
|
||||
|
||||
property string name: ""
|
||||
property string type: ""
|
||||
property bool isConnected: false
|
||||
property bool isLoading: false
|
||||
|
||||
signal buttonClicked
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.marginXS
|
||||
Layout.rightMargin: Style.marginXS
|
||||
implicitHeight: Math.round(netColumn.implicitHeight + (Style.marginXL))
|
||||
|
||||
color: root.isConnected ? Qt.alpha(Color.mPrimary, 0.15) : Color.mSurface
|
||||
|
||||
ColumnLayout {
|
||||
id: netColumn
|
||||
width: parent.width - (Style.marginXL)
|
||||
x: Style.marginM
|
||||
y: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
// Main row
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NIcon {
|
||||
icon: "router"
|
||||
pointSize: Style.fontSizeXXL
|
||||
color: root.isConnected ? Color.mPrimary : Color.mOnSurface
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
|
||||
NText {
|
||||
text: root.name
|
||||
pointSize: Style.fontSizeM
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSurface
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginXS
|
||||
|
||||
NText {
|
||||
text: root.type
|
||||
pointSize: Style.fontSizeXXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: Style.marginXXS
|
||||
}
|
||||
|
||||
|
||||
Rectangle {
|
||||
visible: root.isConnected
|
||||
color: Color.mPrimary
|
||||
radius: height * 0.5
|
||||
width: Math.round(connectedText.implicitWidth + (Style.marginS * 2))
|
||||
height: Math.round(connectedText.implicitHeight + (Style.marginXS))
|
||||
|
||||
NText {
|
||||
id: connectedText
|
||||
anchors.centerIn: parent
|
||||
text: pluginApi?.tr("common.connected")
|
||||
pointSize: Style.fontSizeXXS
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Action area
|
||||
RowLayout {
|
||||
spacing: Style.marginS
|
||||
|
||||
NBusyIndicator {
|
||||
visible: root.isLoading
|
||||
running: visible
|
||||
color: Color.mPrimary
|
||||
size: Style.baseWidgetSize * 0.5
|
||||
}
|
||||
|
||||
NButton {
|
||||
visible: root.isConnected
|
||||
text: pluginApi?.tr("common.disconnect")
|
||||
outlined: !hovered
|
||||
fontSize: Style.fontSizeS
|
||||
backgroundColor: Color.mError
|
||||
enabled: !root.isLoading
|
||||
onClicked: {
|
||||
root.buttonClicked()
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
visible: !root.isConnected
|
||||
text: pluginApi?.tr("common.connect")
|
||||
outlined: !hovered
|
||||
fontSize: Style.fontSizeS
|
||||
enabled: !root.isLoading
|
||||
onClicked: {
|
||||
root.buttonClicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"panel": {
|
||||
"emptyTitle": "Kein VPN gefunden",
|
||||
"emptyDescription": "Verwende den Netzwerk-Manager um ein VPN hinzuzufügen"
|
||||
},
|
||||
"toast": {
|
||||
"connectedTo": "Verbunden mit {name}",
|
||||
"disconnectedFrom": "Von {Name} getrennt",
|
||||
"connectionError": "Verbindungsaufbau mit {name} fehlgeschlagen",
|
||||
"disconnectionError": "Die Trennung von {name} ist fehlgeschlagen",
|
||||
"vpnRemoved": "VPN \"{name}\" wurde entfernt",
|
||||
"vpnRemoveError": "Der VPN \"{name}\" konnte nicht entfernt werden"
|
||||
},
|
||||
"settings": {
|
||||
"pluginSettings": "Plugin Einstellungen",
|
||||
"connectedColor": "Verknüpfte Farbe",
|
||||
"connectedColorDescription": "Wähle die Farbe für das Symbol und den Text aus, die bei bestehender Verbindung angezeigt werden sollen",
|
||||
"disconnectedColor": "Nicht verknüpfte Farbe",
|
||||
"disconnectedColorDescription": "Wähle die Farbe für das Symbol und den Text bei unterbrochener Verbindung aus",
|
||||
"vpnConnections": "VPN Verbindungen",
|
||||
"noVpnConnections": "Keine VPN Verbindungen konfiguriert",
|
||||
"addVpn": "VPN hinzufügen",
|
||||
"addWireguard": "WireGuard hinzufügen",
|
||||
"disableToastNotifications": "Toast-Benachrichtigungen deaktivieren",
|
||||
"disableToastNotificationsDescription": "Keine Toast-Benachrichtigungen anzeigen, wenn eine VPN-Verbindung hergestellt oder getrennt wird"
|
||||
},
|
||||
"common": {
|
||||
"vpn": "VPN",
|
||||
"refresh": "Aktualisieren",
|
||||
"connected": "Verbunden",
|
||||
"disconnected": "Getrennt",
|
||||
"connect": "Verbinden",
|
||||
"disconnect": "Trennen",
|
||||
"close": "Schließen",
|
||||
"settings": "Einstellungen",
|
||||
"edit": "Bearbeiten",
|
||||
"delete": "Löschen",
|
||||
"cancel": "Abbrechen"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"panel": {
|
||||
"emptyTitle": "No VPN found",
|
||||
"emptyDescription": "Use Network Manager to add a VPN"
|
||||
},
|
||||
"toast": {
|
||||
"connectedTo": "Connected to {name}",
|
||||
"disconnectedFrom": "Disconnected from {name}",
|
||||
"connectionError": "Failed to connect {name}",
|
||||
"disconnectionError": "Failed to disconnect {name}",
|
||||
"vpnRemoved": "VPN \"{name}\" removed",
|
||||
"vpnRemoveError": "Failed to remove VPN \"{name}\""
|
||||
},
|
||||
"settings": {
|
||||
"pluginSettings": "Plugin settings",
|
||||
"connectedColor": "Connected color",
|
||||
"connectedColorDescription": "Chose the color to use for icon and text when connected",
|
||||
"disconnectedColor": "Disconnected color",
|
||||
"disconnectedColorDescription": "Chose the color to use for icon and text when disconnected",
|
||||
"vpnConnections": "VPN Connections",
|
||||
"noVpnConnections": "No VPN connections configured.",
|
||||
"addVpn": "Add VPN",
|
||||
"addWireguard": "Add WireGuard",
|
||||
"disableToastNotifications": "Disable toast notifications",
|
||||
"disableToastNotificationsDescription": "Do not show toast notifications when a VPN connects or disconnects"
|
||||
},
|
||||
"common": {
|
||||
"vpn": "VPN",
|
||||
"refresh": "Refresh",
|
||||
"connected": "Connected",
|
||||
"disconnected": "Disconnected",
|
||||
"connect": "Connect",
|
||||
"disconnect": "Disconnect",
|
||||
"close": "Close",
|
||||
"settings": "Settings",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"panel": {
|
||||
"emptyTitle": "VPN не найден",
|
||||
"emptyDescription": "Используйте Менеджер Подключений для добавления VPN"
|
||||
},
|
||||
"toast": {
|
||||
"connectedTo": "Подключиться к {name}",
|
||||
"disconnectedFrom": "Отключиться от {name}",
|
||||
"connectionError": "Ошибка подключения к {name}",
|
||||
"disconnectionError": "Ошибка отключения от {name}",
|
||||
"vpnRemoved": "VPN \"{name}\" удалён",
|
||||
"vpnRemoveError": "Ошибка удаления VPN \"{name}\""
|
||||
},
|
||||
"settings": {
|
||||
"pluginSettings": "Настройка плагина",
|
||||
"connectedColor": "Цвет активного подключения",
|
||||
"connectedColorDescription": "Выберите цвет значка и текста при активном подключении",
|
||||
"disconnectedColor": "Цвет отсутствия подключения",
|
||||
"disconnectedColorDescription": "Выберите цвет значка и текста при отсутствии подключения",
|
||||
"vpnConnections": "VPN-соединения",
|
||||
"noVpnConnections": "VPN-подключения не настроены.",
|
||||
"addVpn": "Добавить VPN",
|
||||
"addWireguard": "Добавить WireGuard",
|
||||
"disableToastNotifications": "Отключить уведомления",
|
||||
"disableToastNotificationsDescription": "Не показывать уведомления о статусе VPN-соединения"
|
||||
},
|
||||
"common": {
|
||||
"vpn": "VPN",
|
||||
"refresh": "Обновить",
|
||||
"connected": "Подключено",
|
||||
"disconnected": "Отключено",
|
||||
"connect": "Подключить",
|
||||
"disconnect": "Отключить",
|
||||
"close": "Закрыть",
|
||||
"settings": "Настройки",
|
||||
"edit": "Редактировать",
|
||||
"delete": "Удалить",
|
||||
"cancel": "Отмена"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"id": "network-manager-vpn",
|
||||
"name": "Network Manager VPN",
|
||||
"version": "1.3.0",
|
||||
"minNoctaliaVersion": "3.6.0",
|
||||
"author": "nZO",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/noctalia-dev/noctalia-plugins",
|
||||
"description": "Connect to VPN via NetworkManager",
|
||||
"tags": [
|
||||
"Bar",
|
||||
"Panel",
|
||||
"Network"
|
||||
],
|
||||
"entryPoints": {
|
||||
"main": "Main.qml",
|
||||
"barWidget": "BarWidget.qml",
|
||||
"controlCenterWidget": "ControlCenterWidget.qml",
|
||||
"panel": "Panel.qml",
|
||||
"settings": "Settings.qml"
|
||||
},
|
||||
"dependencies": {
|
||||
"plugins": []
|
||||
},
|
||||
"metadata": {
|
||||
"defaultSettings": {
|
||||
"displayMode": "onhover",
|
||||
"disconnectedColor": "none",
|
||||
"connectedColor": "primary",
|
||||
"disableToastNotifications": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 421 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"displayMode": "onhover",
|
||||
"disconnectedColor": "none",
|
||||
"connectedColor": "primary",
|
||||
"disableToastNotifications": false
|
||||
}
|
||||
Reference in New Issue
Block a user