mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-05 21:15:38 -05:00
- Dedicated view in settings - VPN profile management - Ethernet disconnection - Turn prompts into floating windows
672 lines
25 KiB
QML
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|