diff --git a/quickshell/DMSShellIPC.qml b/quickshell/DMSShellIPC.qml index 2c5715d5..baeb8f55 100644 --- a/quickshell/DMSShellIPC.qml +++ b/quickshell/DMSShellIPC.qml @@ -956,7 +956,7 @@ Item { function tabs(): string { if (!PopoutService.settingsModal) - return "wallpaper\ntheme\ntypography\ntime_weather\nsounds\ndankbar\ndankbar_settings\ndankbar_appearance\ndankbar_widgets\nframe\nworkspaces\ncompositor\nmedia_player\nnotifications\nosd\nrunning_apps\nupdater\ndock\nlauncher\nkeybinds\ndisplays\nnetwork\nprinters\nlock_screen\npower_sleep\nplugins\nabout"; + return "wallpaper\ntheme\ntypography\ntime_weather\nsounds\ndankbar\ndankbar_settings\ndankbar_appearance\ndankbar_widgets\nframe\nworkspaces\ncompositor\nmedia_player\nnotifications\nosd\nrunning_apps\nupdater\ndock\nlauncher\nkeybinds\ndisplays\nnetwork\nnetwork_status\nnetwork_ethernet\nnetwork_wifi\nnetwork_vpn\nprinters\nlock_screen\npower_sleep\nplugins\nabout"; var modal = PopoutService.settingsModal; var ids = []; var structure = modal.sidebar?.categoryStructure ?? []; diff --git a/quickshell/Modals/Settings/SettingsContent.qml b/quickshell/Modals/Settings/SettingsContent.qml index facd76f0..2cd4df76 100644 --- a/quickshell/Modals/Settings/SettingsContent.qml +++ b/quickshell/Modals/Settings/SettingsContent.qml @@ -1,6 +1,7 @@ import QtQuick import qs.Common import qs.Modules.Settings +import qs.Services import qs.Widgets FocusScope { @@ -232,7 +233,52 @@ FocusScope { visible: active focus: active - sourceComponent: NetworkTab {} + sourceComponent: NetworkStatusTab {} + + onActiveChanged: { + if (active && item) + Qt.callLater(() => item.forceActiveFocus()); + } + } + + Loader { + id: networkEthernetLoader + anchors.fill: parent + active: root.currentIndex === 39 + visible: active + focus: active + + sourceComponent: NetworkEthernetTab {} + + onActiveChanged: { + if (active && item) + Qt.callLater(() => item.forceActiveFocus()); + } + } + + Loader { + id: networkWifiLoader + anchors.fill: parent + active: root.currentIndex === 40 + visible: active + focus: active + + sourceComponent: NetworkWifiTab {} + + onActiveChanged: { + if (active && item) + Qt.callLater(() => item.forceActiveFocus()); + } + } + + Loader { + id: networkVpnLoader + anchors.fill: parent + active: root.currentIndex === 41 + visible: active + focus: active + + sourceComponent: NetworkVpnTab {} onActiveChanged: { if (active && item) diff --git a/quickshell/Modals/Settings/SettingsModal.qml b/quickshell/Modals/Settings/SettingsModal.qml index 99fc2f19..f2d07dd5 100644 --- a/quickshell/Modals/Settings/SettingsModal.qml +++ b/quickshell/Modals/Settings/SettingsModal.qml @@ -53,20 +53,21 @@ FloatingWindow { visible = !visible; } + function setTabIndex(tabIndex: int) { + if (tabIndex < 0) + return; + currentTabIndex = tabIndex; + sidebar.autoExpandForTab(tabIndex); + } + function showWithTab(tabIndex: int) { - if (tabIndex >= 0) { - currentTabIndex = tabIndex; - sidebar.autoExpandForTab(tabIndex); - } + setTabIndex(tabIndex); visible = true; } function showWithTabName(tabName: string) { var idx = sidebar.resolveTabIndex(tabName); - if (idx >= 0) { - currentTabIndex = idx; - sidebar.autoExpandForTab(idx); - } + setTabIndex(idx); visible = true; } diff --git a/quickshell/Modals/Settings/SettingsSidebar.qml b/quickshell/Modals/Settings/SettingsSidebar.qml index 0e3c2cdb..70502ad7 100644 --- a/quickshell/Modals/Settings/SettingsSidebar.qml +++ b/quickshell/Modals/Settings/SettingsSidebar.qml @@ -238,8 +238,33 @@ Rectangle { "id": "network", "text": I18n.tr("Network"), "icon": "wifi", - "tabIndex": 7, - "dmsOnly": true + "dmsOnly": true, + "children": [ + { + "id": "network_status", + "text": I18n.tr("Status"), + "icon": "lan", + "tabIndex": 7 + }, + { + "id": "network_ethernet", + "text": I18n.tr("Ethernet"), + "icon": "settings_ethernet", + "tabIndex": 39 + }, + { + "id": "network_wifi", + "text": I18n.tr("WiFi"), + "icon": "wifi", + "tabIndex": 40 + }, + { + "id": "network_vpn", + "text": I18n.tr("VPN"), + "icon": "vpn_key", + "tabIndex": 41 + } + ] }, { "id": "applications", diff --git a/quickshell/Modules/ControlCenter/Details/NetworkDetail.qml b/quickshell/Modules/ControlCenter/Details/NetworkDetail.qml index bad64792..550acf21 100644 --- a/quickshell/Modules/ControlCenter/Details/NetworkDetail.qml +++ b/quickshell/Modules/ControlCenter/Details/NetworkDetail.qml @@ -151,7 +151,7 @@ Rectangle { iconColor: Theme.surfaceVariantText onClicked: { PopoutService.closeControlCenter(); - PopoutService.openSettingsWithTab("network"); + PopoutService.openSettingsWithTab(currentPreferenceIndex === 0 ? "network_ethernet" : "network_wifi"); } } } diff --git a/quickshell/Modules/Settings/NetworkEthernetTab.qml b/quickshell/Modules/Settings/NetworkEthernetTab.qml new file mode 100644 index 00000000..69ab27b3 --- /dev/null +++ b/quickshell/Modules/Settings/NetworkEthernetTab.qml @@ -0,0 +1,462 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Modules.Settings.Widgets +import qs.Services +import qs.Widgets + +Item { + id: networkEthernetTab + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + Component.onCompleted: { + NetworkService.addRef(); + } + + Component.onDestruction: { + NetworkService.removeRef(); + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + + topPadding: 4 + width: Math.min(600, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingL + + SettingsCard { + id: root + + property string expandedEthDevice: "" + + title: I18n.tr("Ethernet") + iconName: "settings_ethernet" + settingKey: "networkEthernet" + tags: ["ethernet", "wired", "network", "adapters", "connection"] + + width: parent.width + + Column { + id: ethernetSection + + width: parent.width + spacing: Theme.spacingM + + StyledText { + text: { + const devices = NetworkService.ethernetDevices; + const connected = devices.filter(d => d.connected).length; + if (devices.length === 0) + return I18n.tr("No adapters"); + if (connected === 0) + return devices.length === 1 ? I18n.tr("%1 adapter, none connected").arg(devices.length) : I18n.tr("%1 adapters, none connected").arg(devices.length); + return I18n.tr("%1 connected").arg(connected); + } + font.pixelSize: Theme.fontSizeSmall + color: NetworkService.ethernetConnected ? Theme.primary : Theme.surfaceVariantText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + + Column { + width: parent.width + spacing: 4 + visible: NetworkService.ethernetDevices.length > 0 + + StyledText { + text: I18n.tr("Adapters") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + Repeater { + model: NetworkService.ethernetDevices + + delegate: Rectangle { + id: ethDeviceDelegate + required property var modelData + required property int index + + readonly property bool isConnected: modelData.connected || false + readonly property bool isExpanded: root.expandedEthDevice === modelData.name + + width: parent.width + height: isExpanded ? 56 + ethExpandedContent.height : 56 + radius: Theme.cornerRadius + color: ethDeviceMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight + border.width: isConnected ? 2 : 0 + border.color: Theme.primary + clip: true + + Behavior on height { + NumberAnimation { + duration: 150 + easing.type: Easing.OutQuad + } + } + + Column { + anchors.fill: parent + spacing: 0 + + Item { + width: parent.width + height: 56 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + anchors.right: ethDeviceActions.left + anchors.rightMargin: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + name: "lan" + size: 20 + color: isConnected ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + width: parent.width - 20 - Theme.spacingS + + StyledText { + text: modelData.name || I18n.tr("Unknown") + font.pixelSize: Theme.fontSizeMedium + color: isConnected ? Theme.primary : Theme.surfaceText + font.weight: isConnected ? Font.Medium : Font.Normal + elide: Text.ElideRight + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + Row { + anchors.left: parent.left + spacing: Theme.spacingXS + + StyledText { + text: { + switch (modelData.state) { + case "activated": + return I18n.tr("Connected"); + case "disconnected": + return I18n.tr("Disconnected"); + case "unavailable": + return I18n.tr("Unavailable"); + default: + return modelData.state || I18n.tr("Unknown"); + } + } + font.pixelSize: Theme.fontSizeSmall + color: isConnected ? Theme.primary : Theme.surfaceVariantText + } + + StyledText { + text: "•" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: (modelData.ip || "").length > 0 + } + + StyledText { + text: modelData.ip || "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: (modelData.ip || "").length > 0 + } + } + } + } + + Row { + id: ethDeviceActions + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: ethExpandBtn.containsMouse ? Theme.surfacePressed : "transparent" + visible: isConnected + + DankIcon { + anchors.centerIn: parent + name: isExpanded ? "expand_less" : "expand_more" + size: 18 + color: Theme.surfaceText + } + + MouseArea { + id: ethExpandBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (isExpanded) { + root.expandedEthDevice = ""; + } else { + root.expandedEthDevice = modelData.name; + NetworkService.fetchWiredNetworkInfo(NetworkService.ethernetConnectionUuid); + } + } + } + } + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: ethDisconnectBtn.containsMouse ? Theme.errorHover : "transparent" + visible: isConnected + + DankIcon { + anchors.centerIn: parent + name: "link_off" + size: 18 + color: ethDisconnectBtn.containsMouse ? Theme.error : Theme.surfaceVariantText + } + + MouseArea { + id: ethDisconnectBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: NetworkService.disconnectEthernetDevice(modelData.name) + } + } + } + + MouseArea { + id: ethDeviceMouseArea + anchors.fill: parent + anchors.rightMargin: ethDeviceActions.width + Theme.spacingM + hoverEnabled: true + } + } + + Column { + id: ethExpandedContent + width: parent.width + visible: isExpanded + + Rectangle { + width: parent.width - Theme.spacingM * 2 + height: 1 + x: Theme.spacingM + color: Theme.outlineLight + } + + Item { + width: parent.width + height: ethDetailsColumn.implicitHeight + Theme.spacingM * 2 + + Column { + id: ethDetailsColumn + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + Flow { + width: parent.width + spacing: Theme.spacingXS + + Repeater { + model: { + const fields = []; + const dev = modelData; + if (!dev) + return fields; + + if (dev.ip) + fields.push({ + label: I18n.tr("IP"), + value: dev.ip + }); + if (dev.speed && dev.speed > 0) + fields.push({ + label: I18n.tr("Speed"), + value: dev.speed + " Mbps" + }); + if (dev.hwAddress) + fields.push({ + label: I18n.tr("MAC"), + value: dev.hwAddress + }); + if (dev.driver) + fields.push({ + label: I18n.tr("Driver"), + value: dev.driver + }); + fields.push({ + label: I18n.tr("State"), + value: dev.state || I18n.tr("Unknown") + }); + + return fields; + } + + delegate: Rectangle { + required property var modelData + required property int index + + width: ethFieldContent.width + Theme.spacingM * 2 + height: 32 + radius: Theme.cornerRadius - 2 + color: Theme.surfaceContainerHigh + border.width: 1 + border.color: Theme.outlineLight + + Row { + id: ethFieldContent + anchors.centerIn: parent + spacing: Theme.spacingXS + + StyledText { + text: modelData.label + ":" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: modelData.value + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + } + + Item { + width: parent.width + height: NetworkService.networkWiredInfoLoading ? 40 : 0 + visible: NetworkService.networkWiredInfoLoading + + DankSpinner { + anchors.centerIn: parent + size: 20 + } + } + } + } + } + } + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + visible: NetworkService.wiredConnections.length > 0 + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + + StyledText { + text: I18n.tr("Saved Configurations") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + Repeater { + model: NetworkService.wiredConnections + + delegate: Rectangle { + required property var modelData + required property int index + + width: parent.width + height: 48 + radius: Theme.cornerRadius + color: wiredMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight + border.width: modelData.isActive ? 2 : 0 + border.color: Theme.primary + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "lan" + size: 20 + color: modelData.isActive ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: modelData.id || I18n.tr("Unknown") + font.pixelSize: Theme.fontSizeMedium + color: modelData.isActive ? Theme.primary : Theme.surfaceText + font.weight: modelData.isActive ? Font.Medium : Font.Normal + } + + StyledText { + text: modelData.isActive ? I18n.tr("Active") : "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + visible: modelData.isActive + } + } + } + + MouseArea { + id: wiredMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (!modelData.isActive) { + NetworkService.connectToSpecificWiredConfig(modelData.uuid); + } + } + } + } + } + } + } + } + } + } +} diff --git a/quickshell/Modules/Settings/NetworkStatusTab.qml b/quickshell/Modules/Settings/NetworkStatusTab.qml new file mode 100644 index 00000000..ec9c1863 --- /dev/null +++ b/quickshell/Modules/Settings/NetworkStatusTab.qml @@ -0,0 +1,202 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Modules.Settings.Widgets +import qs.Services +import qs.Widgets + +Item { + id: networkStatusTab + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + Component.onCompleted: { + NetworkService.addRef(); + } + + Component.onDestruction: { + NetworkService.removeRef(); + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + + topPadding: 4 + width: Math.min(600, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingL + + SettingsCard { + id: root + + title: I18n.tr("Network Status") + iconName: "lan" + settingKey: "networkStatus" + tags: ["status", "network", "connectivity", "internet"] + + width: parent.width + + Column { + id: overviewSection + + width: parent.width + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Overview of your network connections") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + + Grid { + columns: 2 + columnSpacing: Theme.spacingL + rowSpacing: Theme.spacingS + width: parent.width + + StyledText { + text: I18n.tr("Backend") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + } + StyledText { + text: NetworkService.backend || I18n.tr("Unknown") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + StyledText { + text: I18n.tr("Status") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + } + Row { + spacing: Theme.spacingS + + Rectangle { + width: 8 + height: 8 + radius: 4 + anchors.verticalCenter: parent.verticalCenter + color: { + switch (NetworkService.networkStatus) { + case "ethernet": + case "wifi": + return Theme.success; + case "disconnected": + return Theme.error; + default: + return Theme.warning; + } + } + } + + StyledText { + text: { + switch (NetworkService.networkStatus) { + case "ethernet": + return I18n.tr("Ethernet"); + case "wifi": + return I18n.tr("WiFi"); + case "disconnected": + return I18n.tr("Disconnected"); + default: + return NetworkService.networkStatus || I18n.tr("Unknown"); + } + } + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + } + + StyledText { + text: I18n.tr("Primary") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + visible: NetworkService.primaryConnection.length > 0 + } + StyledText { + text: NetworkService.primaryConnection || "-" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + elide: Text.ElideRight + visible: NetworkService.primaryConnection.length > 0 + } + } + + Row { + width: parent.width + spacing: Theme.spacingM + visible: NetworkService.backend === "networkmanager" && NetworkService.ethernetConnected && NetworkService.wifiConnected + + StyledText { + text: I18n.tr("Preference") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: parent.width - preferenceLabel.width - preferenceButtons.width - Theme.spacingM * 2 + height: 1 + } + + DankButtonGroup { + id: preferenceButtons + model: [I18n.tr("Auto"), I18n.tr("Ethernet"), I18n.tr("WiFi")] + currentIndex: { + switch (NetworkService.userPreference) { + case "ethernet": + return 1; + case "wifi": + return 2; + default: + return 0; + } + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + switch (index) { + case 0: + NetworkService.setNetworkPreference("auto"); + break; + case 1: + NetworkService.setNetworkPreference("ethernet"); + break; + case 2: + NetworkService.setNetworkPreference("wifi"); + break; + } + } + } + } + + StyledText { + id: preferenceLabel + visible: false + text: I18n.tr("Preference") + } + } + } + } + } +} diff --git a/quickshell/Modules/Settings/NetworkTab.qml b/quickshell/Modules/Settings/NetworkTab.qml deleted file mode 100644 index 025b3aeb..00000000 --- a/quickshell/Modules/Settings/NetworkTab.qml +++ /dev/null @@ -1,1904 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import Quickshell -import qs.Common -import qs.Modals.Common -import qs.Modals.FileBrowser -import qs.Services -import qs.Widgets - -Item { - id: networkTab - - LayoutMirroring.enabled: I18n.isRtl - LayoutMirroring.childrenInherit: true - - property string expandedVpnUuid: "" - property string expandedWifiSsid: "" - property string expandedEthDevice: "" - property int maxPinnedWifiNetworks: 3 - - Component.onCompleted: { - NetworkService.addRef(); - } - - Component.onDestruction: { - NetworkService.removeRef(); - } - - function openVpnFileBrowser() { - vpnFileBrowserLoader.active = true; - if (vpnFileBrowserLoader.item) - vpnFileBrowserLoader.item.open(); - } - - function normalizePinList(value) { - if (Array.isArray(value)) - return value.filter(v => v); - if (typeof value === "string" && value.length > 0) - return [value]; - return []; - } - - function getPinnedWifiNetworks() { - const pins = SettingsData.wifiNetworkPins || {}; - return normalizePinList(pins["preferredWifi"]); - } - - function toggleWifiPin(ssid) { - const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {})); - let pinnedList = normalizePinList(pins["preferredWifi"]); - const pinIndex = pinnedList.indexOf(ssid); - - if (pinIndex !== -1) { - pinnedList.splice(pinIndex, 1); - } else { - pinnedList.unshift(ssid); - if (pinnedList.length > maxPinnedWifiNetworks) - pinnedList = pinnedList.slice(0, maxPinnedWifiNetworks); - } - - if (pinnedList.length > 0) - pins["preferredWifi"] = pinnedList; - else - delete pins["preferredWifi"]; - - SettingsData.set("wifiNetworkPins", pins); - } - - LazyLoader { - id: vpnFileBrowserLoader - active: false - - FileBrowserModal { - browserTitle: I18n.tr("Import VPN") - browserIcon: "vpn_key" - browserType: "vpn" - fileExtensions: VPNService.getFileFilter() - - onFileSelected: path => { - VPNService.importVpn(path.replace("file://", "")); - } - } - } - - ConfirmModal { - id: deleteVpnConfirm - } - - ConfirmModal { - id: forgetNetworkConfirm - } - - DankFlickable { - anchors.fill: parent - clip: true - contentHeight: mainColumn.height + Theme.spacingXL - contentWidth: width - - Column { - id: mainColumn - topPadding: 4 - - width: Math.min(600, parent.width - Theme.spacingL * 2) - anchors.horizontalCenter: parent.horizontalCenter - spacing: Theme.spacingL - - StyledRect { - width: parent.width - height: overviewSection.implicitHeight + Theme.spacingL * 2 - radius: Theme.cornerRadius - color: Theme.surfaceContainerHigh - - Column { - id: overviewSection - - anchors.fill: parent - anchors.margins: Theme.spacingL - spacing: Theme.spacingM - - Row { - width: parent.width - spacing: Theme.spacingM - - DankIcon { - name: "lan" - size: Theme.iconSize - color: Theme.primary - anchors.verticalCenter: parent.verticalCenter - } - - Column { - width: parent.width - Theme.iconSize - Theme.spacingM - spacing: Theme.spacingXS - anchors.verticalCenter: parent.verticalCenter - - StyledText { - text: I18n.tr("Network Status") - font.pixelSize: Theme.fontSizeLarge - font.weight: Font.Medium - color: Theme.surfaceText - width: parent.width - horizontalAlignment: Text.AlignLeft - } - - StyledText { - text: I18n.tr("Overview of your network connections") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - width: parent.width - horizontalAlignment: Text.AlignLeft - } - } - } - - Rectangle { - width: parent.width - height: 1 - color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) - } - - Grid { - columns: 2 - columnSpacing: Theme.spacingL - rowSpacing: Theme.spacingS - width: parent.width - - StyledText { - text: I18n.tr("Backend") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceVariantText - } - StyledText { - text: NetworkService.backend || I18n.tr("Unknown") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - font.weight: Font.Medium - } - - StyledText { - text: I18n.tr("Status") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceVariantText - } - Row { - spacing: Theme.spacingS - - Rectangle { - width: 8 - height: 8 - radius: 4 - anchors.verticalCenter: parent.verticalCenter - color: { - switch (NetworkService.networkStatus) { - case "ethernet": - case "wifi": - return Theme.success; - case "disconnected": - return Theme.error; - default: - return Theme.warning; - } - } - } - - StyledText { - text: { - switch (NetworkService.networkStatus) { - case "ethernet": - return I18n.tr("Ethernet"); - case "wifi": - return I18n.tr("WiFi"); - case "disconnected": - return I18n.tr("Disconnected"); - default: - return NetworkService.networkStatus || I18n.tr("Unknown"); - } - } - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - font.weight: Font.Medium - } - } - - StyledText { - text: I18n.tr("Primary") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceVariantText - visible: NetworkService.primaryConnection.length > 0 - } - StyledText { - text: NetworkService.primaryConnection || "-" - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - elide: Text.ElideRight - visible: NetworkService.primaryConnection.length > 0 - } - } - - Row { - width: parent.width - spacing: Theme.spacingM - visible: NetworkService.backend === "networkmanager" && NetworkService.ethernetConnected && NetworkService.wifiConnected - - StyledText { - text: I18n.tr("Preference") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceVariantText - anchors.verticalCenter: parent.verticalCenter - } - - Item { - width: parent.width - preferenceLabel.width - preferenceButtons.width - Theme.spacingM * 2 - height: 1 - } - - DankButtonGroup { - id: preferenceButtons - model: [I18n.tr("Auto"), I18n.tr("Ethernet"), I18n.tr("WiFi")] - currentIndex: { - switch (NetworkService.userPreference) { - case "ethernet": - return 1; - case "wifi": - return 2; - default: - return 0; - } - } - onSelectionChanged: (index, selected) => { - if (!selected) - return; - switch (index) { - case 0: - NetworkService.setNetworkPreference("auto"); - break; - case 1: - NetworkService.setNetworkPreference("ethernet"); - break; - case 2: - NetworkService.setNetworkPreference("wifi"); - break; - } - } - } - } - - StyledText { - id: preferenceLabel - visible: false - text: I18n.tr("Preference") - } - } - } - - StyledRect { - width: parent.width - height: ethernetSection.implicitHeight + Theme.spacingL * 2 - radius: Theme.cornerRadius - color: Theme.surfaceContainerHigh - visible: NetworkService.ethernetConnected || (NetworkService.ethernetDevices?.length ?? 0) > 0 - - Column { - id: ethernetSection - - anchors.fill: parent - anchors.margins: Theme.spacingL - spacing: Theme.spacingM - - Row { - width: parent.width - spacing: Theme.spacingM - - DankIcon { - name: "settings_ethernet" - size: Theme.iconSize - color: Theme.primary - anchors.verticalCenter: parent.verticalCenter - } - - Column { - width: parent.width - Theme.iconSize - Theme.spacingM - spacing: Theme.spacingXS - anchors.verticalCenter: parent.verticalCenter - - StyledText { - text: I18n.tr("Ethernet") - font.pixelSize: Theme.fontSizeLarge - font.weight: Font.Medium - color: Theme.surfaceText - width: parent.width - horizontalAlignment: Text.AlignLeft - } - - StyledText { - text: { - const devices = NetworkService.ethernetDevices; - const connected = devices.filter(d => d.connected).length; - if (devices.length === 0) - return I18n.tr("No adapters"); - if (connected === 0) - return devices.length === 1 ? I18n.tr("%1 adapter, none connected").arg(devices.length) : I18n.tr("%1 adapters, none connected").arg(devices.length); - return I18n.tr("%1 connected").arg(connected); - } - font.pixelSize: Theme.fontSizeSmall - color: NetworkService.ethernetConnected ? Theme.primary : Theme.surfaceVariantText - width: parent.width - horizontalAlignment: Text.AlignLeft - } - } - } - - Rectangle { - width: parent.width - height: 1 - color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) - } - - Column { - width: parent.width - spacing: 4 - visible: NetworkService.ethernetDevices.length > 0 - - StyledText { - text: I18n.tr("Adapters") - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Medium - color: Theme.surfaceText - width: parent.width - horizontalAlignment: Text.AlignLeft - } - - Repeater { - model: NetworkService.ethernetDevices - - delegate: Rectangle { - id: ethDeviceDelegate - required property var modelData - required property int index - - readonly property bool isConnected: modelData.connected || false - readonly property bool isExpanded: networkTab.expandedEthDevice === modelData.name - - width: parent.width - height: isExpanded ? 56 + ethExpandedContent.height : 56 - radius: Theme.cornerRadius - color: ethDeviceMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight - border.width: isConnected ? 2 : 0 - border.color: Theme.primary - clip: true - - Behavior on height { - NumberAnimation { - duration: 150 - easing.type: Easing.OutQuad - } - } - - Column { - anchors.fill: parent - spacing: 0 - - Item { - width: parent.width - height: 56 - - Row { - anchors.left: parent.left - anchors.leftMargin: Theme.spacingM - anchors.verticalCenter: parent.verticalCenter - anchors.right: ethDeviceActions.left - anchors.rightMargin: Theme.spacingS - spacing: Theme.spacingS - - DankIcon { - name: "lan" - size: 20 - color: isConnected ? Theme.primary : Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - Column { - anchors.verticalCenter: parent.verticalCenter - spacing: 2 - width: parent.width - 20 - Theme.spacingS - - StyledText { - text: modelData.name || I18n.tr("Unknown") - font.pixelSize: Theme.fontSizeMedium - color: isConnected ? Theme.primary : Theme.surfaceText - font.weight: isConnected ? Font.Medium : Font.Normal - elide: Text.ElideRight - width: parent.width - horizontalAlignment: Text.AlignLeft - } - - Row { - anchors.left: parent.left - spacing: Theme.spacingXS - - StyledText { - text: { - switch (modelData.state) { - case "activated": - return I18n.tr("Connected"); - case "disconnected": - return I18n.tr("Disconnected"); - case "unavailable": - return I18n.tr("Unavailable"); - default: - return modelData.state || I18n.tr("Unknown"); - } - } - font.pixelSize: Theme.fontSizeSmall - color: isConnected ? Theme.primary : Theme.surfaceVariantText - } - - StyledText { - text: "•" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - visible: (modelData.ip || "").length > 0 - } - - StyledText { - text: modelData.ip || "" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - visible: (modelData.ip || "").length > 0 - } - } - } - } - - Row { - id: ethDeviceActions - anchors.right: parent.right - anchors.rightMargin: Theme.spacingS - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingXS - - Rectangle { - width: 28 - height: 28 - radius: 14 - color: ethExpandBtn.containsMouse ? Theme.surfacePressed : "transparent" - visible: isConnected - - DankIcon { - anchors.centerIn: parent - name: isExpanded ? "expand_less" : "expand_more" - size: 18 - color: Theme.surfaceText - } - - MouseArea { - id: ethExpandBtn - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (isExpanded) { - networkTab.expandedEthDevice = ""; - } else { - networkTab.expandedEthDevice = modelData.name; - NetworkService.fetchWiredNetworkInfo(NetworkService.ethernetConnectionUuid); - } - } - } - } - - Rectangle { - width: 28 - height: 28 - radius: 14 - color: ethDisconnectBtn.containsMouse ? Theme.errorHover : "transparent" - visible: isConnected - - DankIcon { - anchors.centerIn: parent - name: "link_off" - size: 18 - color: ethDisconnectBtn.containsMouse ? Theme.error : Theme.surfaceVariantText - } - - MouseArea { - id: ethDisconnectBtn - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: NetworkService.disconnectEthernetDevice(modelData.name) - } - } - } - - MouseArea { - id: ethDeviceMouseArea - anchors.fill: parent - anchors.rightMargin: ethDeviceActions.width + Theme.spacingM - hoverEnabled: true - } - } - - Column { - id: ethExpandedContent - width: parent.width - visible: isExpanded - - Rectangle { - width: parent.width - Theme.spacingM * 2 - height: 1 - x: Theme.spacingM - color: Theme.outlineLight - } - - Item { - width: parent.width - height: ethDetailsColumn.implicitHeight + Theme.spacingM * 2 - - Column { - id: ethDetailsColumn - anchors.fill: parent - anchors.margins: Theme.spacingM - spacing: Theme.spacingS - - Flow { - width: parent.width - spacing: Theme.spacingXS - - Repeater { - model: { - const fields = []; - const dev = modelData; - if (!dev) - return fields; - - if (dev.ip) - fields.push({ - label: I18n.tr("IP"), - value: dev.ip - }); - if (dev.speed && dev.speed > 0) - fields.push({ - label: I18n.tr("Speed"), - value: dev.speed + " Mbps" - }); - if (dev.hwAddress) - fields.push({ - label: I18n.tr("MAC"), - value: dev.hwAddress - }); - if (dev.driver) - fields.push({ - label: I18n.tr("Driver"), - value: dev.driver - }); - fields.push({ - label: I18n.tr("State"), - value: dev.state || I18n.tr("Unknown") - }); - - return fields; - } - - delegate: Rectangle { - required property var modelData - required property int index - - width: ethFieldContent.width + Theme.spacingM * 2 - height: 32 - radius: Theme.cornerRadius - 2 - color: Theme.surfaceContainerHigh - border.width: 1 - border.color: Theme.outlineLight - - Row { - id: ethFieldContent - anchors.centerIn: parent - spacing: Theme.spacingXS - - StyledText { - text: modelData.label + ":" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: modelData.value - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - font.weight: Font.Medium - anchors.verticalCenter: parent.verticalCenter - } - } - } - } - } - - Item { - width: parent.width - height: NetworkService.networkWiredInfoLoading ? 40 : 0 - visible: NetworkService.networkWiredInfoLoading - - DankSpinner { - anchors.centerIn: parent - size: 20 - } - } - } - } - } - } - } - } - } - - Column { - width: parent.width - spacing: Theme.spacingS - visible: NetworkService.wiredConnections.length > 0 - - Rectangle { - width: parent.width - height: 1 - color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) - } - - StyledText { - text: I18n.tr("Saved Configurations") - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Medium - color: Theme.surfaceText - width: parent.width - horizontalAlignment: Text.AlignLeft - } - - Repeater { - model: NetworkService.wiredConnections - - delegate: Rectangle { - required property var modelData - required property int index - - width: parent.width - height: 48 - radius: Theme.cornerRadius - color: wiredMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight - border.width: modelData.isActive ? 2 : 0 - border.color: Theme.primary - - Row { - anchors.left: parent.left - anchors.leftMargin: Theme.spacingM - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingS - - DankIcon { - name: "lan" - size: 20 - color: modelData.isActive ? Theme.primary : Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - Column { - anchors.verticalCenter: parent.verticalCenter - spacing: 2 - - StyledText { - text: modelData.id || I18n.tr("Unknown") - font.pixelSize: Theme.fontSizeMedium - color: modelData.isActive ? Theme.primary : Theme.surfaceText - font.weight: modelData.isActive ? Font.Medium : Font.Normal - } - - StyledText { - text: modelData.isActive ? I18n.tr("Active") : "" - font.pixelSize: Theme.fontSizeSmall - color: Theme.primary - visible: modelData.isActive - } - } - } - - MouseArea { - id: wiredMouseArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (!modelData.isActive) { - NetworkService.connectToSpecificWiredConfig(modelData.uuid); - } - } - } - } - } - } - } - } - - StyledRect { - width: parent.width - height: wifiSection.implicitHeight + Theme.spacingL * 2 - radius: Theme.cornerRadius - color: Theme.surfaceContainerHigh - - Column { - id: wifiSection - - anchors.fill: parent - anchors.margins: Theme.spacingL - spacing: Theme.spacingM - - Row { - width: parent.width - spacing: Theme.spacingM - - DankIcon { - name: NetworkService.wifiEnabled ? "wifi" : "wifi_off" - size: Theme.iconSize - color: Theme.primary - anchors.verticalCenter: parent.verticalCenter - } - - Column { - width: parent.width - Theme.iconSize - Theme.spacingM - wifiControls.width - Theme.spacingM - spacing: Theme.spacingXS - anchors.verticalCenter: parent.verticalCenter - - StyledText { - text: I18n.tr("WiFi") - font.pixelSize: Theme.fontSizeLarge - font.weight: Font.Medium - color: Theme.surfaceText - width: parent.width - horizontalAlignment: Text.AlignLeft - } - - StyledText { - text: { - if (NetworkService.wifiToggling) - return I18n.tr("Toggling..."); - if (!NetworkService.wifiEnabled) - return I18n.tr("Disabled"); - if (NetworkService.wifiConnected) - return NetworkService.currentWifiSSID; - return I18n.tr("Not connected"); - } - font.pixelSize: Theme.fontSizeSmall - color: NetworkService.wifiConnected ? Theme.primary : Theme.surfaceVariantText - width: parent.width - horizontalAlignment: Text.AlignLeft - } - } - - Row { - id: wifiControls - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingS - - DankActionButton { - iconName: "wifi_find" - buttonSize: 32 - visible: NetworkService.backend === "networkmanager" && NetworkService.wifiEnabled && !NetworkService.wifiToggling - onClicked: PopoutService.showHiddenNetworkModal() - } - - DankActionButton { - iconName: "refresh" - buttonSize: 32 - visible: NetworkService.wifiEnabled && !NetworkService.wifiToggling && !NetworkService.isScanning - onClicked: NetworkService.scanWifi() - } - - DankToggle { - checked: NetworkService.wifiEnabled - enabled: !NetworkService.wifiToggling - onToggled: NetworkService.toggleWifiRadio() - } - } - } - - Row { - width: parent.width - spacing: Theme.spacingM - visible: NetworkService.wifiEnabled && (NetworkService.wifiDevices?.length ?? 0) > 1 - - StyledText { - text: I18n.tr("WiFi Device") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceVariantText - anchors.verticalCenter: parent.verticalCenter - } - - Item { - width: parent.width - wifiDeviceLabel.width - wifiDeviceDropdown.width - Theme.spacingM * 2 - height: 1 - } - - DankDropdown { - id: wifiDeviceDropdown - dropdownWidth: 150 - popupWidth: 180 - currentValue: NetworkService.wifiDeviceOverride || I18n.tr("Auto") - options: { - const devices = NetworkService.wifiDevices; - if (!devices || devices.length === 0) - return [I18n.tr("Auto")]; - return [I18n.tr("Auto")].concat(devices.map(d => d.name)); - } - onValueChanged: value => { - const deviceName = value === I18n.tr("Auto") ? "" : value; - NetworkService.setWifiDeviceOverride(deviceName); - } - } - } - - StyledText { - id: wifiDeviceLabel - visible: false - text: I18n.tr("WiFi Device") - } - - Rectangle { - width: parent.width - height: 1 - color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) - visible: NetworkService.wifiEnabled - } - - Column { - width: parent.width - spacing: Theme.spacingS - visible: NetworkService.wifiEnabled && !NetworkService.wifiToggling - - Column { - width: parent.width - spacing: Theme.spacingS - visible: NetworkService.wifiInterface.length > 0 - - Row { - width: parent.width - height: 24 - - StyledText { - text: I18n.tr("Interface:") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceVariantText - width: 100 - anchors.verticalCenter: parent.verticalCenter - } - StyledText { - text: NetworkService.wifiInterface || "-" - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - } - - Row { - width: parent.width - height: 24 - visible: NetworkService.wifiIP.length > 0 - - StyledText { - text: I18n.tr("IP Address:") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceVariantText - width: 100 - anchors.verticalCenter: parent.verticalCenter - } - StyledText { - text: NetworkService.wifiIP || "-" - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - } - - Row { - width: parent.width - height: 24 - visible: NetworkService.wifiConnected - - StyledText { - text: I18n.tr("Signal:") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceVariantText - width: 100 - anchors.verticalCenter: parent.verticalCenter - } - - Row { - spacing: Theme.spacingXS - anchors.verticalCenter: parent.verticalCenter - - DankIcon { - name: { - const s = NetworkService.wifiSignalStrength; - if (s >= 50) - return "wifi"; - if (s >= 25) - return "wifi_2_bar"; - return "wifi_1_bar"; - } - size: 18 - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: NetworkService.wifiSignalStrength + "%" - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - } - } - } - } - - Item { - width: parent.width - height: Theme.spacingS - } - - Row { - width: parent.width - spacing: Theme.spacingM - - StyledText { - text: I18n.tr("Available Networks") - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Medium - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - Item { - width: 1 - height: 1 - Layout.fillWidth: true - } - - StyledText { - text: NetworkService.wifiNetworks?.length ?? 0 - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - anchors.verticalCenter: parent.verticalCenter - } - } - - Item { - width: parent.width - height: 80 - visible: NetworkService.isScanning && (NetworkService.wifiNetworks?.length ?? 0) === 0 - - Column { - anchors.centerIn: parent - spacing: Theme.spacingS - - DankIcon { - id: scanningIcon - name: "wifi_find" - size: 32 - color: Theme.surfaceVariantText - anchors.horizontalCenter: parent.horizontalCenter - - SequentialAnimation { - running: NetworkService.isScanning - loops: Animation.Infinite - OpacityAnimator { - target: scanningIcon - to: 0.3 - duration: 400 - easing.type: Easing.InOutQuad - } - OpacityAnimator { - target: scanningIcon - to: 1.0 - duration: 400 - easing.type: Easing.InOutQuad - } - onRunningChanged: if (!running) - scanningIcon.opacity = 1.0 - } - } - - StyledText { - text: I18n.tr("Scanning...") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - anchors.horizontalCenter: parent.horizontalCenter - } - } - } - - Column { - width: parent.width - spacing: 4 - visible: (NetworkService.wifiNetworks?.length ?? 0) > 0 - - Repeater { - model: { - const ssid = NetworkService.currentWifiSSID; - const networks = NetworkService.wifiNetworks || []; - const pinnedList = networkTab.getPinnedWifiNetworks(); - - let sorted = [...networks]; - sorted.sort((a, b) => { - const aPinnedIndex = pinnedList.indexOf(a.ssid); - const bPinnedIndex = pinnedList.indexOf(b.ssid); - if (aPinnedIndex !== -1 || bPinnedIndex !== -1) { - if (aPinnedIndex === -1) - return 1; - if (bPinnedIndex === -1) - return -1; - return aPinnedIndex - bPinnedIndex; - } - if (a.ssid === ssid) - return -1; - if (b.ssid === ssid) - return 1; - return b.signal - a.signal; - }); - return sorted; - } - - delegate: Rectangle { - id: wifiNetworkDelegate - required property var modelData - required property int index - - readonly property bool isConnected: modelData.ssid === NetworkService.currentWifiSSID - readonly property bool isPinned: networkTab.getPinnedWifiNetworks().includes(modelData.ssid) - readonly property bool isExpanded: networkTab.expandedWifiSsid === modelData.ssid - - width: parent.width - height: isExpanded ? 56 + wifiExpandedContent.height : 56 - radius: Theme.cornerRadius - color: wifiNetworkMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight - border.width: isConnected ? 2 : 0 - border.color: Theme.primary - clip: true - - Behavior on height { - NumberAnimation { - duration: 150 - easing.type: Easing.OutQuad - } - } - - Column { - anchors.fill: parent - spacing: 0 - - Item { - width: parent.width - height: 56 - - Row { - anchors.left: parent.left - anchors.leftMargin: Theme.spacingM - anchors.verticalCenter: parent.verticalCenter - anchors.right: wifiNetworkActions.left - anchors.rightMargin: Theme.spacingS - spacing: Theme.spacingS - - DankIcon { - name: { - const s = modelData.signal || 0; - if (s >= 50) - return "wifi"; - if (s >= 25) - return "wifi_2_bar"; - return "wifi_1_bar"; - } - size: 20 - color: isConnected ? Theme.primary : Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - Column { - anchors.verticalCenter: parent.verticalCenter - spacing: 2 - width: parent.width - 20 - Theme.spacingS - - Row { - anchors.left: parent.left - spacing: Theme.spacingXS - - StyledText { - text: modelData.ssid || I18n.tr("Unknown") - font.pixelSize: Theme.fontSizeMedium - color: isConnected ? Theme.primary : Theme.surfaceText - font.weight: isConnected ? Font.Medium : Font.Normal - elide: Text.ElideRight - } - - DankIcon { - name: "push_pin" - size: 14 - color: Theme.primary - visible: isPinned - anchors.verticalCenter: parent.verticalCenter - } - - DankIcon { - name: "visibility_off" - size: 14 - color: Theme.surfaceVariantText - visible: modelData.hidden || false - anchors.verticalCenter: parent.verticalCenter - } - } - - Row { - anchors.left: parent.left - spacing: Theme.spacingXS - - StyledText { - text: isConnected ? I18n.tr("Connected") : (modelData.secured ? I18n.tr("Secured") : I18n.tr("Open")) - font.pixelSize: Theme.fontSizeSmall - color: isConnected ? Theme.primary : Theme.surfaceVariantText - } - - StyledText { - text: "•" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - visible: modelData.saved - } - - StyledText { - text: I18n.tr("Saved") - font.pixelSize: Theme.fontSizeSmall - color: Theme.primary - visible: modelData.saved - } - - StyledText { - text: "•" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - visible: modelData.hidden || false - } - - StyledText { - text: I18n.tr("Hidden") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - visible: modelData.hidden || false - } - - StyledText { - text: "•" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - } - - StyledText { - text: modelData.signal + "%" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - } - } - } - } - - Row { - id: wifiNetworkActions - anchors.right: parent.right - anchors.rightMargin: Theme.spacingS - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingXS - - Rectangle { - width: 28 - height: 28 - radius: 14 - color: wifiExpandBtn.containsMouse ? Theme.surfacePressed : "transparent" - visible: isConnected || modelData.saved - - DankIcon { - anchors.centerIn: parent - name: isExpanded ? "expand_less" : "expand_more" - size: 18 - color: Theme.surfaceText - } - - MouseArea { - id: wifiExpandBtn - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (isExpanded) { - networkTab.expandedWifiSsid = ""; - } else { - networkTab.expandedWifiSsid = modelData.ssid; - NetworkService.fetchNetworkInfo(modelData.ssid); - } - } - } - } - - DankActionButton { - iconName: "qr_code" - buttonSize: 28 - visible: modelData.secured && modelData.saved - onClicked: { - PopoutService.showWifiQRCodeModal(modelData.ssid); - } - } - - DankActionButton { - iconName: isPinned ? "push_pin" : "push_pin" - buttonSize: 28 - iconColor: isPinned ? Theme.primary : Theme.surfaceVariantText - onClicked: { - networkTab.toggleWifiPin(modelData.ssid); - } - } - - DankActionButton { - iconName: "delete" - buttonSize: 28 - iconColor: Theme.error - visible: modelData.saved || isConnected - onClicked: { - forgetNetworkConfirm.showWithOptions({ - title: I18n.tr("Forget Network"), - message: I18n.tr("Forget \"%1\"?").arg(modelData.ssid), - confirmText: I18n.tr("Forget"), - confirmColor: Theme.error, - onConfirm: () => NetworkService.forgetWifiNetwork(modelData.ssid) - }); - } - } - } - - MouseArea { - id: wifiNetworkMouseArea - - anchors.fill: parent - anchors.rightMargin: wifiNetworkActions.width + Theme.spacingM - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (isConnected) { - NetworkService.disconnectWifi(); - return; - } - if (modelData.secured && !modelData.saved && (DMSService.apiVersion < 7 || modelData.enterprise)) { - PopoutService.showWifiPasswordModal(modelData.ssid); - return; - } - NetworkService.connectToWifi(modelData.ssid); - } - } - } - - Column { - id: wifiExpandedContent - width: parent.width - visible: isExpanded - - Rectangle { - width: parent.width - Theme.spacingM * 2 - height: 1 - x: Theme.spacingM - color: Theme.outlineLight - } - - Item { - width: parent.width - height: wifiDetailsColumn.implicitHeight + Theme.spacingM * 2 - - Column { - id: wifiDetailsColumn - anchors.fill: parent - anchors.margins: Theme.spacingM - spacing: Theme.spacingS - - Item { - width: parent.width - height: NetworkService.networkInfoLoading ? 40 : 0 - visible: NetworkService.networkInfoLoading - - DankSpinner { - anchors.centerIn: parent - size: 20 - } - } - - Flow { - width: parent.width - spacing: Theme.spacingXS - visible: !NetworkService.networkInfoLoading - - Repeater { - model: { - const fields = []; - const net = modelData; - if (!net) - return fields; - - fields.push({ - label: I18n.tr("Signal"), - value: net.signal + "%" - }); - if (net.frequency) - fields.push({ - label: I18n.tr("Frequency"), - value: (net.frequency / 1000).toFixed(1) + " GHz" - }); - if (net.channel) - fields.push({ - label: I18n.tr("Channel"), - value: String(net.channel) - }); - if (net.rate) - fields.push({ - label: I18n.tr("Rate"), - value: net.rate + " Mbps" - }); - if (net.mode) - fields.push({ - label: I18n.tr("Mode"), - value: net.mode - }); - if (net.bssid) - fields.push({ - label: I18n.tr("BSSID"), - value: net.bssid - }); - fields.push({ - label: I18n.tr("Security"), - value: net.secured ? (net.enterprise ? I18n.tr("Enterprise") : I18n.tr("WPA/WPA2")) : I18n.tr("Open") - }); - - return fields; - } - - delegate: Rectangle { - required property var modelData - required property int index - - width: wifiFieldContent.width + Theme.spacingM * 2 - height: 32 - radius: Theme.cornerRadius - 2 - color: Theme.surfaceContainerHigh - border.width: 1 - border.color: Theme.outlineLight - - Row { - id: wifiFieldContent - anchors.centerIn: parent - spacing: Theme.spacingXS - - StyledText { - text: modelData.label + ":" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: modelData.value - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - font.weight: Font.Medium - anchors.verticalCenter: parent.verticalCenter - } - } - } - } - } - - Row { - spacing: Theme.spacingS - visible: (modelData.saved || isConnected) && DMSService.apiVersion > 13 - - DankToggle { - id: autoconnectToggle - text: I18n.tr("Autoconnect") - checked: modelData.autoconnect || false - onToggled: checked => { - NetworkService.setWifiAutoconnect(modelData.ssid, checked); - } - } - } - } - } - } - } - } - } - } - } - } - } - - StyledRect { - width: parent.width - height: vpnSection.implicitHeight + Theme.spacingL * 2 - radius: Theme.cornerRadius - color: Theme.surfaceContainerHigh - visible: DMSNetworkService.vpnAvailable - - Column { - id: vpnSection - - anchors.fill: parent - anchors.margins: Theme.spacingL - spacing: Theme.spacingM - - Row { - width: parent.width - spacing: Theme.spacingM - - DankIcon { - name: DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off" - size: Theme.iconSize - color: Theme.primary - anchors.verticalCenter: parent.verticalCenter - } - - Column { - width: parent.width - Theme.iconSize - Theme.spacingM - vpnHeaderControls.width - Theme.spacingM - spacing: Theme.spacingXS - anchors.verticalCenter: parent.verticalCenter - - StyledText { - text: I18n.tr("VPN") - font.pixelSize: Theme.fontSizeLarge - font.weight: Font.Medium - color: Theme.surfaceText - width: parent.width - horizontalAlignment: Text.AlignLeft - } - - StyledText { - text: { - if (!DMSNetworkService.connected) - return I18n.tr("Disconnected"); - const names = DMSNetworkService.activeNames || []; - if (names.length <= 1) - return names[0] || I18n.tr("Connected"); - return names[0] + " +" + (names.length - 1); - } - font.pixelSize: Theme.fontSizeSmall - color: DMSNetworkService.connected ? Theme.primary : Theme.surfaceVariantText - width: parent.width - horizontalAlignment: Text.AlignLeft - } - } - - Row { - id: vpnHeaderControls - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingS - - Rectangle { - height: 28 - radius: 14 - width: importVpnRow.width + Theme.spacingM * 2 - color: importVpnArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight - opacity: VPNService.importing ? 0.5 : 1.0 - - Row { - id: importVpnRow - anchors.centerIn: parent - spacing: Theme.spacingXS - - DankIcon { - name: VPNService.importing ? "sync" : "add" - size: Theme.fontSizeSmall - color: Theme.primary - } - - StyledText { - text: I18n.tr("Import") - font.pixelSize: Theme.fontSizeSmall - color: Theme.primary - font.weight: Font.Medium - } - } - - MouseArea { - id: importVpnArea - anchors.fill: parent - hoverEnabled: true - cursorShape: VPNService.importing ? Qt.BusyCursor : Qt.PointingHandCursor - enabled: !VPNService.importing - onClicked: networkTab.openVpnFileBrowser() - } - } - - Rectangle { - height: 28 - radius: 14 - width: disconnectAllRow.width + Theme.spacingM * 2 - color: disconnectAllArea.containsMouse ? Theme.errorHover : Theme.surfaceLight - visible: DMSNetworkService.connected - opacity: DMSNetworkService.isBusy ? 0.5 : 1.0 - - Row { - id: disconnectAllRow - anchors.centerIn: parent - spacing: Theme.spacingXS - - DankIcon { - name: "link_off" - size: Theme.fontSizeSmall - color: Theme.surfaceText - } - - StyledText { - text: I18n.tr("Disconnect") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - font.weight: Font.Medium - } - } - - MouseArea { - id: disconnectAllArea - anchors.fill: parent - hoverEnabled: true - cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor - enabled: !DMSNetworkService.isBusy - onClicked: DMSNetworkService.disconnectAllActive() - } - } - } - } - - Rectangle { - width: parent.width - height: 1 - color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) - } - - Item { - width: parent.width - height: 100 - visible: DMSNetworkService.profiles.length === 0 - - Column { - anchors.centerIn: parent - spacing: Theme.spacingS - - DankIcon { - name: "vpn_key_off" - size: 36 - color: Theme.surfaceVariantText - anchors.horizontalCenter: parent.horizontalCenter - } - - StyledText { - text: I18n.tr("No VPN profiles") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceVariantText - anchors.horizontalCenter: parent.horizontalCenter - } - - StyledText { - text: I18n.tr("Click Import to add a .ovpn or .conf") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - anchors.horizontalCenter: parent.horizontalCenter - } - } - } - - Column { - width: parent.width - spacing: 4 - visible: DMSNetworkService.profiles.length > 0 - - Repeater { - model: DMSNetworkService.profiles - - delegate: Rectangle { - id: vpnProfileRow - required property var modelData - required property int index - - readonly property bool isActive: DMSNetworkService.isActiveUuid(modelData.uuid) - readonly property bool isTransient: !!modelData.transient - readonly property bool canExpand: modelData.canExpand !== false - readonly property bool canDelete: modelData.canDelete !== false - readonly property bool isExpanded: networkTab.expandedVpnUuid === modelData.uuid - readonly property var configData: (!isTransient && isExpanded) ? VPNService.editConfig : null - - width: parent.width - height: isExpanded ? 56 + vpnExpandedContent.height : 56 - radius: Theme.cornerRadius - color: vpnRowArea.containsMouse ? Theme.primaryHoverLight : (isActive ? Theme.primaryPressed : Theme.surfaceLight) - border.width: isActive ? 2 : 0 - border.color: Theme.primary - opacity: DMSNetworkService.isBusy ? 0.6 : 1.0 - clip: true - - Behavior on height { - NumberAnimation { - duration: 150 - easing.type: Easing.OutQuad - } - } - - MouseArea { - id: vpnRowArea - anchors.fill: parent - hoverEnabled: true - cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor - enabled: !DMSNetworkService.isBusy - onClicked: DMSNetworkService.toggle(modelData.uuid) - } - - Column { - anchors.fill: parent - anchors.margins: Theme.spacingS - spacing: Theme.spacingS - - Row { - width: parent.width - height: 56 - Theme.spacingS * 2 - spacing: Theme.spacingS - - DankIcon { - name: isActive ? "vpn_lock" : "vpn_key_off" - size: 20 - color: isActive ? Theme.primary : Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - Column { - spacing: 2 - anchors.verticalCenter: parent.verticalCenter - width: parent.width - 20 - ((canExpand ? 28 : 0) + (canDelete ? 28 : 0)) - Theme.spacingS * 4 - - StyledText { - text: modelData.name - font.pixelSize: Theme.fontSizeMedium - color: isActive ? Theme.primary : Theme.surfaceText - elide: Text.ElideRight - width: parent.width - horizontalAlignment: Text.AlignLeft - } - - StyledText { - text: VPNService.getVpnTypeFromProfile(modelData) - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - anchors.left: parent.left - } - } - - Item { - width: Theme.spacingXS - height: 1 - } - - Rectangle { - width: 28 - height: 28 - radius: 14 - color: vpnExpandBtn.containsMouse ? Theme.surfacePressed : "transparent" - anchors.verticalCenter: parent.verticalCenter - visible: canExpand - - DankIcon { - anchors.centerIn: parent - name: isExpanded ? "expand_less" : "expand_more" - size: 18 - color: Theme.surfaceText - } - - MouseArea { - id: vpnExpandBtn - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (isExpanded) { - networkTab.expandedVpnUuid = ""; - } else { - networkTab.expandedVpnUuid = modelData.uuid; - VPNService.getConfig(modelData.uuid); - } - } - } - } - - Rectangle { - width: 28 - height: 28 - radius: 14 - color: vpnDeleteBtn.containsMouse ? Theme.errorHover : "transparent" - anchors.verticalCenter: parent.verticalCenter - visible: canDelete - - DankIcon { - anchors.centerIn: parent - name: "delete" - size: 18 - color: vpnDeleteBtn.containsMouse ? Theme.error : Theme.surfaceVariantText - } - - MouseArea { - id: vpnDeleteBtn - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - deleteVpnConfirm.showWithOptions({ - title: I18n.tr("Delete VPN"), - message: I18n.tr("Delete \"%1\"?").arg(modelData.name), - confirmText: I18n.tr("Delete"), - confirmColor: Theme.error, - onConfirm: () => VPNService.deleteVpn(modelData.uuid) - }); - } - } - } - } - - Column { - id: vpnExpandedContent - width: parent.width - spacing: Theme.spacingXS - visible: !isTransient && isExpanded - - Rectangle { - width: parent.width - height: 1 - color: Theme.outlineLight - } - - Item { - width: parent.width - height: VPNService.configLoading ? 40 : 0 - visible: VPNService.configLoading - - DankSpinner { - anchors.centerIn: parent - size: 20 - } - } - - Flow { - width: parent.width - spacing: Theme.spacingXS - visible: !VPNService.configLoading && configData - - Repeater { - model: { - if (!configData) - return []; - const fields = []; - const data = configData.data || {}; - - if (data.remote) - fields.push({ - label: I18n.tr("Server"), - value: data.remote - }); - if (configData.username || data.username) - fields.push({ - label: I18n.tr("Username"), - value: configData.username || data.username - }); - if (data.cipher) - fields.push({ - label: I18n.tr("Cipher"), - value: data.cipher - }); - if (data.auth) - fields.push({ - label: I18n.tr("Auth"), - value: data.auth - }); - if (data["proto-tcp"] === "yes" || data["proto-tcp"] === "no") - fields.push({ - label: I18n.tr("Protocol"), - value: data["proto-tcp"] === "yes" ? "TCP" : "UDP" - }); - if (data["tunnel-mtu"]) - fields.push({ - label: I18n.tr("MTU"), - value: data["tunnel-mtu"] - }); - if (data["connection-type"]) - fields.push({ - label: I18n.tr("Auth Type"), - value: data["connection-type"] - }); - return fields; - } - - delegate: Rectangle { - required property var modelData - required property int index - - width: vpnFieldContent.width + Theme.spacingM * 2 - height: 32 - radius: Theme.cornerRadius - 2 - color: Theme.surfaceContainerHigh - border.width: 1 - border.color: Theme.outlineLight - - Row { - id: vpnFieldContent - anchors.centerIn: parent - spacing: Theme.spacingXS - - StyledText { - text: modelData.label + ":" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: modelData.value - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - font.weight: Font.Medium - anchors.verticalCenter: parent.verticalCenter - } - } - } - } - } - - DankToggle { - width: parent.width - text: I18n.tr("Autoconnect") - checked: configData ? (configData.autoconnect || false) : false - visible: !VPNService.configLoading && configData !== null - onToggled: checked => { - VPNService.updateConfig(modelData.uuid, { - autoconnect: checked - }); - } - } - - Item { - width: 1 - height: Theme.spacingXS - } - } - } - } - } - } - } - } - } - } -} diff --git a/quickshell/Modules/Settings/NetworkVpnTab.qml b/quickshell/Modules/Settings/NetworkVpnTab.qml new file mode 100644 index 00000000..2714cab9 --- /dev/null +++ b/quickshell/Modules/Settings/NetworkVpnTab.qml @@ -0,0 +1,516 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import qs.Common +import qs.Modules.Settings.Widgets +import qs.Modals.Common +import qs.Modals.FileBrowser +import qs.Services +import qs.Widgets + +Item { + id: networkVpnTab + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + Component.onCompleted: { + NetworkService.addRef(); + } + + Component.onDestruction: { + NetworkService.removeRef(); + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + + topPadding: 4 + width: Math.min(600, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingL + + SettingsCard { + id: root + + property string expandedVpnUuid: "" + + title: I18n.tr("VPN") + iconName: "vpn_key" + settingKey: "networkVpn" + tags: ["vpn", "network", "profiles", "import", "openvpn", "wireguard"] + + function openVpnFileBrowser() { + vpnFileBrowserLoader.active = true; + if (vpnFileBrowserLoader.item) + vpnFileBrowserLoader.item.open(); + } + + property var vpnFileBrowserLoader: LazyLoader { + active: false + + FileBrowserModal { + browserTitle: I18n.tr("Import VPN") + browserIcon: "vpn_key" + browserType: "vpn" + fileExtensions: VPNService.getFileFilter() + + onFileSelected: path => { + VPNService.importVpn(path.replace("file://", "")); + } + } + } + + property var deleteVpnConfirm: ConfirmModal {} + + width: parent.width + + Column { + id: vpnSection + + width: parent.width + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Unavailable") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + horizontalAlignment: Text.AlignLeft + visible: !DMSNetworkService.vpnAvailable + } + + Row { + width: parent.width + spacing: Theme.spacingM + visible: DMSNetworkService.vpnAvailable + + StyledText { + text: { + if (!DMSNetworkService.connected) + return I18n.tr("Disconnected"); + const names = DMSNetworkService.activeNames || []; + if (names.length <= 1) + return names[0] || I18n.tr("Connected"); + return names[0] + " +" + (names.length - 1); + } + font.pixelSize: Theme.fontSizeSmall + color: DMSNetworkService.connected ? Theme.primary : Theme.surfaceVariantText + width: parent.width - vpnHeaderControls.width - Theme.spacingM + horizontalAlignment: Text.AlignLeft + anchors.verticalCenter: parent.verticalCenter + } + + Row { + id: vpnHeaderControls + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + Rectangle { + height: 28 + radius: 14 + width: importVpnRow.width + Theme.spacingM * 2 + color: importVpnArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight + opacity: VPNService.importing ? 0.5 : 1.0 + + Row { + id: importVpnRow + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: VPNService.importing ? "sync" : "add" + size: Theme.fontSizeSmall + color: Theme.primary + } + + StyledText { + text: I18n.tr("Import") + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + font.weight: Font.Medium + } + } + + MouseArea { + id: importVpnArea + anchors.fill: parent + hoverEnabled: true + cursorShape: VPNService.importing ? Qt.BusyCursor : Qt.PointingHandCursor + enabled: !VPNService.importing + onClicked: root.openVpnFileBrowser() + } + } + + Rectangle { + height: 28 + radius: 14 + width: disconnectAllRow.width + Theme.spacingM * 2 + color: disconnectAllArea.containsMouse ? Theme.errorHover : Theme.surfaceLight + visible: DMSNetworkService.connected + opacity: DMSNetworkService.isBusy ? 0.5 : 1.0 + + Row { + id: disconnectAllRow + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: "link_off" + size: Theme.fontSizeSmall + color: Theme.surfaceText + } + + StyledText { + text: I18n.tr("Disconnect") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + } + + MouseArea { + id: disconnectAllArea + anchors.fill: parent + hoverEnabled: true + cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor + enabled: !DMSNetworkService.isBusy + onClicked: DMSNetworkService.disconnectAllActive() + } + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + visible: DMSNetworkService.vpnAvailable + } + + Item { + width: parent.width + height: 100 + visible: DMSNetworkService.vpnAvailable && DMSNetworkService.profiles.length === 0 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: "vpn_key_off" + size: 36 + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: I18n.tr("No VPN profiles") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: I18n.tr("Click Import to add a .ovpn or .conf") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + Column { + width: parent.width + spacing: 4 + visible: DMSNetworkService.vpnAvailable && DMSNetworkService.profiles.length > 0 + + Repeater { + model: DMSNetworkService.profiles + + delegate: Rectangle { + id: vpnProfileRow + required property var modelData + required property int index + + readonly property bool isActive: DMSNetworkService.isActiveUuid(modelData.uuid) + readonly property bool isTransient: !!modelData.transient + readonly property bool canExpand: modelData.canExpand !== false + readonly property bool canDelete: modelData.canDelete !== false + readonly property bool isExpanded: root.expandedVpnUuid === modelData.uuid + readonly property var configData: (!isTransient && isExpanded) ? VPNService.editConfig : null + + width: parent.width + height: isExpanded ? 56 + vpnExpandedContent.height : 56 + radius: Theme.cornerRadius + color: vpnRowArea.containsMouse ? Theme.primaryHoverLight : (isActive ? Theme.primaryPressed : Theme.surfaceLight) + border.width: isActive ? 2 : 0 + border.color: Theme.primary + opacity: DMSNetworkService.isBusy ? 0.6 : 1.0 + clip: true + + Behavior on height { + NumberAnimation { + duration: 150 + easing.type: Easing.OutQuad + } + } + + MouseArea { + id: vpnRowArea + anchors.fill: parent + hoverEnabled: true + cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor + enabled: !DMSNetworkService.isBusy + onClicked: DMSNetworkService.toggle(modelData.uuid) + } + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: Theme.spacingS + + Row { + width: parent.width + height: 56 - Theme.spacingS * 2 + spacing: Theme.spacingS + + DankIcon { + name: isActive ? "vpn_lock" : "vpn_key_off" + size: 20 + color: isActive ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: 2 + anchors.verticalCenter: parent.verticalCenter + width: parent.width - 20 - ((canExpand ? 28 : 0) + (canDelete ? 28 : 0)) - Theme.spacingS * 4 + + StyledText { + text: modelData.name + font.pixelSize: Theme.fontSizeMedium + color: isActive ? Theme.primary : Theme.surfaceText + elide: Text.ElideRight + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: VPNService.getVpnTypeFromProfile(modelData) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.left: parent.left + } + } + + Item { + width: Theme.spacingXS + height: 1 + } + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: vpnExpandBtn.containsMouse ? Theme.surfacePressed : "transparent" + anchors.verticalCenter: parent.verticalCenter + visible: canExpand + + DankIcon { + anchors.centerIn: parent + name: isExpanded ? "expand_less" : "expand_more" + size: 18 + color: Theme.surfaceText + } + + MouseArea { + id: vpnExpandBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (isExpanded) { + root.expandedVpnUuid = ""; + } else { + root.expandedVpnUuid = modelData.uuid; + VPNService.getConfig(modelData.uuid); + } + } + } + } + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: vpnDeleteBtn.containsMouse ? Theme.errorHover : "transparent" + anchors.verticalCenter: parent.verticalCenter + visible: canDelete + + DankIcon { + anchors.centerIn: parent + name: "delete" + size: 18 + color: vpnDeleteBtn.containsMouse ? Theme.error : Theme.surfaceVariantText + } + + MouseArea { + id: vpnDeleteBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + deleteVpnConfirm.showWithOptions({ + title: I18n.tr("Delete VPN"), + message: I18n.tr("Delete \"%1\"?").arg(modelData.name), + confirmText: I18n.tr("Delete"), + confirmColor: Theme.error, + onConfirm: () => VPNService.deleteVpn(modelData.uuid) + }); + } + } + } + } + + Column { + id: vpnExpandedContent + width: parent.width + spacing: Theme.spacingXS + visible: !isTransient && isExpanded + + Rectangle { + width: parent.width + height: 1 + color: Theme.outlineLight + } + + Item { + width: parent.width + height: VPNService.configLoading ? 40 : 0 + visible: VPNService.configLoading + + DankSpinner { + anchors.centerIn: parent + size: 20 + } + } + + Flow { + width: parent.width + spacing: Theme.spacingXS + visible: !VPNService.configLoading && configData + + Repeater { + model: { + if (!configData) + return []; + const fields = []; + const data = configData.data || {}; + + if (data.remote) + fields.push({ + label: I18n.tr("Server"), + value: data.remote + }); + if (configData.username || data.username) + fields.push({ + label: I18n.tr("Username"), + value: configData.username || data.username + }); + if (data.cipher) + fields.push({ + label: I18n.tr("Cipher"), + value: data.cipher + }); + if (data.auth) + fields.push({ + label: I18n.tr("Auth"), + value: data.auth + }); + if (data["proto-tcp"] === "yes" || data["proto-tcp"] === "no") + fields.push({ + label: I18n.tr("Protocol"), + value: data["proto-tcp"] === "yes" ? "TCP" : "UDP" + }); + if (data["tunnel-mtu"]) + fields.push({ + label: I18n.tr("MTU"), + value: data["tunnel-mtu"] + }); + if (data["connection-type"]) + fields.push({ + label: I18n.tr("Auth Type"), + value: data["connection-type"] + }); + return fields; + } + + delegate: Rectangle { + required property var modelData + required property int index + + width: vpnFieldContent.width + Theme.spacingM * 2 + height: 32 + radius: Theme.cornerRadius - 2 + color: Theme.surfaceContainerHigh + border.width: 1 + border.color: Theme.outlineLight + + Row { + id: vpnFieldContent + anchors.centerIn: parent + spacing: Theme.spacingXS + + StyledText { + text: modelData.label + ":" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: modelData.value + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + } + + DankToggle { + width: parent.width + text: I18n.tr("Autoconnect") + checked: configData ? (configData.autoconnect || false) : false + visible: !VPNService.configLoading && configData !== null + onToggled: checked => { + VPNService.updateConfig(modelData.uuid, { + autoconnect: checked + }); + } + } + + Item { + width: 1 + height: Theme.spacingXS + } + } + } + } + } + } + } + } + } + } +} diff --git a/quickshell/Modules/Settings/NetworkWifiTab.qml b/quickshell/Modules/Settings/NetworkWifiTab.qml new file mode 100644 index 00000000..bb39ad6b --- /dev/null +++ b/quickshell/Modules/Settings/NetworkWifiTab.qml @@ -0,0 +1,761 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import qs.Common +import qs.Modules.Settings.Widgets +import qs.Modals.Common +import qs.Services +import qs.Widgets + +Item { + id: networkWifiTab + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + Component.onCompleted: { + NetworkService.addRef(); + } + + Component.onDestruction: { + NetworkService.removeRef(); + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + + topPadding: 4 + width: Math.min(600, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingL + + SettingsCard { + id: root + + property string expandedWifiSsid: "" + property int maxPinnedWifiNetworks: 3 + + function normalizePinList(value) { + if (Array.isArray(value)) + return value.filter(v => v); + if (typeof value === "string" && value.length > 0) + return [value]; + return []; + } + + function getPinnedWifiNetworks() { + const pins = SettingsData.wifiNetworkPins || {}; + return normalizePinList(pins["preferredWifi"]); + } + + function toggleWifiPin(ssid) { + const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {})); + let pinnedList = normalizePinList(pins["preferredWifi"]); + const pinIndex = pinnedList.indexOf(ssid); + + if (pinIndex !== -1) { + pinnedList.splice(pinIndex, 1); + } else { + pinnedList.unshift(ssid); + if (pinnedList.length > maxPinnedWifiNetworks) + pinnedList = pinnedList.slice(0, maxPinnedWifiNetworks); + } + + if (pinnedList.length > 0) + pins["preferredWifi"] = pinnedList; + else + delete pins["preferredWifi"]; + + SettingsData.set("wifiNetworkPins", pins); + } + + property var forgetNetworkConfirm: ConfirmModal {} + + width: parent.width + title: I18n.tr("WiFi") + iconName: "wifi" + settingKey: "networkWifi" + tags: ["wifi", "wi-fi", "wireless", "network", "ssid", "adapter", "radio"] + + Column { + id: wifiSection + + width: parent.width + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + StyledText { + text: { + if (NetworkService.wifiToggling) + return I18n.tr("Toggling..."); + if (!NetworkService.wifiEnabled) + return I18n.tr("Disabled"); + if (NetworkService.wifiConnected) + return NetworkService.currentWifiSSID; + return I18n.tr("Not connected"); + } + font.pixelSize: Theme.fontSizeSmall + color: NetworkService.wifiConnected ? Theme.primary : Theme.surfaceVariantText + width: parent.width - wifiControls.width - Theme.spacingM + horizontalAlignment: Text.AlignLeft + anchors.verticalCenter: parent.verticalCenter + } + + Row { + id: wifiControls + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankActionButton { + iconName: "wifi_find" + buttonSize: 32 + visible: NetworkService.backend === "networkmanager" && NetworkService.wifiEnabled && !NetworkService.wifiToggling + onClicked: PopoutService.showHiddenNetworkModal() + } + + DankActionButton { + iconName: "refresh" + buttonSize: 32 + visible: NetworkService.wifiEnabled && !NetworkService.wifiToggling && !NetworkService.isScanning + onClicked: NetworkService.scanWifi() + } + + DankToggle { + checked: NetworkService.wifiEnabled + enabled: !NetworkService.wifiToggling + onToggled: NetworkService.toggleWifiRadio() + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingM + visible: NetworkService.wifiEnabled && (NetworkService.wifiDevices?.length ?? 0) > 1 + + StyledText { + text: I18n.tr("WiFi Device") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: parent.width - wifiDeviceLabel.width - wifiDeviceDropdown.width - Theme.spacingM * 2 + height: 1 + } + + DankDropdown { + id: wifiDeviceDropdown + dropdownWidth: 150 + popupWidth: 180 + currentValue: NetworkService.wifiDeviceOverride || I18n.tr("Auto") + options: { + const devices = NetworkService.wifiDevices; + if (!devices || devices.length === 0) + return [I18n.tr("Auto")]; + return [I18n.tr("Auto")].concat(devices.map(d => d.name)); + } + onValueChanged: value => { + const deviceName = value === I18n.tr("Auto") ? "" : value; + NetworkService.setWifiDeviceOverride(deviceName); + } + } + } + + StyledText { + id: wifiDeviceLabel + visible: false + text: I18n.tr("WiFi Device") + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + visible: NetworkService.wifiEnabled + } + + Column { + width: parent.width + spacing: Theme.spacingS + visible: NetworkService.wifiEnabled && !NetworkService.wifiToggling + + Column { + width: parent.width + spacing: Theme.spacingS + visible: NetworkService.wifiInterface.length > 0 + + Row { + width: parent.width + height: 24 + + StyledText { + text: I18n.tr("Interface:") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + width: 100 + anchors.verticalCenter: parent.verticalCenter + } + StyledText { + text: NetworkService.wifiInterface || "-" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + width: parent.width + height: 24 + visible: NetworkService.wifiIP.length > 0 + + StyledText { + text: I18n.tr("IP Address:") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + width: 100 + anchors.verticalCenter: parent.verticalCenter + } + StyledText { + text: NetworkService.wifiIP || "-" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + width: parent.width + height: 24 + visible: NetworkService.wifiConnected + + StyledText { + text: I18n.tr("Signal:") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + width: 100 + anchors.verticalCenter: parent.verticalCenter + } + + Row { + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + name: { + const s = NetworkService.wifiSignalStrength; + if (s >= 50) + return "wifi"; + if (s >= 25) + return "wifi_2_bar"; + return "wifi_1_bar"; + } + size: 18 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: NetworkService.wifiSignalStrength + "%" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + } + } + } + } + + Item { + width: parent.width + height: Theme.spacingS + } + + Row { + width: parent.width + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Available Networks") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: 1 + height: 1 + Layout.fillWidth: true + } + + StyledText { + text: NetworkService.wifiNetworks?.length ?? 0 + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + } + + Item { + width: parent.width + height: 80 + visible: NetworkService.isScanning && (NetworkService.wifiNetworks?.length ?? 0) === 0 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + id: scanningIcon + name: "wifi_find" + size: 32 + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + + SequentialAnimation { + running: NetworkService.isScanning + loops: Animation.Infinite + OpacityAnimator { + target: scanningIcon + to: 0.3 + duration: 400 + easing.type: Easing.InOutQuad + } + OpacityAnimator { + target: scanningIcon + to: 1.0 + duration: 400 + easing.type: Easing.InOutQuad + } + onRunningChanged: if (!running) + scanningIcon.opacity = 1.0 + } + } + + StyledText { + text: I18n.tr("Scanning...") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + Column { + width: parent.width + spacing: 4 + visible: (NetworkService.wifiNetworks?.length ?? 0) > 0 + + Repeater { + model: { + const ssid = NetworkService.currentWifiSSID; + const networks = NetworkService.wifiNetworks || []; + const pinnedList = root.getPinnedWifiNetworks(); + + let sorted = [...networks]; + sorted.sort((a, b) => { + const aPinnedIndex = pinnedList.indexOf(a.ssid); + const bPinnedIndex = pinnedList.indexOf(b.ssid); + if (aPinnedIndex !== -1 || bPinnedIndex !== -1) { + if (aPinnedIndex === -1) + return 1; + if (bPinnedIndex === -1) + return -1; + return aPinnedIndex - bPinnedIndex; + } + if (a.ssid === ssid) + return -1; + if (b.ssid === ssid) + return 1; + return b.signal - a.signal; + }); + return sorted; + } + + delegate: Rectangle { + id: wifiNetworkDelegate + required property var modelData + required property int index + + readonly property bool isConnected: modelData.ssid === NetworkService.currentWifiSSID + readonly property bool isPinned: root.getPinnedWifiNetworks().includes(modelData.ssid) + readonly property bool isExpanded: root.expandedWifiSsid === modelData.ssid + + width: parent.width + height: isExpanded ? 56 + wifiExpandedContent.height : 56 + radius: Theme.cornerRadius + color: wifiNetworkMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight + border.width: isConnected ? 2 : 0 + border.color: Theme.primary + clip: true + + Behavior on height { + NumberAnimation { + duration: 150 + easing.type: Easing.OutQuad + } + } + + Column { + anchors.fill: parent + spacing: 0 + + Item { + width: parent.width + height: 56 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + anchors.right: wifiNetworkActions.left + anchors.rightMargin: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + name: { + const s = modelData.signal || 0; + if (s >= 50) + return "wifi"; + if (s >= 25) + return "wifi_2_bar"; + return "wifi_1_bar"; + } + size: 20 + color: isConnected ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + width: parent.width - 20 - Theme.spacingS + + Row { + anchors.left: parent.left + spacing: Theme.spacingXS + + StyledText { + text: modelData.ssid || I18n.tr("Unknown") + font.pixelSize: Theme.fontSizeMedium + color: isConnected ? Theme.primary : Theme.surfaceText + font.weight: isConnected ? Font.Medium : Font.Normal + elide: Text.ElideRight + } + + DankIcon { + name: "push_pin" + size: 14 + color: Theme.primary + visible: isPinned + anchors.verticalCenter: parent.verticalCenter + } + + DankIcon { + name: "visibility_off" + size: 14 + color: Theme.surfaceVariantText + visible: modelData.hidden || false + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + anchors.left: parent.left + spacing: Theme.spacingXS + + StyledText { + text: isConnected ? I18n.tr("Connected") : (modelData.secured ? I18n.tr("Secured") : I18n.tr("Open")) + font.pixelSize: Theme.fontSizeSmall + color: isConnected ? Theme.primary : Theme.surfaceVariantText + } + + StyledText { + text: "•" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: modelData.saved + } + + StyledText { + text: I18n.tr("Saved") + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + visible: modelData.saved + } + + StyledText { + text: "•" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: modelData.hidden || false + } + + StyledText { + text: I18n.tr("Hidden") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: modelData.hidden || false + } + + StyledText { + text: "•" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + StyledText { + text: modelData.signal + "%" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + } + + Row { + id: wifiNetworkActions + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: wifiExpandBtn.containsMouse ? Theme.surfacePressed : "transparent" + visible: isConnected || modelData.saved + + DankIcon { + anchors.centerIn: parent + name: isExpanded ? "expand_less" : "expand_more" + size: 18 + color: Theme.surfaceText + } + + MouseArea { + id: wifiExpandBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (isExpanded) { + root.expandedWifiSsid = ""; + } else { + root.expandedWifiSsid = modelData.ssid; + NetworkService.fetchNetworkInfo(modelData.ssid); + } + } + } + } + + DankActionButton { + iconName: "qr_code" + buttonSize: 28 + visible: modelData.secured && modelData.saved + onClicked: { + PopoutService.showWifiQRCodeModal(modelData.ssid); + } + } + + DankActionButton { + iconName: isPinned ? "push_pin" : "push_pin" + buttonSize: 28 + iconColor: isPinned ? Theme.primary : Theme.surfaceVariantText + onClicked: { + root.toggleWifiPin(modelData.ssid); + } + } + + DankActionButton { + iconName: "delete" + buttonSize: 28 + iconColor: Theme.error + visible: modelData.saved || isConnected + onClicked: { + forgetNetworkConfirm.showWithOptions({ + title: I18n.tr("Forget Network"), + message: I18n.tr("Forget \"%1\"?").arg(modelData.ssid), + confirmText: I18n.tr("Forget"), + confirmColor: Theme.error, + onConfirm: () => NetworkService.forgetWifiNetwork(modelData.ssid) + }); + } + } + } + + MouseArea { + id: wifiNetworkMouseArea + + anchors.fill: parent + anchors.rightMargin: wifiNetworkActions.width + Theme.spacingM + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (isConnected) { + NetworkService.disconnectWifi(); + return; + } + if (modelData.secured && !modelData.saved && (DMSService.apiVersion < 7 || modelData.enterprise)) { + PopoutService.showWifiPasswordModal(modelData.ssid); + return; + } + NetworkService.connectToWifi(modelData.ssid); + } + } + } + + Column { + id: wifiExpandedContent + width: parent.width + visible: isExpanded + + Rectangle { + width: parent.width - Theme.spacingM * 2 + height: 1 + x: Theme.spacingM + color: Theme.outlineLight + } + + Item { + width: parent.width + height: wifiDetailsColumn.implicitHeight + Theme.spacingM * 2 + + Column { + id: wifiDetailsColumn + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + Item { + width: parent.width + height: NetworkService.networkInfoLoading ? 40 : 0 + visible: NetworkService.networkInfoLoading + + DankSpinner { + anchors.centerIn: parent + size: 20 + } + } + + Flow { + width: parent.width + spacing: Theme.spacingXS + visible: !NetworkService.networkInfoLoading + + Repeater { + model: { + const fields = []; + const net = modelData; + if (!net) + return fields; + + fields.push({ + label: I18n.tr("Signal"), + value: net.signal + "%" + }); + if (net.frequency) + fields.push({ + label: I18n.tr("Frequency"), + value: (net.frequency / 1000).toFixed(1) + " GHz" + }); + if (net.channel) + fields.push({ + label: I18n.tr("Channel"), + value: String(net.channel) + }); + if (net.rate) + fields.push({ + label: I18n.tr("Rate"), + value: net.rate + " Mbps" + }); + if (net.mode) + fields.push({ + label: I18n.tr("Mode"), + value: net.mode + }); + if (net.bssid) + fields.push({ + label: I18n.tr("BSSID"), + value: net.bssid + }); + fields.push({ + label: I18n.tr("Security"), + value: net.secured ? (net.enterprise ? I18n.tr("Enterprise") : I18n.tr("WPA/WPA2")) : I18n.tr("Open") + }); + + return fields; + } + + delegate: Rectangle { + required property var modelData + required property int index + + width: wifiFieldContent.width + Theme.spacingM * 2 + height: 32 + radius: Theme.cornerRadius - 2 + color: Theme.surfaceContainerHigh + border.width: 1 + border.color: Theme.outlineLight + + Row { + id: wifiFieldContent + anchors.centerIn: parent + spacing: Theme.spacingXS + + StyledText { + text: modelData.label + ":" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: modelData.value + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + } + + Row { + spacing: Theme.spacingS + visible: (modelData.saved || isConnected) && DMSService.apiVersion > 13 + + DankToggle { + id: autoconnectToggle + text: I18n.tr("Autoconnect") + checked: modelData.autoconnect || false + onToggled: checked => { + NetworkService.setWifiAutoconnect(modelData.ssid, checked); + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/quickshell/Services/PopoutService.qml b/quickshell/Services/PopoutService.qml index 90cb2bd2..58ed737e 100644 --- a/quickshell/Services/PopoutService.qml +++ b/quickshell/Services/PopoutService.qml @@ -392,8 +392,7 @@ Singleton { function toggleSettingsWithTab(tabName: string) { if (settingsModal) { var idx = settingsModal.resolveTabIndex(tabName); - if (idx >= 0) - settingsModal.currentTabIndex = idx; + settingsModal.setTabIndex(idx); settingsModal.toggle(); return; } @@ -433,8 +432,7 @@ Singleton { return; } var idx = settingsModal.resolveTabIndex(tabName); - if (idx >= 0) - settingsModal.currentTabIndex = idx; + settingsModal.setTabIndex(idx); toplevel.activate(); return; } @@ -466,12 +464,11 @@ Singleton { if (_settingsWantsToggle) { _settingsWantsToggle = false; if (_settingsPendingTabIndex >= 0) { - settingsModal.currentTabIndex = _settingsPendingTabIndex; + settingsModal?.setTabIndex(_settingsPendingTabIndex); _settingsPendingTabIndex = -1; } else if (_settingsPendingTab) { var idx = settingsModal?.resolveTabIndex(_settingsPendingTab) ?? -1; - if (idx >= 0) - settingsModal.currentTabIndex = idx; + settingsModal?.setTabIndex(idx); _settingsPendingTab = ""; } settingsModal?.toggle(); diff --git a/quickshell/Widgets/VpnDetailContent.qml b/quickshell/Widgets/VpnDetailContent.qml index 077235c3..8a984f67 100644 --- a/quickshell/Widgets/VpnDetailContent.qml +++ b/quickshell/Widgets/VpnDetailContent.qml @@ -148,7 +148,7 @@ Rectangle { iconColor: Theme.surfaceVariantText onClicked: { PopoutService.closeControlCenter(); - PopoutService.openSettingsWithTab("network"); + PopoutService.openSettingsWithTab("network_vpn"); } } } diff --git a/quickshell/translations/extract_settings_index.py b/quickshell/translations/extract_settings_index.py index e32e3702..5c025d3c 100755 --- a/quickshell/translations/extract_settings_index.py +++ b/quickshell/translations/extract_settings_index.py @@ -102,7 +102,10 @@ TAB_INDEX_MAP = { "DockTab.qml": 5, "DankBarAppearanceTab.qml": 6, "WorkspaceAppearanceCard.qml": 6, - "NetworkTab.qml": 7, + "NetworkStatusTab.qml": 7, + "NetworkEthernetTab.qml": 39, + "NetworkWifiTab.qml": 40, + "NetworkVpnTab.qml": 41, "PrinterTab.qml": 8, "LauncherTab.qml": 9, "ThemeColorsTab.qml": 10, @@ -172,6 +175,9 @@ TAB_CATEGORY_MAP = { 36: "Autostart", 37: "Personalization", 38: "Applications", + 39: "Network", + 40: "Network", + 41: "Network", } SEARCHABLE_COMPONENTS = [ diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index 0c0914e1..ee4454fd 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -2118,6 +2118,25 @@ "icon": "wifi", "conditionKey": "dmsConnected" }, + { + "section": "networkStatus", + "label": "Network Status", + "tabIndex": 7, + "category": "Network", + "keywords": [ + "connection", + "connectivity", + "ethernet", + "internet", + "network", + "online", + "status", + "wi-fi", + "wifi", + "wireless" + ], + "icon": "lan" + }, { "section": "_tab_8", "label": "Printers", @@ -7302,7 +7321,8 @@ "screen", "widgets" ], - "icon": "widgets" + "icon": "widgets", + "conditionKey": "dmsConnected" }, { "section": "_tab_27", @@ -8901,5 +8921,95 @@ "icon": "select_window", "description": "Define compositor rules for window behavior", "conditionKey": "windowRulesCapable" + }, + { + "section": "_tab_39", + "label": "Ethernet", + "tabIndex": 39, + "category": "Network", + "keywords": [ + "connectivity", + "ethernet", + "network", + "online" + ], + "icon": "settings_ethernet" + }, + { + "section": "networkEthernet", + "label": "Ethernet", + "tabIndex": 39, + "category": "Network", + "keywords": [ + "adapters", + "connection", + "connectivity", + "ethernet", + "network", + "online", + "wired" + ], + "icon": "settings_ethernet" + }, + { + "section": "_tab_40", + "label": "WiFi", + "tabIndex": 40, + "category": "Network", + "keywords": [ + "connectivity", + "network", + "online", + "wifi" + ], + "icon": "wifi" + }, + { + "section": "networkWifi", + "label": "WiFi", + "tabIndex": 40, + "category": "Network", + "keywords": [ + "adapter", + "connectivity", + "network", + "online", + "radio", + "ssid", + "wi-fi", + "wifi", + "wireless" + ], + "icon": "wifi" + }, + { + "section": "_tab_41", + "label": "VPN", + "tabIndex": 41, + "category": "Network", + "keywords": [ + "connectivity", + "network", + "online", + "vpn" + ], + "icon": "vpn_key" + }, + { + "section": "networkVpn", + "label": "VPN", + "tabIndex": 41, + "category": "Network", + "keywords": [ + "connectivity", + "import", + "network", + "online", + "openvpn", + "profiles", + "vpn", + "wireguard" + ], + "icon": "vpn_key" } ]