mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-08 14:33:23 +00:00
Compare commits
13 Commits
4.8.15.0
...
feat/locat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
532a2b49b5 | ||
|
|
a685548b30 | ||
|
|
4c4d5a62ce | ||
|
|
38c48c86e4 | ||
|
|
0540cd0818 | ||
|
|
8e75da27ea | ||
|
|
5938abb605 | ||
|
|
1f14af1d22 | ||
|
|
e00bf7d591 | ||
|
|
d474d251bf | ||
|
|
1414f0ee84 | ||
|
|
47f4a83983 | ||
|
|
a9175f8dfc |
@@ -1,6 +1,8 @@
|
||||
#include "apiCountryModel.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QSettings>
|
||||
#include <utility>
|
||||
|
||||
#include "core/api/apiDefs.h"
|
||||
#include "logger.h"
|
||||
@@ -8,14 +10,143 @@
|
||||
namespace
|
||||
{
|
||||
Logger logger("ApiCountryModel");
|
||||
|
||||
constexpr QLatin1String countryConfig("country_config");
|
||||
constexpr QLatin1String regionEurope("Europe");
|
||||
constexpr QLatin1String regionAmerica("America");
|
||||
constexpr QLatin1String regionAsia("Asia");
|
||||
constexpr QLatin1String regionOceaniaAfrica("Oceania and Africa");
|
||||
constexpr QLatin1String regionOther("Other");
|
||||
|
||||
struct RegionRowData
|
||||
{
|
||||
bool isRegionHeader = false;
|
||||
QString regionName;
|
||||
bool isExpanded = true;
|
||||
int sourceIndex = -1;
|
||||
QString countryName;
|
||||
QString sourceCountryName;
|
||||
QString countryCode;
|
||||
QString countryImageCode;
|
||||
};
|
||||
|
||||
QString resolveRegionByIsoCode(const QString &isoCode)
|
||||
{
|
||||
static const QHash<QString, QString> isoToRegion = {
|
||||
{"BE", regionEurope},
|
||||
{"EE", regionEurope},
|
||||
{"FI", regionEurope},
|
||||
{"FR", regionEurope},
|
||||
{"GE", regionEurope},
|
||||
{"DE", regionEurope},
|
||||
{"NL", regionEurope},
|
||||
{"PL", regionEurope},
|
||||
{"RU", regionEurope},
|
||||
{"ES", regionEurope},
|
||||
{"SE", regionEurope},
|
||||
{"CH", regionEurope},
|
||||
{"TR", regionEurope},
|
||||
{"BR", regionAmerica},
|
||||
{"CA", regionAmerica},
|
||||
{"US", regionAmerica},
|
||||
{"AE", regionAsia},
|
||||
{"JP", regionAsia},
|
||||
{"KZ", regionAsia},
|
||||
{"KR", regionAsia},
|
||||
{"SG", regionAsia},
|
||||
{"AU", regionOceaniaAfrica},
|
||||
{"NZ", regionOceaniaAfrica},
|
||||
{"ZA", regionOceaniaAfrica},
|
||||
};
|
||||
|
||||
return isoToRegion.value(isoCode, regionOther);
|
||||
}
|
||||
}
|
||||
|
||||
ApiCountryModel::ApiCountryModel(QObject *parent) : QAbstractListModel(parent)
|
||||
class ApiCountryModel::RegionRowsModel : public QAbstractListModel
|
||||
{
|
||||
public:
|
||||
enum Roles {
|
||||
RowTypeRole = Qt::UserRole + 1,
|
||||
RegionNameRole,
|
||||
IsExpandedRole,
|
||||
SourceIndexRole,
|
||||
CountryNameRole,
|
||||
SourceCountryNameRole,
|
||||
CountryCodeRole,
|
||||
CountryImageCodeRole
|
||||
};
|
||||
|
||||
explicit RegionRowsModel(QObject *parent = nullptr) : QAbstractListModel(parent) {}
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
return m_rows.size();
|
||||
}
|
||||
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= m_rows.size()) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
const RegionRowData &row = m_rows.at(index.row());
|
||||
switch (role) {
|
||||
case RowTypeRole:
|
||||
return row.isRegionHeader ? "region" : "country";
|
||||
case RegionNameRole:
|
||||
return row.regionName;
|
||||
case IsExpandedRole:
|
||||
return row.isExpanded;
|
||||
case SourceIndexRole:
|
||||
return row.sourceIndex;
|
||||
case CountryNameRole:
|
||||
return row.countryName;
|
||||
case SourceCountryNameRole:
|
||||
return row.sourceCountryName;
|
||||
case CountryCodeRole:
|
||||
return row.countryCode;
|
||||
case CountryImageCodeRole:
|
||||
return row.countryImageCode;
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[RowTypeRole] = "rowType";
|
||||
roles[RegionNameRole] = "regionName";
|
||||
roles[IsExpandedRole] = "isExpanded";
|
||||
roles[SourceIndexRole] = "sourceIndex";
|
||||
roles[CountryNameRole] = "countryName";
|
||||
roles[SourceCountryNameRole] = "sourceCountryName";
|
||||
roles[CountryCodeRole] = "countryCode";
|
||||
roles[CountryImageCodeRole] = "countryImageCode";
|
||||
return roles;
|
||||
}
|
||||
|
||||
void setRows(QVector<RegionRowData> &&rows)
|
||||
{
|
||||
beginResetModel();
|
||||
m_rows = std::move(rows);
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
private:
|
||||
QVector<RegionRowData> m_rows;
|
||||
};
|
||||
|
||||
ApiCountryModel::ApiCountryModel(QObject *parent)
|
||||
: QAbstractListModel(parent), m_regionRowsModel(std::make_unique<RegionRowsModel>(this))
|
||||
{
|
||||
loadRegionExpansionState();
|
||||
rebuildGroupedRegions();
|
||||
}
|
||||
|
||||
ApiCountryModel::~ApiCountryModel() = default;
|
||||
|
||||
int ApiCountryModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
@@ -24,32 +155,28 @@ int ApiCountryModel::rowCount(const QModelIndex &parent) const
|
||||
|
||||
QVariant ApiCountryModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= static_cast<int>(rowCount()))
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= static_cast<int>(rowCount())) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
CountryInfo countryInfo = m_countries.at(index.row());
|
||||
IssuedConfigInfo issuedConfigInfo = m_issuedConfigs.value(countryInfo.countryCode);
|
||||
bool isIssued = issuedConfigInfo.sourceType == countryConfig;
|
||||
const CountryInfo &countryInfo = m_countries.at(index.row());
|
||||
const IssuedConfigInfo issuedConfigInfo = m_issuedConfigs.value(countryInfo.countryCode);
|
||||
const bool isIssued = issuedConfigInfo.sourceType == countryConfig;
|
||||
|
||||
switch (role) {
|
||||
case CountryCodeRole: {
|
||||
case CountryCodeRole:
|
||||
return countryInfo.countryCode;
|
||||
}
|
||||
case CountryNameRole: {
|
||||
case CountryNameRole:
|
||||
return countryInfo.countryName;
|
||||
}
|
||||
case CountryImageCodeRole: {
|
||||
case CountryImageCodeRole:
|
||||
return countryInfo.countryCode.toUpper();
|
||||
}
|
||||
case IsIssuedRole: {
|
||||
case IsIssuedRole:
|
||||
return isIssued;
|
||||
}
|
||||
case IsWorkerExpiredRole: {
|
||||
case IsWorkerExpiredRole:
|
||||
return issuedConfigInfo.lastDownloaded < issuedConfigInfo.workerLastUpdated;
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
void ApiCountryModel::updateModel(const QJsonArray &countries, const QString ¤tCountryCode)
|
||||
@@ -57,9 +184,9 @@ void ApiCountryModel::updateModel(const QJsonArray &countries, const QString &cu
|
||||
beginResetModel();
|
||||
|
||||
m_countries.clear();
|
||||
for (int i = 0; i < countries.size(); i++) {
|
||||
for (int i = 0; i < countries.size(); ++i) {
|
||||
CountryInfo countryInfo;
|
||||
QJsonObject countryObject = countries.at(i).toObject();
|
||||
const QJsonObject countryObject = countries.at(i).toObject();
|
||||
|
||||
countryInfo.countryName = countryObject.value(apiDefs::key::serverCountryName).toString();
|
||||
countryInfo.countryCode = countryObject.value(apiDefs::key::serverCountryCode).toString();
|
||||
@@ -72,6 +199,7 @@ void ApiCountryModel::updateModel(const QJsonArray &countries, const QString &cu
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
rebuildGroupedRegions();
|
||||
}
|
||||
|
||||
void ApiCountryModel::updateIssuedConfigsInfo(const QJsonArray &issuedConfigs)
|
||||
@@ -79,9 +207,9 @@ void ApiCountryModel::updateIssuedConfigsInfo(const QJsonArray &issuedConfigs)
|
||||
beginResetModel();
|
||||
|
||||
m_issuedConfigs.clear();
|
||||
for (int i = 0; i < issuedConfigs.size(); i++) {
|
||||
for (int i = 0; i < issuedConfigs.size(); ++i) {
|
||||
IssuedConfigInfo issuedConfigInfo;
|
||||
QJsonObject issuedConfigObject = issuedConfigs.at(i).toObject();
|
||||
const QJsonObject issuedConfigObject = issuedConfigs.at(i).toObject();
|
||||
|
||||
if (issuedConfigObject.value(apiDefs::key::sourceType).toString() != countryConfig) {
|
||||
continue;
|
||||
@@ -110,6 +238,52 @@ void ApiCountryModel::setCurrentIndex(const int i)
|
||||
emit currentIndexChanged(m_currentIndex);
|
||||
}
|
||||
|
||||
QString ApiCountryModel::searchText() const
|
||||
{
|
||||
return m_searchText;
|
||||
}
|
||||
|
||||
void ApiCountryModel::setSearchText(const QString &text)
|
||||
{
|
||||
if (m_searchText == text) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_searchText = text;
|
||||
emit searchTextChanged();
|
||||
rebuildGroupedRegions();
|
||||
}
|
||||
|
||||
QAbstractListModel *ApiCountryModel::regionRowsModel() const
|
||||
{
|
||||
return m_regionRowsModel.get();
|
||||
}
|
||||
|
||||
bool ApiCountryModel::hasVisibleRegions() const
|
||||
{
|
||||
return m_regionRowsModel && m_regionRowsModel->rowCount() > 0;
|
||||
}
|
||||
|
||||
bool ApiCountryModel::isRegionExpanded(const QString ®ionName) const
|
||||
{
|
||||
if (isSearchActive()) {
|
||||
return true;
|
||||
}
|
||||
return m_regionsExpanded.contains(regionName) ? m_regionsExpanded.value(regionName) : true;
|
||||
}
|
||||
|
||||
void ApiCountryModel::toggleRegionExpanded(const QString ®ionName)
|
||||
{
|
||||
if (regionName.isEmpty() || isSearchActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool currentValue = isRegionExpanded(regionName);
|
||||
m_regionsExpanded.insert(regionName, !currentValue);
|
||||
saveRegionExpansionState();
|
||||
rebuildGroupedRegions();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> ApiCountryModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
@@ -120,3 +294,168 @@ QHash<int, QByteArray> ApiCountryModel::roleNames() const
|
||||
roles[IsWorkerExpiredRole] = "isWorkerExpired";
|
||||
return roles;
|
||||
}
|
||||
|
||||
QString ApiCountryModel::normalizeCountryCode(const QString &countryCode) const
|
||||
{
|
||||
return countryCode.trimmed().toUpper();
|
||||
}
|
||||
|
||||
QString ApiCountryModel::extractCountryIsoCode(const QString &countryCode) const
|
||||
{
|
||||
const QString normalizedCode = normalizeCountryCode(countryCode);
|
||||
for (int i = 0; i + 1 < normalizedCode.size(); ++i) {
|
||||
const QChar first = normalizedCode.at(i);
|
||||
const QChar second = normalizedCode.at(i + 1);
|
||||
if (first.isUpper() && second.isUpper()) {
|
||||
return normalizedCode.mid(i, 2);
|
||||
}
|
||||
}
|
||||
return normalizedCode;
|
||||
}
|
||||
|
||||
QString ApiCountryModel::normalizeCountryName(const QString &countryName) const
|
||||
{
|
||||
return countryName.trimmed().toLower();
|
||||
}
|
||||
|
||||
QString ApiCountryModel::normalizeSearchComparableText(const QString &textValue) const
|
||||
{
|
||||
QString normalizedText = normalizeCountryName(textValue);
|
||||
normalizedText.replace(QChar(0x0451), QChar(0x0435)); // ё -> е
|
||||
normalizedText.replace(QChar(0x0439), QChar(0x0438)); // й -> и
|
||||
|
||||
QString result;
|
||||
result.reserve(normalizedText.size());
|
||||
for (int i = 0; i < normalizedText.size(); ++i) {
|
||||
const QChar currentChar = normalizedText.at(i);
|
||||
if (currentChar.isSpace()) {
|
||||
const QChar prevChar = i > 0 ? normalizedText.at(i - 1) : QChar();
|
||||
const QChar nextChar = i + 1 < normalizedText.size() ? normalizedText.at(i + 1) : QChar();
|
||||
const bool hasSpaceNeighbor = prevChar.isSpace() || nextChar.isSpace();
|
||||
if (hasSpaceNeighbor) {
|
||||
result.append(currentChar);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const bool isSeparator = currentChar == '.' || currentChar == '-';
|
||||
|
||||
if (!isSeparator) {
|
||||
result.append(currentChar);
|
||||
continue;
|
||||
}
|
||||
|
||||
const QChar prevChar = i > 0 ? normalizedText.at(i - 1) : QChar();
|
||||
const QChar nextChar = i + 1 < normalizedText.size() ? normalizedText.at(i + 1) : QChar();
|
||||
const bool hasSeparatorNeighbor = prevChar == '.' || prevChar == '-' || nextChar == '.' || nextChar == '-';
|
||||
if (hasSeparatorNeighbor) {
|
||||
result.append(currentChar);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ApiCountryModel::isCountryMatchingSearch(const QString &countryName, const QString &sourceCountryCode,
|
||||
const QString &normalizedSearchText) const
|
||||
{
|
||||
if (normalizedSearchText.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const QString normalizedCountryName = normalizeSearchComparableText(countryName);
|
||||
const QString normalizedSourceCountryCode = normalizeCountryCode(sourceCountryCode).toLower();
|
||||
|
||||
return normalizedCountryName.startsWith(normalizedSearchText) || normalizedSourceCountryCode.startsWith(normalizedSearchText);
|
||||
}
|
||||
|
||||
QString ApiCountryModel::getDisplayCountryName(const QString &countryName) const
|
||||
{
|
||||
const QString p2pPrefix = "[P2P] ";
|
||||
if (countryName.startsWith(p2pPrefix)) {
|
||||
return countryName.mid(p2pPrefix.size()) + " [P2P]";
|
||||
}
|
||||
return countryName;
|
||||
}
|
||||
|
||||
void ApiCountryModel::rebuildGroupedRegions()
|
||||
{
|
||||
QVector<RegionRowData> rows;
|
||||
const QString normalizedSearchText = normalizeSearchComparableText(m_searchText);
|
||||
const QStringList orderedRegions = {
|
||||
regionEurope,
|
||||
regionAmerica,
|
||||
regionAsia,
|
||||
regionOceaniaAfrica,
|
||||
regionOther,
|
||||
};
|
||||
|
||||
QHash<QString, QVector<RegionRowData>> groupedCountries;
|
||||
for (int sourceIndex = 0; sourceIndex < m_countries.size(); ++sourceIndex) {
|
||||
const CountryInfo &sourceCountry = m_countries.at(sourceIndex);
|
||||
if (!isCountryMatchingSearch(sourceCountry.countryName, sourceCountry.countryCode, normalizedSearchText)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QString regionName = resolveRegionByIsoCode(extractCountryIsoCode(sourceCountry.countryCode));
|
||||
RegionRowData countryRow;
|
||||
countryRow.isRegionHeader = false;
|
||||
countryRow.regionName = regionName;
|
||||
countryRow.sourceIndex = sourceIndex;
|
||||
countryRow.countryName = getDisplayCountryName(sourceCountry.countryName);
|
||||
countryRow.sourceCountryName = sourceCountry.countryName;
|
||||
countryRow.countryCode = sourceCountry.countryCode;
|
||||
countryRow.countryImageCode = extractCountryIsoCode(sourceCountry.countryCode);
|
||||
groupedCountries[regionName].push_back(std::move(countryRow));
|
||||
}
|
||||
|
||||
for (const QString ®ionName : orderedRegions) {
|
||||
QVector<RegionRowData> countries = groupedCountries.value(regionName);
|
||||
if (countries.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool expanded = isRegionExpanded(regionName);
|
||||
|
||||
RegionRowData headerRow;
|
||||
headerRow.isRegionHeader = true;
|
||||
headerRow.regionName = regionName;
|
||||
headerRow.isExpanded = expanded;
|
||||
rows.push_back(std::move(headerRow));
|
||||
|
||||
if (expanded) {
|
||||
for (RegionRowData &countryRow : countries) {
|
||||
countryRow.isExpanded = expanded;
|
||||
rows.push_back(std::move(countryRow));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_regionRowsModel->setRows(std::move(rows));
|
||||
emit regionRowsChanged();
|
||||
}
|
||||
|
||||
void ApiCountryModel::loadRegionExpansionState()
|
||||
{
|
||||
QSettings settings;
|
||||
const QVariantMap stored = settings.value("PageSettingsApiAvailableCountries/regionsExpanded").toMap();
|
||||
m_regionsExpanded.clear();
|
||||
for (auto it = stored.constBegin(); it != stored.constEnd(); ++it) {
|
||||
m_regionsExpanded.insert(it.key(), it.value().toBool());
|
||||
}
|
||||
}
|
||||
|
||||
void ApiCountryModel::saveRegionExpansionState() const
|
||||
{
|
||||
QVariantMap stored;
|
||||
for (auto it = m_regionsExpanded.constBegin(); it != m_regionsExpanded.constEnd(); ++it) {
|
||||
stored.insert(it.key(), it.value());
|
||||
}
|
||||
|
||||
QSettings settings;
|
||||
settings.setValue("PageSettingsApiAvailableCountries/regionsExpanded", stored);
|
||||
}
|
||||
|
||||
bool ApiCountryModel::isSearchActive() const
|
||||
{
|
||||
return !normalizeSearchComparableText(m_searchText).isEmpty();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QAbstractListModel>
|
||||
#include <QHash>
|
||||
#include <QJsonArray>
|
||||
#include <memory>
|
||||
|
||||
class ApiCountryModel : public QAbstractListModel
|
||||
{
|
||||
@@ -19,12 +20,16 @@ public:
|
||||
};
|
||||
|
||||
explicit ApiCountryModel(QObject *parent = nullptr);
|
||||
~ApiCountryModel() override;
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
Q_PROPERTY(int currentIndex READ getCurrentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged)
|
||||
Q_PROPERTY(QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged)
|
||||
Q_PROPERTY(QAbstractListModel *regionRowsModel READ regionRowsModel CONSTANT)
|
||||
Q_PROPERTY(bool hasVisibleRegions READ hasVisibleRegions NOTIFY regionRowsChanged)
|
||||
|
||||
public slots:
|
||||
void updateModel(const QJsonArray &countries, const QString ¤tCountryCode);
|
||||
@@ -32,9 +37,17 @@ public slots:
|
||||
|
||||
int getCurrentIndex();
|
||||
void setCurrentIndex(const int i);
|
||||
QString searchText() const;
|
||||
void setSearchText(const QString &text);
|
||||
QAbstractListModel *regionRowsModel() const;
|
||||
bool hasVisibleRegions() const;
|
||||
Q_INVOKABLE bool isRegionExpanded(const QString ®ionName) const;
|
||||
Q_INVOKABLE void toggleRegionExpanded(const QString ®ionName);
|
||||
|
||||
signals:
|
||||
void currentIndexChanged(const int index);
|
||||
void searchTextChanged();
|
||||
void regionRowsChanged();
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
@@ -57,7 +70,23 @@ private:
|
||||
|
||||
QVector<CountryInfo> m_countries;
|
||||
QHash<QString, IssuedConfigInfo> m_issuedConfigs;
|
||||
int m_currentIndex;
|
||||
int m_currentIndex = -1;
|
||||
QString m_searchText;
|
||||
QHash<QString, bool> m_regionsExpanded;
|
||||
class RegionRowsModel;
|
||||
std::unique_ptr<RegionRowsModel> m_regionRowsModel;
|
||||
|
||||
QString normalizeCountryCode(const QString &countryCode) const;
|
||||
QString extractCountryIsoCode(const QString &countryCode) const;
|
||||
QString normalizeCountryName(const QString &countryName) const;
|
||||
QString normalizeSearchComparableText(const QString &textValue) const;
|
||||
bool isCountryMatchingSearch(const QString &countryName, const QString &sourceCountryCode,
|
||||
const QString &normalizedSearchText) const;
|
||||
QString getDisplayCountryName(const QString &countryName) const;
|
||||
void rebuildGroupedRegions();
|
||||
void loadRegionExpansionState();
|
||||
void saveRegionExpansionState() const;
|
||||
bool isSearchActive() const;
|
||||
};
|
||||
|
||||
#endif // APICOUNTRYMODEL_H
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs
|
||||
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
@@ -18,6 +17,7 @@ PageType {
|
||||
id: root
|
||||
|
||||
property var processedServer
|
||||
readonly property int topBarHeight: 20 + SettingsController.safeAreaTopMargin + backButton.implicitHeight + 12
|
||||
|
||||
Connections {
|
||||
target: ServersModel
|
||||
@@ -44,12 +44,38 @@ PageType {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: topBar
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: root.topBarHeight
|
||||
color: AmneziaStyle.color.midnightBlack
|
||||
z: 10
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.AllButtons
|
||||
}
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
objectName: "backButton"
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
|
||||
anchors.leftMargin: 16
|
||||
z: 1
|
||||
}
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: menuContent
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
model: ApiCountryModel
|
||||
model: ApiCountryModel.regionRowsModel
|
||||
|
||||
currentIndex: 0
|
||||
|
||||
@@ -62,13 +88,6 @@ PageType {
|
||||
|
||||
spacing: 4
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
objectName: "backButton"
|
||||
|
||||
Layout.topMargin: 20 + SettingsController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
HeaderTypeWithButton {
|
||||
id: headerContent
|
||||
objectName: "headerContent"
|
||||
@@ -76,6 +95,7 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: topBar.height + 4
|
||||
Layout.bottomMargin: 10
|
||||
|
||||
actionButtonImage: "qrc:/images/controls/settings.svg"
|
||||
@@ -94,76 +114,238 @@ PageType {
|
||||
PageController.goToPage(PageEnum.PageSettingsApiServerInfo)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 10
|
||||
|
||||
implicitHeight: 56
|
||||
radius: 16
|
||||
|
||||
color: AmneziaStyle.color.onyxBlack
|
||||
border.color: searchField.activeFocus ? AmneziaStyle.color.paleGray : AmneziaStyle.color.slateGray
|
||||
border.width: 1
|
||||
|
||||
Behavior on border.color {
|
||||
PropertyAnimation { duration: 200 }
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 8
|
||||
spacing: 8
|
||||
|
||||
Image {
|
||||
source: "qrc:/images/controls/search.svg"
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: searchField
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: AmneziaStyle.color.paleGray
|
||||
placeholderText: "country or country code"
|
||||
placeholderTextColor: AmneziaStyle.color.charcoalGray
|
||||
|
||||
selectionColor: AmneziaStyle.color.richBrown
|
||||
selectedTextColor: AmneziaStyle.color.paleGray
|
||||
|
||||
font.pixelSize: 16
|
||||
font.weight: 400
|
||||
font.family: "PT Root UI VF"
|
||||
|
||||
inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText
|
||||
|
||||
topPadding: 0
|
||||
rightPadding: 0
|
||||
leftPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
background: Rectangle {
|
||||
color: AmneziaStyle.color.transparent
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
const shouldRestoreFocus = activeFocus
|
||||
const previousCursorPosition = cursorPosition
|
||||
|
||||
ApiCountryModel.searchText = text
|
||||
|
||||
if (shouldRestoreFocus) {
|
||||
Qt.callLater(function() {
|
||||
searchField.forceActiveFocus()
|
||||
searchField.cursorPosition = Math.min(previousCursorPosition, searchField.text.length)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
searchField.text = ""
|
||||
}
|
||||
|
||||
ContextMenu.menu: ContextMenuType {
|
||||
textObj: searchField
|
||||
}
|
||||
}
|
||||
|
||||
ImageButtonType {
|
||||
visible: searchField.text !== ""
|
||||
|
||||
implicitWidth: 40
|
||||
implicitHeight: 40
|
||||
|
||||
hoverEnabled: true
|
||||
image: "qrc:/images/controls/close.svg"
|
||||
imageColor: AmneziaStyle.color.paleGray
|
||||
|
||||
onClicked: {
|
||||
searchField.text = ""
|
||||
}
|
||||
Keys.onEnterPressed: {
|
||||
searchField.text = ""
|
||||
}
|
||||
Keys.onReturnPressed: {
|
||||
searchField.text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate: ColumnLayout {
|
||||
id: content
|
||||
|
||||
footer: Item {
|
||||
width: menuContent.width
|
||||
height: content.implicitHeight
|
||||
height: ApiCountryModel.hasVisibleRegions ? 0 : emptyStateText.implicitHeight + 32
|
||||
|
||||
RowLayout {
|
||||
VerticalRadioButton {
|
||||
id: containerRadioButton
|
||||
CaptionTextType {
|
||||
id: emptyStateText
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 16
|
||||
|
||||
text: countryName
|
||||
visible: !ApiCountryModel.hasVisibleRegions
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
|
||||
ButtonGroup.group: containersRadioButtonGroup
|
||||
font.pixelSize: 15
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
wrapMode: Text.WordWrap
|
||||
text: "Nothing found. Try a different spelling or switch keyboard layout."
|
||||
}
|
||||
}
|
||||
|
||||
imageSource: "qrc:/images/controls/download.svg"
|
||||
delegate: Item {
|
||||
width: menuContent.width
|
||||
implicitHeight: rowType === "region" ? 44 : 88
|
||||
|
||||
checked: index === ApiCountryModel.currentIndex
|
||||
checkable: !ConnectionController.isConnected
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: rowType === "region"
|
||||
|
||||
onClicked: {
|
||||
if (ConnectionController.isConnectionInProgress) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change server location while trying to make an active connection"))
|
||||
return
|
||||
}
|
||||
if (ConnectionController.isConnected) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change server location while there is an active connection"))
|
||||
return
|
||||
}
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.topMargin: 12
|
||||
anchors.bottomMargin: 8
|
||||
spacing: 8
|
||||
|
||||
if (index !== ApiCountryModel.currentIndex) {
|
||||
PageController.showBusyIndicator(true)
|
||||
var prevIndex = ApiCountryModel.currentIndex
|
||||
ApiCountryModel.currentIndex = index
|
||||
if (!ApiConfigsController.updateServiceFromGateway(ServersModel.defaultIndex, countryCode, countryName)) {
|
||||
ApiCountryModel.currentIndex = prevIndex
|
||||
}
|
||||
PageController.showBusyIndicator(false)
|
||||
}
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
text: regionName
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
if (checkable) {
|
||||
checked = true
|
||||
}
|
||||
containerRadioButton.clicked()
|
||||
}
|
||||
Keys.onReturnPressed: {
|
||||
if (checkable) {
|
||||
checked = true
|
||||
}
|
||||
containerRadioButton.clicked()
|
||||
Image {
|
||||
source: isExpanded ? "qrc:/images/controls/chevron-up.svg"
|
||||
: "qrc:/images/controls/chevron-down.svg"
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
Layout.rightMargin: 32
|
||||
Layout.alignment: Qt.AlignRight
|
||||
|
||||
source: "qrc:/countriesFlags/images/flagKit/" + countryImageCode + ".svg"
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
ApiCountryModel.toggleRegionExpanded(regionName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
Layout.fillWidth: true
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
visible: rowType === "country"
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
VerticalRadioButton {
|
||||
id: containerRadioButton
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
|
||||
text: countryName
|
||||
ButtonGroup.group: containersRadioButtonGroup
|
||||
imageSource: "qrc:/images/controls/download.svg"
|
||||
|
||||
checked: sourceIndex >= 0 && sourceIndex === ApiCountryModel.currentIndex
|
||||
checkable: !ConnectionController.isConnected
|
||||
|
||||
onClicked: {
|
||||
if (ConnectionController.isConnectionInProgress) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change server location while trying to make an active connection"))
|
||||
return
|
||||
}
|
||||
if (ConnectionController.isConnected) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change server location while there is an active connection"))
|
||||
return
|
||||
}
|
||||
|
||||
if (sourceIndex !== ApiCountryModel.currentIndex) {
|
||||
PageController.showBusyIndicator(true)
|
||||
var prevIndex = ApiCountryModel.currentIndex
|
||||
ApiCountryModel.currentIndex = sourceIndex
|
||||
if (!ApiConfigsController.updateServiceFromGateway(ServersModel.defaultIndex, countryCode, sourceCountryName)) {
|
||||
ApiCountryModel.currentIndex = prevIndex
|
||||
}
|
||||
PageController.showBusyIndicator(false)
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
if (checkable) {
|
||||
checked = true
|
||||
}
|
||||
containerRadioButton.clicked()
|
||||
}
|
||||
Keys.onReturnPressed: {
|
||||
if (checkable) {
|
||||
checked = true
|
||||
}
|
||||
containerRadioButton.clicked()
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
Layout.rightMargin: 32
|
||||
Layout.alignment: Qt.AlignRight
|
||||
source: "qrc:/countriesFlags/images/flagKit/" + countryImageCode + ".svg"
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user