diff --git a/client/resources.qrc b/client/resources.qrc
index 1472f66df..03ac6eddf 100644
--- a/client/resources.qrc
+++ b/client/resources.qrc
@@ -247,6 +247,7 @@
ui/qml/Components/OtpCodeDrawer.qml
ui/qml/Components/AwgTextField.qml
ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml
+ ui/qml/Components/SmartScroll.qml
images/flagKit/ZW.svg
diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp
index 7eef384c5..b32408c07 100644
--- a/client/ui/controllers/settingsController.cpp
+++ b/client/ui/controllers/settingsController.cpp
@@ -446,7 +446,11 @@ bool SettingsController::isOnTv()
bool SettingsController::isEdgeToEdgeEnabled()
{
#ifdef Q_OS_ANDROID
- return AndroidController::instance()->isEdgeToEdgeEnabled();
+ if (!m_edgeToEdgeCached) {
+ m_cachedEdgeToEdgeEnabled = AndroidController::instance()->isEdgeToEdgeEnabled();
+ m_edgeToEdgeCached = true;
+ }
+ return m_cachedEdgeToEdgeEnabled;
#else
return false;
#endif
diff --git a/client/ui/controllers/settingsController.h b/client/ui/controllers/settingsController.h
index cc90de567..6ee5c6310 100644
--- a/client/ui/controllers/settingsController.h
+++ b/client/ui/controllers/settingsController.h
@@ -150,6 +150,8 @@ private:
mutable int m_cachedStatusBarHeight = -1;
mutable int m_cachedNavigationBarHeight = -1;
+ mutable bool m_cachedEdgeToEdgeEnabled = false;
+ mutable bool m_edgeToEdgeCached = false;
int m_imeHeight = 0;
std::shared_ptr m_settings;
diff --git a/client/ui/qml/Components/SmartScroll.qml b/client/ui/qml/Components/SmartScroll.qml
new file mode 100644
index 000000000..374f7f4aa
--- /dev/null
+++ b/client/ui/qml/Components/SmartScroll.qml
@@ -0,0 +1,55 @@
+import QtQuick
+import QtQuick.Controls
+
+QtObject {
+ id: root
+
+ property var listView: null
+ property var scrollToItemTarget: null
+
+ property Connections imeConnection: Connections {
+ target: SettingsController
+ function onImeHeightChanged() {
+ if (root.scrollToItemTarget && SettingsController.imeHeight > 0) {
+ scrollTimer.restart()
+ }
+ }
+ }
+
+ property Timer scrollTimer: Timer {
+ interval: 100
+ repeat: false
+ onTriggered: {
+ if (root.scrollToItemTarget && root.listView) {
+ if (SettingsController.imeHeight > 0) {
+ var item = root.scrollToItemTarget
+ var itemY = item.mapToItem(root.listView.contentItem, 0, 0).y
+ var itemHeight = item.height
+ var keyboardHeight = SettingsController.imeHeight + SettingsController.safeAreaBottomMargin
+ var visibleHeight = root.listView.height - keyboardHeight
+
+ var desiredTopOffset = visibleHeight * 0.25
+ var targetContentY = itemY - desiredTopOffset
+
+ if (targetContentY < 0) {
+ targetContentY = 0
+ }
+
+ var maxContentY = root.listView.contentHeight - root.listView.height
+ if (targetContentY > maxContentY) {
+ targetContentY = maxContentY
+ }
+
+ root.listView.contentY = targetContentY
+ root.scrollToItemTarget = null
+ }
+ }
+ }
+ }
+
+ function scrollToItem(item) {
+ scrollToItemTarget = item
+ scrollTimer.restart()
+ }
+}
+
diff --git a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml
index fdb7a3aef..f8c74a59c 100644
--- a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml
+++ b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml
@@ -19,6 +19,9 @@ Item {
property string buttonText
property string buttonImageSource
+ property string buttonImageColor: AmneziaStyle.color.midnightBlack
+ property string buttonBackgroundColor: AmneziaStyle.color.paleGray
+ property string buttonHoveredColor: AmneziaStyle.color.lightGray
property var clickedFunc
property alias textField: textField
@@ -48,7 +51,7 @@ Item {
Keys.onUpPressed: {
FocusController.nextKeyUpItem()
}
-
+
Keys.onDownPressed: {
FocusController.nextKeyDownItem()
}
@@ -67,7 +70,7 @@ Item {
border.width: 1
Behavior on border.color {
- PropertyAnimation { duration: 200 }
+ PropertyAnimation { duration: 100 }
}
RowLayout {
@@ -194,6 +197,14 @@ Item {
focusPolicy: Qt.NoFocus
text: root.buttonText
leftImageSource: root.buttonImageSource
+ leftImageColor: root.buttonImageColor
+
+ defaultColor: root.buttonBackgroundColor
+ hoveredColor: root.buttonHoveredColor
+ pressedColor: root.buttonHoveredColor
+ disabledColor: AmneziaStyle.color.transparent
+
+ borderWidth: 0
anchors.top: content.top
anchors.bottom: content.bottom
@@ -201,7 +212,7 @@ Item {
height: content.implicitHeight
width: content.implicitHeight
- squareLeftSide: true
+ squareLeftSide: false
clickedFunc: function() {
if (root.clickedFunc && typeof root.clickedFunc === "function") {
diff --git a/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml b/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml
index 47b4fb58c..d3f5be7f1 100644
--- a/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml
+++ b/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml
@@ -31,6 +31,11 @@ PageType {
}
}
+ SmartScroll {
+ id: smartScroll
+ listView: listView
+ }
+
ListViewType {
id: listView
@@ -80,6 +85,13 @@ PageType {
clientMtu = textField.text
}
}
+
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(mtuTextField)
+ }
+ }
+
checkEmptyText: true
}
@@ -97,6 +109,12 @@ PageType {
clientJunkPacketCount = textField.text
}
}
+
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(junkPacketCountTextField)
+ }
+ }
}
AwgTextField {
@@ -113,6 +131,12 @@ PageType {
clientJunkPacketMinSize = textField.text
}
}
+
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(junkPacketMinSizeTextField)
+ }
+ }
}
AwgTextField {
@@ -129,6 +153,12 @@ PageType {
clientJunkPacketMaxSize = textField.text
}
}
+
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(junkPacketMaxSizeTextField)
+ }
+ }
}
AwgTextField {
@@ -147,6 +177,12 @@ PageType {
clientSpecialJunk1 = textField.text
}
}
+
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(specialJunk1TextField)
+ }
+ }
}
AwgTextField {
@@ -165,6 +201,12 @@ PageType {
clientSpecialJunk2 = textField.text
}
}
+
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(specialJunk2TextField)
+ }
+ }
}
AwgTextField {
@@ -183,6 +225,12 @@ PageType {
clientSpecialJunk3 = textField.text
}
}
+
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(specialJunk3TextField)
+ }
+ }
}
AwgTextField {
@@ -201,6 +249,12 @@ PageType {
clientSpecialJunk4 = textField.text
}
}
+
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(specialJunk4TextField)
+ }
+ }
}
AwgTextField {
@@ -219,6 +273,12 @@ PageType {
clientSpecialJunk5 = textField.text
}
}
+
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(specialJunk5TextField)
+ }
+ }
}
AwgTextField {
@@ -237,6 +297,12 @@ PageType {
clientControlledJunk1 = textField.text
}
}
+
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(controlledJunk1TextField)
+ }
+ }
}
AwgTextField {
@@ -255,6 +321,12 @@ PageType {
clientControlledJunk2 = textField.text
}
}
+
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(controlledJunk2TextField)
+ }
+ }
}
AwgTextField {
@@ -273,6 +345,12 @@ PageType {
clientControlledJunk3 = textField.text
}
}
+
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(controlledJunk3TextField)
+ }
+ }
}
AwgTextField {
@@ -290,6 +368,12 @@ PageType {
clientSpecialHandshakeTimeout = textField.text
}
}
+
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(iTimeTextField)
+ }
+ }
}
Header2TextType {
diff --git a/client/ui/qml/Pages2/PageProtocolAwgSettings.qml b/client/ui/qml/Pages2/PageProtocolAwgSettings.qml
index 1727b4fec..ff1794462 100644
--- a/client/ui/qml/Pages2/PageProtocolAwgSettings.qml
+++ b/client/ui/qml/Pages2/PageProtocolAwgSettings.qml
@@ -34,6 +34,11 @@ PageType {
}
}
+ SmartScroll {
+ id: smartScroll
+ listView: listView
+ }
+
ListViewType {
id: listView
@@ -81,6 +86,12 @@ PageType {
}
}
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(vpnAddressSubnetTextField)
+ }
+ }
+
checkEmptyText: true
}
@@ -104,6 +115,12 @@ PageType {
}
}
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(portTextField)
+ }
+ }
+
checkEmptyText: true
}
@@ -121,6 +138,12 @@ PageType {
serverJunkPacketCount = textField.text
}
}
+
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(junkPacketCountTextField)
+ }
+ }
}
AwgTextField {
@@ -137,6 +160,12 @@ PageType {
serverJunkPacketMinSize = textField.text
}
}
+
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(junkPacketMinSizeTextField)
+ }
+ }
}
AwgTextField {
@@ -153,6 +182,12 @@ PageType {
serverJunkPacketMaxSize = textField.text
}
}
+
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(junkPacketMaxSizeTextField)
+ }
+ }
}
AwgTextField {
@@ -169,6 +204,12 @@ PageType {
serverInitPacketJunkSize = textField.text
}
}
+
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(initPacketJunkSizeTextField)
+ }
+ }
}
AwgTextField {
@@ -185,6 +226,12 @@ PageType {
serverResponsePacketJunkSize = textField.text
}
}
+
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(responsePacketJunkSizeTextField)
+ }
+ }
}
// AwgTextField {
@@ -233,6 +280,12 @@ PageType {
serverInitPacketMagicHeader = textField.text
}
}
+
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(initPacketMagicHeaderTextField)
+ }
+ }
}
AwgTextField {
@@ -249,6 +302,12 @@ PageType {
serverResponsePacketMagicHeader = textField.text
}
}
+
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(responsePacketMagicHeaderTextField)
+ }
+ }
}
AwgTextField {
@@ -265,6 +324,12 @@ PageType {
serverUnderloadPacketMagicHeader = textField.text
}
}
+
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(underloadPacketMagicHeaderTextField)
+ }
+ }
}
AwgTextField {
@@ -281,6 +346,12 @@ PageType {
serverTransportPacketMagicHeader = textField.text
}
}
+
+ textField.onActiveFocusChanged: {
+ if (textField.activeFocus) {
+ smartScroll.scrollToItem(transportPacketMagicHeaderTextField)
+ }
+ }
}
BasicButtonType {
diff --git a/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml b/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml
index 34a6e1753..4445b08bf 100644
--- a/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml
+++ b/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml
@@ -165,9 +165,11 @@ PageType {
ScrollBar.vertical: ScrollBarType { policy: ScrollBar.AlwaysOn }
anchors.top: header.bottom
- anchors.bottom: addAppButton.top
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: addAppButton.implicitHeight + 48 + SettingsController.safeAreaBottomMargin + (searchField.textField.activeFocus ? 0 : SettingsController.imeHeight)
anchors.left: parent.left
anchors.right: parent.right
+ clip: true
model: SortFilterProxyModel {
id: proxyAppSplitTunnelingModel
@@ -215,50 +217,54 @@ PageType {
}
Rectangle {
- anchors.fill: addAppButton
- anchors.bottomMargin: -24 - SettingsController.safeAreaBottomMargin
- color: AmneziaStyle.color.midnightBlack
- opacity: 0.8
- }
-
- RowLayout {
- id: addAppButton
-
- enabled: root.pageEnabled
-
- anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
- anchors.topMargin: 24
- anchors.rightMargin: 16
- anchors.leftMargin: 16
- anchors.bottomMargin: 24 + SettingsController.safeAreaBottomMargin
+ anchors.bottom: parent.bottom
+
+ height: addAppButton.implicitHeight + 48 + SettingsController.safeAreaBottomMargin
+
+ color: AmneziaStyle.color.midnightBlack
+ opacity: 0.8
+
+ RowLayout {
+ id: addAppButton
- TextFieldWithHeaderType {
- id: searchField
+ enabled: root.pageEnabled
- Layout.fillWidth: true
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.topMargin: 24
+ anchors.rightMargin: 16
+ anchors.leftMargin: 16
+ anchors.bottomMargin: 24 + SettingsController.safeAreaBottomMargin
- textField.placeholderText: qsTr("application name")
- buttonImageSource: "qrc:/images/controls/plus.svg"
+ TextFieldWithHeaderType {
+ id: searchField
- rightButtonClickedOnEnter: true
+ Layout.fillWidth: true
- clickedFunc: function() {
- searchField.focus = false
- PageController.showBusyIndicator(true)
+ textField.placeholderText: qsTr("application name")
+ buttonImageSource: "qrc:/images/controls/plus.svg"
- if (Qt.platform.os === "windows") {
- var fileName = SystemController.getFileName(qsTr("Open executable file"),
- qsTr("Executable files (*.*)"))
- if (fileName !== "") {
- AppSplitTunnelingController.addApp(fileName)
+ rightButtonClickedOnEnter: true
+
+ clickedFunc: function() {
+ searchField.focus = false
+ PageController.showBusyIndicator(true)
+
+ if (Qt.platform.os === "windows") {
+ var fileName = SystemController.getFileName(qsTr("Open executable file"),
+ qsTr("Executable files (*.*)"))
+ if (fileName !== "") {
+ AppSplitTunnelingController.addApp(fileName)
+ }
+ } else if (Qt.platform.os === "android"){
+ installedAppDrawer.openTriggered()
}
- } else if (Qt.platform.os === "android"){
- installedAppDrawer.openTriggered()
- }
- PageController.showBusyIndicator(false)
+ PageController.showBusyIndicator(false)
+ }
}
}
}
diff --git a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml
index 3f6b8a71a..27aa5dea6 100644
--- a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml
+++ b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml
@@ -168,11 +168,13 @@ PageType {
anchors.top: header.bottom
anchors.topMargin: 16
- anchors.bottom: addSiteButton.top
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: addSiteButton.implicitHeight + 48 + (searchField.textField.activeFocus ? 0 : SettingsController.imeHeight)
width: parent.width
enabled: root.pageEnabled
+ clip: true
model: SortFilterProxyModel {
id: proxySitesModel
@@ -231,56 +233,60 @@ PageType {
}
Rectangle {
- anchors.fill: addSiteButton
- anchors.bottomMargin: -24
- color: AmneziaStyle.color.midnightBlack
- opacity: 0.8
- }
-
- RowLayout {
- id: addSiteButton
-
- enabled: root.pageEnabled
-
- anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
- anchors.topMargin: 24
- anchors.rightMargin: 16
- anchors.leftMargin: 16
- anchors.bottomMargin: 24
+ anchors.bottom: parent.bottom
+
+ height: addSiteButton.implicitHeight + 48
+
+ color: AmneziaStyle.color.midnightBlack
+ opacity: 0.8
+
+ RowLayout {
+ id: addSiteButton
- TextFieldWithHeaderType {
- id: searchField
+ enabled: root.pageEnabled
- Layout.fillWidth: true
- rightButtonClickedOnEnter: true
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.topMargin: 24
+ anchors.rightMargin: 16
+ anchors.leftMargin: 16
+ anchors.bottomMargin: 24
- textField.placeholderText: qsTr("website or IP")
- buttonImageSource: "qrc:/images/controls/plus.svg"
+ TextFieldWithHeaderType {
+ id: searchField
- clickedFunc: function() {
- PageController.showBusyIndicator(true)
- SitesController.addSite(textField.text)
- textField.text = ""
- PageController.showBusyIndicator(false)
- }
- }
+ Layout.fillWidth: true
+ rightButtonClickedOnEnter: true
- ImageButtonType {
- id: addSiteButtonImage
- implicitWidth: 56
- implicitHeight: 56
+ textField.placeholderText: qsTr("website or IP")
+ buttonImageSource: "qrc:/images/controls/plus.svg"
- image: "qrc:/images/controls/more-vertical.svg"
- imageColor: AmneziaStyle.color.paleGray
-
- onClicked: function () {
- moreActionsDrawer.openTriggered()
+ clickedFunc: function() {
+ PageController.showBusyIndicator(true)
+ SitesController.addSite(textField.text)
+ textField.text = ""
+ PageController.showBusyIndicator(false)
+ }
}
- Keys.onReturnPressed: addSiteButtonImage.clicked()
- Keys.onEnterPressed: addSiteButtonImage.clicked()
+ ImageButtonType {
+ id: addSiteButtonImage
+ implicitWidth: 56
+ implicitHeight: 56
+
+ image: "qrc:/images/controls/more-vertical.svg"
+ imageColor: AmneziaStyle.color.paleGray
+
+ onClicked: function () {
+ moreActionsDrawer.openTriggered()
+ }
+
+ Keys.onReturnPressed: addSiteButtonImage.clicked()
+ Keys.onEnterPressed: addSiteButtonImage.clicked()
+ }
}
}
diff --git a/client/ui/qml/main2.qml b/client/ui/qml/main2.qml
index 020f76c1d..de2c24b9b 100644
--- a/client/ui/qml/main2.qml
+++ b/client/ui/qml/main2.qml
@@ -183,7 +183,7 @@ Window {
id: privateKeyPassphraseDrawer
anchors.fill: parent
- expandedHeight: root.height * 0.35 + SettingsController.safeAreaBottomMargin
+ expandedHeight: root.height * 0.35 + SettingsController.safeAreaBottomMargin + SettingsController.imeHeight
expandedStateContent: ColumnLayout {
anchors.top: parent.top