diff options
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/NetworkTab.qml')
| -rw-r--r-- | raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/NetworkTab.qml | 1999 |
1 files changed, 1999 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/NetworkTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/NetworkTab.qml new file mode 100644 index 0000000..b9b2566 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/NetworkTab.qml @@ -0,0 +1,1999 @@ +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 + + Row { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + id: wiredLoadIcon + name: "sync" + size: 16 + color: Theme.surfaceVariantText + + SequentialAnimation { + running: NetworkService.networkWiredInfoLoading + loops: Animation.Infinite + NumberAnimation { + target: wiredLoadIcon + property: "opacity" + to: 0.3 + duration: 400 + easing.type: Easing.InOutQuad + } + NumberAnimation { + target: wiredLoadIcon + property: "opacity" + to: 1.0 + duration: 400 + easing.type: Easing.InOutQuad + } + onRunningChanged: if (!running) + wiredLoadIcon.opacity = 1.0 + } + } + + StyledText { + text: I18n.tr("Loading...") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + } + } + } + } + } + } + } + + 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 + NumberAnimation { + target: scanningIcon + property: "opacity" + to: 0.3 + duration: 400 + easing.type: Easing.InOutQuad + } + NumberAnimation { + target: scanningIcon + property: "opacity" + 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; + } + 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 + + Row { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + id: wifiInfoLoadIcon + name: "sync" + size: 16 + color: Theme.surfaceVariantText + + SequentialAnimation { + running: NetworkService.networkInfoLoading + loops: Animation.Infinite + NumberAnimation { + target: wifiInfoLoadIcon + property: "opacity" + to: 0.3 + duration: 400 + easing.type: Easing.InOutQuad + } + NumberAnimation { + target: wifiInfoLoadIcon + property: "opacity" + to: 1.0 + duration: 400 + easing.type: Easing.InOutQuad + } + onRunningChanged: if (!running) + wifiInfoLoadIcon.opacity = 1.0 + } + } + + StyledText { + text: I18n.tr("Loading...") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + + 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 isExpanded: networkTab.expandedVpnUuid === modelData.uuid + readonly property var configData: 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 - 28 - 28 - 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 + + 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 + + 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: isExpanded + + Rectangle { + width: parent.width + height: 1 + color: Theme.outlineLight + } + + Item { + width: parent.width + height: VPNService.configLoading ? 40 : 0 + visible: VPNService.configLoading + + Row { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + id: vpnLoadIcon + name: "sync" + size: 16 + color: Theme.surfaceVariantText + + SequentialAnimation { + running: VPNService.configLoading + loops: Animation.Infinite + NumberAnimation { + target: vpnLoadIcon + property: "opacity" + to: 0.3 + duration: 400 + easing.type: Easing.InOutQuad + } + NumberAnimation { + target: vpnLoadIcon + property: "opacity" + to: 1.0 + duration: 400 + easing.type: Easing.InOutQuad + } + onRunningChanged: if (!running) + vpnLoadIcon.opacity = 1.0 + } + } + + StyledText { + text: I18n.tr("Loading...") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + + 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 + } + } + } + } + } + } + } + } + } + } +} |