1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-05 21:15:38 -05:00
Files
DankMaterialShell/quickshell/Modals/WifiPasswordModal.qml
bbedward 1d3fe81ff7 network: big feature enrichment
- Dedicated view in settings
- VPN profile management
- Ethernet disconnection
- Turn prompts into floating windows
2025-11-29 10:00:05 -05:00

672 lines
25 KiB
QML

import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
FloatingWindow {
id: root
property string wifiPasswordSSID: ""
property string wifiPasswordInput: ""
property string wifiUsernameInput: ""
property bool requiresEnterprise: false
property string wifiAnonymousIdentityInput: ""
property string wifiDomainInput: ""
property bool isPromptMode: false
property string promptToken: ""
property string promptReason: ""
property var promptFields: []
property string promptSetting: ""
property bool isVpnPrompt: false
property string connectionName: ""
property string vpnServiceType: ""
property string connectionType: ""
property var fieldsInfo: []
property var secretValues: ({})
property int calculatedHeight: {
if (fieldsInfo.length > 0)
return 180 + (fieldsInfo.length * 60);
if (requiresEnterprise)
return 430;
if (isVpnPrompt)
return 260;
return 230;
}
function focusFirstField() {
if (fieldsInfo.length > 0) {
if (dynamicFieldsRepeater.count > 0) {
const firstItem = dynamicFieldsRepeater.itemAt(0);
if (firstItem)
firstItem.children[0].forceActiveFocus();
}
return;
}
if (requiresEnterprise && !isVpnPrompt) {
usernameInput.forceActiveFocus();
return;
}
passwordInput.forceActiveFocus();
}
function show(ssid) {
wifiPasswordSSID = ssid;
wifiPasswordInput = "";
wifiUsernameInput = "";
wifiAnonymousIdentityInput = "";
wifiDomainInput = "";
isPromptMode = false;
promptToken = "";
promptReason = "";
promptFields = [];
promptSetting = "";
isVpnPrompt = false;
connectionName = "";
vpnServiceType = "";
connectionType = "";
fieldsInfo = [];
secretValues = {};
const network = NetworkService.wifiNetworks.find(n => n.ssid === ssid);
requiresEnterprise = network?.enterprise || false;
visible = true;
Qt.callLater(focusFirstField);
}
function showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fInfo) {
isPromptMode = true;
promptToken = token;
promptReason = reason;
promptFields = fields || [];
promptSetting = setting || "802-11-wireless-security";
connectionType = connType || "802-11-wireless";
connectionName = connName || ssid || "";
vpnServiceType = vpnService || "";
fieldsInfo = fInfo || [];
secretValues = {};
isVpnPrompt = (connectionType === "vpn" || connectionType === "wireguard");
wifiPasswordSSID = isVpnPrompt ? connectionName : ssid;
requiresEnterprise = setting === "802-1x";
wifiPasswordInput = "";
wifiUsernameInput = "";
wifiAnonymousIdentityInput = "";
wifiDomainInput = "";
visible = true;
Qt.callLater(() => {
if (reason === "wrong-password" && fieldsInfo.length === 0) {
passwordInput.text = "";
}
focusFirstField();
});
}
function hide() {
visible = false;
}
function getFieldLabel(fieldName) {
switch (fieldName) {
case "username":
case "identity":
return I18n.tr("Username");
case "password":
return I18n.tr("Password");
case "cert-pass":
case "certpass":
return I18n.tr("Certificate Password");
case "private-key-password":
return I18n.tr("Private Key Password");
case "pin":
return I18n.tr("PIN");
case "psk":
return I18n.tr("Password");
case "anonymous-identity":
return I18n.tr("Anonymous Identity");
default:
return fieldName.charAt(0).toUpperCase() + fieldName.slice(1).replace(/-/g, " ");
}
}
function submitCredentialsAndClose() {
if (fieldsInfo.length > 0) {
NetworkService.submitCredentials(promptToken, secretValues, savePasswordCheckbox.checked);
hide();
secretValues = {};
return;
}
if (isPromptMode) {
const secrets = {};
if (isVpnPrompt) {
if (passwordInput.text)
secrets["password"] = passwordInput.text;
} else if (promptSetting === "802-11-wireless-security") {
secrets["psk"] = passwordInput.text;
} else if (promptSetting === "802-1x") {
if (usernameInput.text)
secrets["identity"] = usernameInput.text;
if (passwordInput.text)
secrets["password"] = passwordInput.text;
if (wifiAnonymousIdentityInput)
secrets["anonymous-identity"] = wifiAnonymousIdentityInput;
}
NetworkService.submitCredentials(promptToken, secrets, savePasswordCheckbox.checked);
} else {
const username = requiresEnterprise ? usernameInput.text : "";
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text, username, wifiAnonymousIdentityInput, wifiDomainInput);
}
hide();
wifiPasswordInput = "";
wifiUsernameInput = "";
wifiAnonymousIdentityInput = "";
wifiDomainInput = "";
passwordInput.text = "";
if (requiresEnterprise)
usernameInput.text = "";
}
function clearAndClose() {
if (isPromptMode)
NetworkService.cancelCredentials(promptToken);
hide();
wifiPasswordInput = "";
wifiUsernameInput = "";
wifiAnonymousIdentityInput = "";
wifiDomainInput = "";
secretValues = {};
}
objectName: "wifiPasswordModal"
title: isVpnPrompt ? I18n.tr("VPN Password") : I18n.tr("Wi-Fi Password")
minimumSize: Qt.size(420, calculatedHeight)
maximumSize: Qt.size(420, calculatedHeight)
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false
onVisibleChanged: {
if (visible) {
Qt.callLater(focusFirstField);
return;
}
wifiPasswordInput = "";
wifiUsernameInput = "";
wifiAnonymousIdentityInput = "";
wifiDomainInput = "";
secretValues = {};
passwordInput.text = "";
usernameInput.text = "";
anonInput.text = "";
domainMatchInput.text = "";
for (let i = 0; i < dynamicFieldsRepeater.count; i++) {
const item = dynamicFieldsRepeater.itemAt(i);
if (item?.children[0])
item.children[0].text = "";
}
}
Connections {
target: NetworkService
function onPasswordDialogShouldReopenChanged() {
if (!NetworkService.passwordDialogShouldReopen || NetworkService.connectingSSID === "")
return;
wifiPasswordSSID = NetworkService.connectingSSID;
wifiPasswordInput = "";
visible = true;
NetworkService.passwordDialogShouldReopen = false;
}
}
FocusScope {
id: contentFocusScope
anchors.fill: parent
focus: true
Keys.onEscapePressed: event => {
clearAndClose();
event.accepted = true;
}
Column {
id: contentCol
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
spacing: Theme.spacingM
Row {
width: contentCol.width
Column {
width: parent.width - 40
spacing: Theme.spacingXS
StyledText {
text: isVpnPrompt ? I18n.tr("Connect to VPN") : I18n.tr("Connect to Wi-Fi")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: {
if (fieldsInfo.length > 0)
return I18n.tr("Enter credentials for ") + wifiPasswordSSID;
if (isVpnPrompt)
return I18n.tr("Enter password for ") + wifiPasswordSSID;
const prefix = requiresEnterprise ? I18n.tr("Enter credentials for ") : I18n.tr("Enter password for ");
return prefix + wifiPasswordSSID;
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width
elide: Text.ElideRight
}
StyledText {
visible: isPromptMode && promptReason === "wrong-password"
text: I18n.tr("Incorrect password")
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
width: parent.width
}
}
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: clearAndClose()
}
}
Repeater {
id: dynamicFieldsRepeater
model: fieldsInfo
delegate: Rectangle {
required property var modelData
required property int index
width: contentCol.width
height: 50
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: fieldInput.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: fieldInput.activeFocus ? 2 : 1
DankTextField {
id: fieldInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
echoMode: modelData.isSecret ? TextInput.Password : TextInput.Normal
placeholderText: getFieldLabel(modelData.name)
backgroundColor: "transparent"
enabled: root.visible
Keys.onTabPressed: event => {
if (index < fieldsInfo.length - 1) {
const nextItem = dynamicFieldsRepeater.itemAt(index + 1);
if (nextItem)
nextItem.children[0].forceActiveFocus();
} else {
const firstItem = dynamicFieldsRepeater.itemAt(0);
if (firstItem)
firstItem.children[0].forceActiveFocus();
}
event.accepted = true;
}
Keys.onBacktabPressed: event => {
if (index > 0) {
const prevItem = dynamicFieldsRepeater.itemAt(index - 1);
if (prevItem)
prevItem.children[0].forceActiveFocus();
} else {
const lastItem = dynamicFieldsRepeater.itemAt(fieldsInfo.length - 1);
if (lastItem)
lastItem.children[0].forceActiveFocus();
}
event.accepted = true;
}
onTextEdited: {
let updated = Object.assign({}, root.secretValues);
updated[modelData.name] = text;
root.secretValues = updated;
}
onAccepted: {
if (index < fieldsInfo.length - 1) {
const nextItem = dynamicFieldsRepeater.itemAt(index + 1);
if (nextItem)
nextItem.children[0].forceActiveFocus();
return;
}
submitCredentialsAndClose();
}
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: usernameInput.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: usernameInput.activeFocus ? 2 : 1
visible: requiresEnterprise && !isVpnPrompt && fieldsInfo.length === 0
MouseArea {
anchors.fill: parent
onClicked: usernameInput.forceActiveFocus()
}
DankTextField {
id: usernameInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: wifiUsernameInput
placeholderText: I18n.tr("Username")
backgroundColor: "transparent"
enabled: root.visible
keyNavigationTab: passwordInput
keyNavigationBacktab: domainMatchInput
onTextEdited: wifiUsernameInput = text
onAccepted: passwordInput.forceActiveFocus()
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: passwordInput.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: passwordInput.activeFocus ? 2 : 1
visible: fieldsInfo.length === 0
MouseArea {
anchors.fill: parent
onClicked: passwordInput.forceActiveFocus()
}
DankTextField {
id: passwordInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: wifiPasswordInput
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
placeholderText: (requiresEnterprise && !isVpnPrompt) ? I18n.tr("Password") : ""
backgroundColor: "transparent"
enabled: root.visible
keyNavigationTab: (requiresEnterprise && !isVpnPrompt) ? anonInput : null
keyNavigationBacktab: (requiresEnterprise && !isVpnPrompt) ? usernameInput : null
onTextEdited: wifiPasswordInput = text
onAccepted: {
if (requiresEnterprise && !isVpnPrompt) {
anonInput.forceActiveFocus();
return;
}
submitCredentialsAndClose();
}
}
}
Rectangle {
visible: requiresEnterprise && !isVpnPrompt
width: parent.width
height: 50
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: anonInput.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: anonInput.activeFocus ? 2 : 1
MouseArea {
anchors.fill: parent
onClicked: anonInput.forceActiveFocus()
}
DankTextField {
id: anonInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: wifiAnonymousIdentityInput
placeholderText: I18n.tr("Anonymous Identity (optional)")
backgroundColor: "transparent"
enabled: root.visible
keyNavigationTab: domainMatchInput
keyNavigationBacktab: passwordInput
onTextEdited: wifiAnonymousIdentityInput = text
onAccepted: domainMatchInput.forceActiveFocus()
}
}
Rectangle {
visible: requiresEnterprise && !isVpnPrompt
width: parent.width
height: 50
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: domainMatchInput.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: domainMatchInput.activeFocus ? 2 : 1
MouseArea {
anchors.fill: parent
onClicked: domainMatchInput.forceActiveFocus()
}
DankTextField {
id: domainMatchInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: wifiDomainInput
placeholderText: I18n.tr("Domain (optional)")
backgroundColor: "transparent"
enabled: root.visible
keyNavigationTab: usernameInput
keyNavigationBacktab: anonInput
onTextEdited: wifiDomainInput = text
onAccepted: submitCredentialsAndClose()
}
}
Column {
spacing: Theme.spacingS
width: parent.width
Row {
spacing: Theme.spacingS
visible: fieldsInfo.length === 0
Rectangle {
id: showPasswordCheckbox
property bool checked: false
width: 20
height: 20
radius: 4
color: checked ? Theme.primary : "transparent"
border.color: checked ? Theme.primary : Theme.outlineButton
border.width: 2
DankIcon {
anchors.centerIn: parent
name: "check"
size: 12
color: Theme.background
visible: parent.checked
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: showPasswordCheckbox.checked = !showPasswordCheckbox.checked
}
}
StyledText {
text: I18n.tr("Show password")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingS
visible: isVpnPrompt || fieldsInfo.length > 0
Rectangle {
id: savePasswordCheckbox
property bool checked: false
width: 20
height: 20
radius: 4
color: checked ? Theme.primary : "transparent"
border.color: checked ? Theme.primary : Theme.outlineButton
border.width: 2
DankIcon {
anchors.centerIn: parent
name: "check"
size: 12
color: Theme.background
visible: parent.checked
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: savePasswordCheckbox.checked = !savePasswordCheckbox.checked
}
}
StyledText {
text: I18n.tr("Save password")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
Item {
width: parent.width
height: 40
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Rectangle {
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
border.color: Theme.surfaceVariantAlpha
border.width: 1
StyledText {
id: cancelText
anchors.centerIn: parent
text: I18n.tr("Cancel")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
MouseArea {
id: cancelArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: clearAndClose()
}
}
Rectangle {
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
enabled: {
if (fieldsInfo.length > 0) {
for (let i = 0; i < fieldsInfo.length; i++) {
if (!fieldsInfo[i].isSecret)
continue;
const fieldName = fieldsInfo[i].name;
if (!secretValues[fieldName] || secretValues[fieldName].length === 0)
return false;
}
return true;
}
if (isVpnPrompt)
return passwordInput.text.length > 0;
return requiresEnterprise ? (usernameInput.text.length > 0 && passwordInput.text.length > 0) : passwordInput.text.length > 0;
}
opacity: enabled ? 1 : 0.5
StyledText {
id: connectText
anchors.centerIn: parent
text: I18n.tr("Connect")
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
}
MouseArea {
id: connectArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: parent.enabled
onClicked: submitCredentialsAndClose()
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
}