mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-22 02:01:08 +07:00
refactor: replace groupedRegions with internal list model
This commit is contained in:
@@ -94,9 +94,6 @@ void CoreController::initModels()
|
|||||||
m_apiCountryModel.reset(new ApiCountryModel(this));
|
m_apiCountryModel.reset(new ApiCountryModel(this));
|
||||||
m_engine->rootContext()->setContextProperty("ApiCountryModel", m_apiCountryModel.get());
|
m_engine->rootContext()->setContextProperty("ApiCountryModel", m_apiCountryModel.get());
|
||||||
|
|
||||||
m_apiCountriesRegionModel.reset(new ApiCountriesRegionModel(m_apiCountryModel.get(), this));
|
|
||||||
m_engine->rootContext()->setContextProperty("ApiCountriesRegionModel", m_apiCountriesRegionModel.get());
|
|
||||||
|
|
||||||
m_apiAccountInfoModel.reset(new ApiAccountInfoModel(this));
|
m_apiAccountInfoModel.reset(new ApiAccountInfoModel(this));
|
||||||
m_engine->rootContext()->setContextProperty("ApiAccountInfoModel", m_apiAccountInfoModel.get());
|
m_engine->rootContext()->setContextProperty("ApiAccountInfoModel", m_apiAccountInfoModel.get());
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
#endif
|
#endif
|
||||||
#include "ui/models/api/apiAccountInfoModel.h"
|
#include "ui/models/api/apiAccountInfoModel.h"
|
||||||
#include "ui/models/api/apiCountryModel.h"
|
#include "ui/models/api/apiCountryModel.h"
|
||||||
#include "ui/models/api/apiCountriesRegionModel.h"
|
|
||||||
#include "ui/models/api/apiDevicesModel.h"
|
#include "ui/models/api/apiDevicesModel.h"
|
||||||
#include "ui/models/api/apiServicesModel.h"
|
#include "ui/models/api/apiServicesModel.h"
|
||||||
#include "ui/models/appSplitTunnelingModel.h"
|
#include "ui/models/appSplitTunnelingModel.h"
|
||||||
@@ -135,7 +134,6 @@ private:
|
|||||||
|
|
||||||
QSharedPointer<ApiServicesModel> m_apiServicesModel;
|
QSharedPointer<ApiServicesModel> m_apiServicesModel;
|
||||||
QSharedPointer<ApiCountryModel> m_apiCountryModel;
|
QSharedPointer<ApiCountryModel> m_apiCountryModel;
|
||||||
QSharedPointer<ApiCountriesRegionModel> m_apiCountriesRegionModel;
|
|
||||||
QSharedPointer<ApiAccountInfoModel> m_apiAccountInfoModel;
|
QSharedPointer<ApiAccountInfoModel> m_apiAccountInfoModel;
|
||||||
QSharedPointer<ApiDevicesModel> m_apiDevicesModel;
|
QSharedPointer<ApiDevicesModel> m_apiDevicesModel;
|
||||||
|
|
||||||
|
|||||||
@@ -1,367 +0,0 @@
|
|||||||
#include "apiCountriesRegionModel.h"
|
|
||||||
|
|
||||||
#include <utility>
|
|
||||||
#include <QSettings>
|
|
||||||
#include <QVariantMap>
|
|
||||||
|
|
||||||
#include "apiCountryModel.h"
|
|
||||||
|
|
||||||
ApiCountriesRegionModel::ApiCountriesRegionModel(ApiCountryModel *sourceModel, QObject *parent)
|
|
||||||
: QAbstractListModel(parent), m_sourceModel(sourceModel)
|
|
||||||
{
|
|
||||||
m_regionDefinitions = {
|
|
||||||
{
|
|
||||||
"Europe",
|
|
||||||
{
|
|
||||||
{"BE", "Belgium", "Бельгия"},
|
|
||||||
{"EE", "Estonia", "Эстония"},
|
|
||||||
{"FI", "Finland", "Финляндия"},
|
|
||||||
{"FR", "France", "Франция"},
|
|
||||||
{"GE", "Georgia", "Грузия"},
|
|
||||||
{"DE", "Germany", "Германия"},
|
|
||||||
{"NL", "Netherlands", "Нидерланды"},
|
|
||||||
{"PL", "Poland", "Польша"},
|
|
||||||
{"RU", "Russia", "Россия"},
|
|
||||||
{"ES", "Spain", "Испания"},
|
|
||||||
{"SE", "Sweden", "Швеция"},
|
|
||||||
{"CH", "Switzerland", "Швейцария"},
|
|
||||||
{"TR", "Turkey", "Турция"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"America",
|
|
||||||
{
|
|
||||||
{"BR", "Brazil", "Бразилия"},
|
|
||||||
{"CA", "Canada East", "Канада"},
|
|
||||||
{"US", "USA East", "США"},
|
|
||||||
{"US", "USA West", "США"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Asia",
|
|
||||||
{
|
|
||||||
{"AE", "UAE", "ОАЭ"},
|
|
||||||
{"JP", "Japan", "Япония"},
|
|
||||||
{"KZ", "Kazakhstan", "Казахстан"},
|
|
||||||
{"KR", "South Korea", "Южная Корея"},
|
|
||||||
{"SG", "Singapore", "Сингапур"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Oceania and Africa",
|
|
||||||
{
|
|
||||||
{"AU", "Australia", "Австралия"},
|
|
||||||
{"NZ", "New Zealand", "Новая Зеландия"},
|
|
||||||
{"ZA", "South Africa", "Южная Африка"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (m_sourceModel) {
|
|
||||||
connect(m_sourceModel, &QAbstractItemModel::modelReset, this, &ApiCountriesRegionModel::rebuildModel);
|
|
||||||
connect(m_sourceModel, &QAbstractItemModel::rowsInserted, this, &ApiCountriesRegionModel::rebuildModel);
|
|
||||||
connect(m_sourceModel, &QAbstractItemModel::rowsRemoved, this, &ApiCountriesRegionModel::rebuildModel);
|
|
||||||
connect(m_sourceModel, &QAbstractItemModel::dataChanged, this, &ApiCountriesRegionModel::rebuildModel);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadRegionExpansionState();
|
|
||||||
rebuildModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
int ApiCountriesRegionModel::rowCount(const QModelIndex &parent) const
|
|
||||||
{
|
|
||||||
Q_UNUSED(parent)
|
|
||||||
return m_regions.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant ApiCountriesRegionModel::data(const QModelIndex &index, int role) const
|
|
||||||
{
|
|
||||||
if (!index.isValid() || index.row() < 0 || index.row() >= static_cast<int>(rowCount())) {
|
|
||||||
return QVariant();
|
|
||||||
}
|
|
||||||
|
|
||||||
const RegionData ®ion = m_regions.at(index.row());
|
|
||||||
switch (role) {
|
|
||||||
case RegionNameRole:
|
|
||||||
return region.regionName;
|
|
||||||
case CountriesRole:
|
|
||||||
return region.countries;
|
|
||||||
default:
|
|
||||||
return QVariant();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ApiCountriesRegionModel::searchText() const
|
|
||||||
{
|
|
||||||
return m_searchText;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ApiCountriesRegionModel::setSearchText(const QString &text)
|
|
||||||
{
|
|
||||||
if (m_searchText == text) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_searchText = text;
|
|
||||||
emit searchTextChanged();
|
|
||||||
rebuildModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ApiCountriesRegionModel::isRegionExpanded(const QString ®ionName) const
|
|
||||||
{
|
|
||||||
if (isSearchActive()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return m_regionsExpanded.contains(regionName) ? m_regionsExpanded.value(regionName) : true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ApiCountriesRegionModel::toggleRegionExpanded(const QString ®ionName)
|
|
||||||
{
|
|
||||||
if (regionName.isEmpty() || isSearchActive()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool currentValue = isRegionExpanded(regionName);
|
|
||||||
m_regionsExpanded.insert(regionName, !currentValue);
|
|
||||||
saveRegionExpansionState();
|
|
||||||
|
|
||||||
beginResetModel();
|
|
||||||
endResetModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<int, QByteArray> ApiCountriesRegionModel::roleNames() const
|
|
||||||
{
|
|
||||||
QHash<int, QByteArray> roles;
|
|
||||||
roles[RegionNameRole] = "regionName";
|
|
||||||
roles[CountriesRole] = "countries";
|
|
||||||
return roles;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ApiCountriesRegionModel::normalizeCountryCode(const QString &countryCode) const
|
|
||||||
{
|
|
||||||
return countryCode.trimmed().toUpper();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ApiCountriesRegionModel::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 ApiCountriesRegionModel::normalizeCountryName(const QString &countryName) const
|
|
||||||
{
|
|
||||||
return countryName.trimmed().toLower();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ApiCountriesRegionModel::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);
|
|
||||||
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 ApiCountriesRegionModel::isCountryMatchingSearch(const QString &countryName, const QString ®ionCountryCode,
|
|
||||||
const QString &sourceCountryCode, const QString &ruCountryName) const
|
|
||||||
{
|
|
||||||
const QString normalizedSearchText = normalizeSearchComparableText(m_searchText);
|
|
||||||
if (normalizedSearchText.isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString normalizedCountryName = normalizeSearchComparableText(countryName);
|
|
||||||
const QString normalizedRuCountryName = normalizeSearchComparableText(ruCountryName);
|
|
||||||
const QString normalizedRegionCountryCode = normalizeCountryCode(regionCountryCode).toLower();
|
|
||||||
const QString normalizedSourceCountryCode = normalizeCountryCode(sourceCountryCode).toLower();
|
|
||||||
|
|
||||||
return normalizedCountryName.startsWith(normalizedSearchText) || normalizedRuCountryName.startsWith(normalizedSearchText) ||
|
|
||||||
normalizedRegionCountryCode.startsWith(normalizedSearchText) ||
|
|
||||||
normalizedSourceCountryCode.startsWith(normalizedSearchText);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ApiCountriesRegionModel::isSearchActive() const
|
|
||||||
{
|
|
||||||
return !normalizeSearchComparableText(m_searchText).isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ApiCountriesRegionModel::getDisplayCountryName(const QString &countryName) const
|
|
||||||
{
|
|
||||||
const QString p2pPrefix = "[P2P] ";
|
|
||||||
if (countryName.startsWith(p2pPrefix)) {
|
|
||||||
return countryName.mid(p2pPrefix.size()) + " [P2P]";
|
|
||||||
}
|
|
||||||
return countryName;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ApiCountriesRegionModel::getSourceCountry(int sourceIndex, SourceCountry &country) const
|
|
||||||
{
|
|
||||||
if (!m_sourceModel || sourceIndex < 0 || sourceIndex >= m_sourceModel->rowCount()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QModelIndex modelIndex = m_sourceModel->index(sourceIndex, 0);
|
|
||||||
if (!modelIndex.isValid()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
country.countryName = m_sourceModel->data(modelIndex, ApiCountryModel::CountryNameRole).toString();
|
|
||||||
country.countryCode = m_sourceModel->data(modelIndex, ApiCountryModel::CountryCodeRole).toString();
|
|
||||||
country.countryImageCode = m_sourceModel->data(modelIndex, ApiCountryModel::CountryImageCodeRole).toString();
|
|
||||||
|
|
||||||
return !country.countryName.isEmpty() && !country.countryCode.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
int ApiCountriesRegionModel::findCountryIndexByRef(const CountryRef &countryRef, const QHash<int, bool> &usedIndices) const
|
|
||||||
{
|
|
||||||
if (!m_sourceModel) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString expectedCode = normalizeCountryCode(countryRef.code);
|
|
||||||
const QString expectedName = normalizeCountryName(countryRef.name);
|
|
||||||
|
|
||||||
const int countriesCount = m_sourceModel->rowCount();
|
|
||||||
for (int i = 0; i < countriesCount; ++i) {
|
|
||||||
if (usedIndices.value(i)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
SourceCountry sourceCountry;
|
|
||||||
if (!getSourceCountry(i, sourceCountry)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString modelCode = normalizeCountryCode(sourceCountry.countryCode);
|
|
||||||
const QString modelIsoCode = extractCountryIsoCode(sourceCountry.countryCode);
|
|
||||||
const QString modelName = normalizeCountryName(sourceCountry.countryName);
|
|
||||||
|
|
||||||
if (!expectedName.isEmpty() && modelName == expectedName) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!expectedCode.isEmpty() && (modelCode == expectedCode || modelIsoCode == expectedCode)) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ApiCountriesRegionModel::rebuildModel()
|
|
||||||
{
|
|
||||||
beginResetModel();
|
|
||||||
|
|
||||||
QVector<RegionData> regions;
|
|
||||||
regions.reserve(m_regionDefinitions.size());
|
|
||||||
const auto ®ionDefinitions = std::as_const(m_regionDefinitions);
|
|
||||||
for (const RegionDefinition ®ionDefinition : regionDefinitions) {
|
|
||||||
RegionData region;
|
|
||||||
region.regionName = regionDefinition.regionName;
|
|
||||||
regions.push_back(region);
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<int, bool> usedIndices;
|
|
||||||
for (int regionIndex = 0; regionIndex < m_regionDefinitions.size(); ++regionIndex) {
|
|
||||||
const RegionDefinition ®ionDefinition = m_regionDefinitions.at(regionIndex);
|
|
||||||
for (const CountryRef &countryRef : regionDefinition.countries) {
|
|
||||||
const int sourceIndex = findCountryIndexByRef(countryRef, usedIndices);
|
|
||||||
|
|
||||||
if (sourceIndex < 0) {
|
|
||||||
if (isCountryMatchingSearch(countryRef.name, countryRef.code, countryRef.code, countryRef.ruName)) {
|
|
||||||
QVariantMap fallbackCountry;
|
|
||||||
fallbackCountry.insert("sourceIndex", -1);
|
|
||||||
fallbackCountry.insert("countryName", getDisplayCountryName(countryRef.name));
|
|
||||||
fallbackCountry.insert("sourceCountryName", countryRef.name);
|
|
||||||
fallbackCountry.insert("countryCode", countryRef.code);
|
|
||||||
fallbackCountry.insert("countryImageCode", extractCountryIsoCode(countryRef.code));
|
|
||||||
fallbackCountry.insert("isAvailable", false);
|
|
||||||
regions[regionIndex].countries.push_back(fallbackCountry);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
SourceCountry sourceCountry;
|
|
||||||
if (!getSourceCountry(sourceIndex, sourceCountry)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString displayCountryName = getDisplayCountryName(sourceCountry.countryName);
|
|
||||||
if (!isCountryMatchingSearch(displayCountryName, countryRef.code, sourceCountry.countryCode, countryRef.ruName)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariantMap countryData;
|
|
||||||
countryData.insert("sourceIndex", sourceIndex);
|
|
||||||
countryData.insert("countryName", displayCountryName);
|
|
||||||
countryData.insert("sourceCountryName", sourceCountry.countryName);
|
|
||||||
countryData.insert("countryCode", sourceCountry.countryCode);
|
|
||||||
countryData.insert("countryImageCode", extractCountryIsoCode(sourceCountry.countryImageCode));
|
|
||||||
countryData.insert("isAvailable", true);
|
|
||||||
regions[regionIndex].countries.push_back(countryData);
|
|
||||||
usedIndices.insert(sourceIndex, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_regions.clear();
|
|
||||||
m_regions.reserve(regions.size());
|
|
||||||
const auto ®ionsConst = std::as_const(regions);
|
|
||||||
for (const RegionData ®ion : regionsConst) {
|
|
||||||
if (!region.countries.isEmpty()) {
|
|
||||||
m_regions.push_back(region);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
endResetModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ApiCountriesRegionModel::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 ApiCountriesRegionModel::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);
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
#ifndef APICOUNTRIESREGIONMODEL_H
|
|
||||||
#define APICOUNTRIESREGIONMODEL_H
|
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
|
||||||
#include <QHash>
|
|
||||||
#include <QVector>
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
class ApiCountryModel;
|
|
||||||
|
|
||||||
class ApiCountriesRegionModel : public QAbstractListModel
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
Q_PROPERTY(QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged)
|
|
||||||
|
|
||||||
public:
|
|
||||||
enum Roles {
|
|
||||||
RegionNameRole = Qt::UserRole + 1,
|
|
||||||
CountriesRole
|
|
||||||
};
|
|
||||||
|
|
||||||
explicit ApiCountriesRegionModel(ApiCountryModel *sourceModel, QObject *parent = nullptr);
|
|
||||||
|
|
||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
|
||||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
|
||||||
|
|
||||||
QString searchText() const;
|
|
||||||
void setSearchText(const QString &text);
|
|
||||||
Q_INVOKABLE bool isRegionExpanded(const QString ®ionName) const;
|
|
||||||
Q_INVOKABLE void toggleRegionExpanded(const QString ®ionName);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void searchTextChanged();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
QHash<int, QByteArray> roleNames() const override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct CountryRef
|
|
||||||
{
|
|
||||||
QString code;
|
|
||||||
QString name;
|
|
||||||
QString ruName;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct RegionDefinition
|
|
||||||
{
|
|
||||||
QString regionName;
|
|
||||||
QVector<CountryRef> countries;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct SourceCountry
|
|
||||||
{
|
|
||||||
QString countryName;
|
|
||||||
QString countryCode;
|
|
||||||
QString countryImageCode;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct RegionData
|
|
||||||
{
|
|
||||||
QString regionName;
|
|
||||||
QVariantList countries;
|
|
||||||
};
|
|
||||||
|
|
||||||
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 ®ionCountryCode, const QString &sourceCountryCode,
|
|
||||||
const QString &ruCountryName) const;
|
|
||||||
QString getDisplayCountryName(const QString &countryName) const;
|
|
||||||
|
|
||||||
bool getSourceCountry(int sourceIndex, SourceCountry &country) const;
|
|
||||||
int findCountryIndexByRef(const CountryRef &countryRef, const QHash<int, bool> &usedIndices) const;
|
|
||||||
void rebuildModel();
|
|
||||||
void loadRegionExpansionState();
|
|
||||||
void saveRegionExpansionState() const;
|
|
||||||
bool isSearchActive() const;
|
|
||||||
|
|
||||||
QVector<RegionDefinition> m_regionDefinitions;
|
|
||||||
QVector<RegionData> m_regions;
|
|
||||||
ApiCountryModel *m_sourceModel = nullptr;
|
|
||||||
QString m_searchText;
|
|
||||||
QHash<QString, bool> m_regionsExpanded;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // APICOUNTRIESREGIONMODEL_H
|
|
||||||
@@ -1,21 +1,160 @@
|
|||||||
#include "apiCountryModel.h"
|
#include "apiCountryModel.h"
|
||||||
|
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QSettings>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
#include "core/api/apiDefs.h"
|
#include "core/api/apiDefs.h"
|
||||||
#include "logger.h"
|
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
Logger logger("ApiCountryModel");
|
constexpr QLatin1String countryConfig("country_config");
|
||||||
|
|
||||||
constexpr QLatin1String countryConfig("country_config");
|
struct RegionRowData
|
||||||
}
|
|
||||||
|
|
||||||
ApiCountryModel::ApiCountryModel(QObject *parent) : QAbstractListModel(parent)
|
|
||||||
{
|
{
|
||||||
|
bool isRegionHeader = false;
|
||||||
|
QString regionName;
|
||||||
|
bool isExpanded = true;
|
||||||
|
int sourceIndex = -1;
|
||||||
|
QString countryName;
|
||||||
|
QString sourceCountryName;
|
||||||
|
QString countryCode;
|
||||||
|
QString countryImageCode;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
m_regionDefinitions = {
|
||||||
|
{
|
||||||
|
"Europe",
|
||||||
|
{
|
||||||
|
{"BE", "Belgium", "Бельгия"},
|
||||||
|
{"EE", "Estonia", "Эстония"},
|
||||||
|
{"FI", "Finland", "Финляндия"},
|
||||||
|
{"FR", "France", "Франция"},
|
||||||
|
{"GE", "Georgia", "Грузия"},
|
||||||
|
{"DE", "Germany", "Германия"},
|
||||||
|
{"NL", "Netherlands", "Нидерланды"},
|
||||||
|
{"PL", "Poland", "Польша"},
|
||||||
|
{"RU", "Russia", "Россия"},
|
||||||
|
{"ES", "Spain", "Испания"},
|
||||||
|
{"SE", "Sweden", "Швеция"},
|
||||||
|
{"CH", "Switzerland", "Швейцария"},
|
||||||
|
{"TR", "Turkey", "Турция"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"America",
|
||||||
|
{
|
||||||
|
{"BR", "Brazil", "Бразилия"},
|
||||||
|
{"CA", "Canada East", "Канада"},
|
||||||
|
{"US", "USA East", "США"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Asia",
|
||||||
|
{
|
||||||
|
{"AE", "UAE", "ОАЭ"},
|
||||||
|
{"JP", "Japan", "Япония"},
|
||||||
|
{"KZ", "Kazakhstan", "Казахстан"},
|
||||||
|
{"KR", "South Korea", "Южная Корея"},
|
||||||
|
{"SG", "Singapore", "Сингапур"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Oceania and Africa",
|
||||||
|
{
|
||||||
|
{"AU", "Australia", "Австралия"},
|
||||||
|
{"NZ", "New Zealand", "Новая Зеландия"},
|
||||||
|
{"ZA", "South Africa", "Южная Африка"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
loadRegionExpansionState();
|
||||||
|
rebuildGroupedRegions();
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiCountryModel::~ApiCountryModel() = default;
|
||||||
|
|
||||||
int ApiCountryModel::rowCount(const QModelIndex &parent) const
|
int ApiCountryModel::rowCount(const QModelIndex &parent) const
|
||||||
{
|
{
|
||||||
Q_UNUSED(parent)
|
Q_UNUSED(parent)
|
||||||
@@ -24,32 +163,28 @@ int ApiCountryModel::rowCount(const QModelIndex &parent) const
|
|||||||
|
|
||||||
QVariant ApiCountryModel::data(const QModelIndex &index, int role) 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();
|
return QVariant();
|
||||||
|
}
|
||||||
|
|
||||||
CountryInfo countryInfo = m_countries.at(index.row());
|
const CountryInfo &countryInfo = m_countries.at(index.row());
|
||||||
IssuedConfigInfo issuedConfigInfo = m_issuedConfigs.value(countryInfo.countryCode);
|
const IssuedConfigInfo issuedConfigInfo = m_issuedConfigs.value(countryInfo.countryCode);
|
||||||
bool isIssued = issuedConfigInfo.sourceType == countryConfig;
|
const bool isIssued = issuedConfigInfo.sourceType == countryConfig;
|
||||||
|
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case CountryCodeRole: {
|
case CountryCodeRole:
|
||||||
return countryInfo.countryCode;
|
return countryInfo.countryCode;
|
||||||
}
|
case CountryNameRole:
|
||||||
case CountryNameRole: {
|
|
||||||
return countryInfo.countryName;
|
return countryInfo.countryName;
|
||||||
}
|
case CountryImageCodeRole:
|
||||||
case CountryImageCodeRole: {
|
|
||||||
return countryInfo.countryCode.toUpper();
|
return countryInfo.countryCode.toUpper();
|
||||||
}
|
case IsIssuedRole:
|
||||||
case IsIssuedRole: {
|
|
||||||
return isIssued;
|
return isIssued;
|
||||||
}
|
case IsWorkerExpiredRole:
|
||||||
case IsWorkerExpiredRole: {
|
|
||||||
return issuedConfigInfo.lastDownloaded < issuedConfigInfo.workerLastUpdated;
|
return issuedConfigInfo.lastDownloaded < issuedConfigInfo.workerLastUpdated;
|
||||||
|
default:
|
||||||
|
return QVariant();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return QVariant();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ApiCountryModel::updateModel(const QJsonArray &countries, const QString ¤tCountryCode)
|
void ApiCountryModel::updateModel(const QJsonArray &countries, const QString ¤tCountryCode)
|
||||||
@@ -57,9 +192,9 @@ void ApiCountryModel::updateModel(const QJsonArray &countries, const QString &cu
|
|||||||
beginResetModel();
|
beginResetModel();
|
||||||
|
|
||||||
m_countries.clear();
|
m_countries.clear();
|
||||||
for (int i = 0; i < countries.size(); i++) {
|
for (int i = 0; i < countries.size(); ++i) {
|
||||||
CountryInfo countryInfo;
|
CountryInfo countryInfo;
|
||||||
QJsonObject countryObject = countries.at(i).toObject();
|
const QJsonObject countryObject = countries.at(i).toObject();
|
||||||
|
|
||||||
countryInfo.countryName = countryObject.value(apiDefs::key::serverCountryName).toString();
|
countryInfo.countryName = countryObject.value(apiDefs::key::serverCountryName).toString();
|
||||||
countryInfo.countryCode = countryObject.value(apiDefs::key::serverCountryCode).toString();
|
countryInfo.countryCode = countryObject.value(apiDefs::key::serverCountryCode).toString();
|
||||||
@@ -72,6 +207,7 @@ void ApiCountryModel::updateModel(const QJsonArray &countries, const QString &cu
|
|||||||
}
|
}
|
||||||
|
|
||||||
endResetModel();
|
endResetModel();
|
||||||
|
rebuildGroupedRegions();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ApiCountryModel::updateIssuedConfigsInfo(const QJsonArray &issuedConfigs)
|
void ApiCountryModel::updateIssuedConfigsInfo(const QJsonArray &issuedConfigs)
|
||||||
@@ -79,9 +215,9 @@ void ApiCountryModel::updateIssuedConfigsInfo(const QJsonArray &issuedConfigs)
|
|||||||
beginResetModel();
|
beginResetModel();
|
||||||
|
|
||||||
m_issuedConfigs.clear();
|
m_issuedConfigs.clear();
|
||||||
for (int i = 0; i < issuedConfigs.size(); i++) {
|
for (int i = 0; i < issuedConfigs.size(); ++i) {
|
||||||
IssuedConfigInfo issuedConfigInfo;
|
IssuedConfigInfo issuedConfigInfo;
|
||||||
QJsonObject issuedConfigObject = issuedConfigs.at(i).toObject();
|
const QJsonObject issuedConfigObject = issuedConfigs.at(i).toObject();
|
||||||
|
|
||||||
if (issuedConfigObject.value(apiDefs::key::sourceType).toString() != countryConfig) {
|
if (issuedConfigObject.value(apiDefs::key::sourceType).toString() != countryConfig) {
|
||||||
continue;
|
continue;
|
||||||
@@ -110,6 +246,52 @@ void ApiCountryModel::setCurrentIndex(const int i)
|
|||||||
emit currentIndexChanged(m_currentIndex);
|
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> ApiCountryModel::roleNames() const
|
||||||
{
|
{
|
||||||
QHash<int, QByteArray> roles;
|
QHash<int, QByteArray> roles;
|
||||||
@@ -120,3 +302,192 @@ QHash<int, QByteArray> ApiCountryModel::roleNames() const
|
|||||||
roles[IsWorkerExpiredRole] = "isWorkerExpired";
|
roles[IsWorkerExpiredRole] = "isWorkerExpired";
|
||||||
return roles;
|
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);
|
||||||
|
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 ®ionCountryCode,
|
||||||
|
const QString &sourceCountryCode, const QString &ruCountryName,
|
||||||
|
const QString &normalizedSearchText) const
|
||||||
|
{
|
||||||
|
if (normalizedSearchText.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString normalizedCountryName = normalizeSearchComparableText(countryName);
|
||||||
|
const QString normalizedRuCountryName = normalizeSearchComparableText(ruCountryName);
|
||||||
|
const QString normalizedRegionCountryCode = normalizeCountryCode(regionCountryCode).toLower();
|
||||||
|
const QString normalizedSourceCountryCode = normalizeCountryCode(sourceCountryCode).toLower();
|
||||||
|
|
||||||
|
return normalizedCountryName.startsWith(normalizedSearchText) || normalizedRuCountryName.startsWith(normalizedSearchText) ||
|
||||||
|
normalizedRegionCountryCode.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ApiCountryModel::findCountryIndexByRef(const CountryRef &countryRef, const QHash<int, bool> &usedIndices) const
|
||||||
|
{
|
||||||
|
const QString expectedCode = normalizeCountryCode(countryRef.code);
|
||||||
|
const QString expectedName = normalizeCountryName(countryRef.name);
|
||||||
|
const int countriesCount = m_countries.size();
|
||||||
|
|
||||||
|
for (int i = 0; i < countriesCount; ++i) {
|
||||||
|
if (usedIndices.value(i)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountryInfo &country = m_countries.at(i);
|
||||||
|
const QString modelCode = normalizeCountryCode(country.countryCode);
|
||||||
|
const QString modelIsoCode = extractCountryIsoCode(country.countryCode);
|
||||||
|
const QString modelName = normalizeCountryName(country.countryName);
|
||||||
|
|
||||||
|
if (!expectedName.isEmpty() && modelName == expectedName) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
if (!expectedCode.isEmpty() && (modelCode == expectedCode || modelIsoCode == expectedCode)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApiCountryModel::rebuildGroupedRegions()
|
||||||
|
{
|
||||||
|
QVector<RegionRowData> rows;
|
||||||
|
QHash<int, bool> usedIndices;
|
||||||
|
const QString normalizedSearchText = normalizeSearchComparableText(m_searchText);
|
||||||
|
|
||||||
|
for (int regionIndex = 0; regionIndex < m_regionDefinitions.size(); ++regionIndex) {
|
||||||
|
const RegionDefinition ®ionDefinition = m_regionDefinitions.at(regionIndex);
|
||||||
|
QVector<RegionRowData> countries;
|
||||||
|
|
||||||
|
const auto &countryRefs = std::as_const(regionDefinition.countries);
|
||||||
|
for (const CountryRef &countryRef : countryRefs) {
|
||||||
|
const int sourceIndex = findCountryIndexByRef(countryRef, usedIndices);
|
||||||
|
if (sourceIndex < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountryInfo &sourceCountry = m_countries.at(sourceIndex);
|
||||||
|
const QString displayCountryName = getDisplayCountryName(sourceCountry.countryName);
|
||||||
|
if (!isCountryMatchingSearch(displayCountryName, countryRef.code, sourceCountry.countryCode, countryRef.ruName,
|
||||||
|
normalizedSearchText)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
RegionRowData countryRow;
|
||||||
|
countryRow.isRegionHeader = false;
|
||||||
|
countryRow.regionName = regionDefinition.regionName;
|
||||||
|
countryRow.sourceIndex = sourceIndex;
|
||||||
|
countryRow.countryName = displayCountryName;
|
||||||
|
countryRow.sourceCountryName = sourceCountry.countryName;
|
||||||
|
countryRow.countryCode = sourceCountry.countryCode;
|
||||||
|
countryRow.countryImageCode = extractCountryIsoCode(sourceCountry.countryCode);
|
||||||
|
countries.push_back(std::move(countryRow));
|
||||||
|
usedIndices.insert(sourceIndex, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!countries.isEmpty()) {
|
||||||
|
const bool expanded = isRegionExpanded(regionDefinition.regionName);
|
||||||
|
|
||||||
|
RegionRowData headerRow;
|
||||||
|
headerRow.isRegionHeader = true;
|
||||||
|
headerRow.regionName = regionDefinition.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 <QAbstractListModel>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
class ApiCountryModel : public QAbstractListModel
|
class ApiCountryModel : public QAbstractListModel
|
||||||
{
|
{
|
||||||
@@ -19,12 +20,16 @@ public:
|
|||||||
};
|
};
|
||||||
|
|
||||||
explicit ApiCountryModel(QObject *parent = nullptr);
|
explicit ApiCountryModel(QObject *parent = nullptr);
|
||||||
|
~ApiCountryModel() override;
|
||||||
|
|
||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
|
|
||||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) 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(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:
|
public slots:
|
||||||
void updateModel(const QJsonArray &countries, const QString ¤tCountryCode);
|
void updateModel(const QJsonArray &countries, const QString ¤tCountryCode);
|
||||||
@@ -32,9 +37,17 @@ public slots:
|
|||||||
|
|
||||||
int getCurrentIndex();
|
int getCurrentIndex();
|
||||||
void setCurrentIndex(const int i);
|
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:
|
signals:
|
||||||
void currentIndexChanged(const int index);
|
void currentIndexChanged(const int index);
|
||||||
|
void searchTextChanged();
|
||||||
|
void regionRowsChanged();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QHash<int, QByteArray> roleNames() const override;
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
@@ -55,9 +68,40 @@ private:
|
|||||||
QString countryCode;
|
QString countryCode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct CountryRef
|
||||||
|
{
|
||||||
|
QString code;
|
||||||
|
QString name;
|
||||||
|
QString ruName;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RegionDefinition
|
||||||
|
{
|
||||||
|
QString regionName;
|
||||||
|
QVector<CountryRef> countries;
|
||||||
|
};
|
||||||
|
|
||||||
QVector<CountryInfo> m_countries;
|
QVector<CountryInfo> m_countries;
|
||||||
QHash<QString, IssuedConfigInfo> m_issuedConfigs;
|
QHash<QString, IssuedConfigInfo> m_issuedConfigs;
|
||||||
int m_currentIndex;
|
int m_currentIndex = -1;
|
||||||
|
QString m_searchText;
|
||||||
|
QVector<RegionDefinition> m_regionDefinitions;
|
||||||
|
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 ®ionCountryCode, const QString &sourceCountryCode,
|
||||||
|
const QString &ruCountryName, const QString &normalizedSearchText) const;
|
||||||
|
QString getDisplayCountryName(const QString &countryName) const;
|
||||||
|
int findCountryIndexByRef(const CountryRef &countryRef, const QHash<int, bool> &usedIndices) const;
|
||||||
|
void rebuildGroupedRegions();
|
||||||
|
void loadRegionExpansionState();
|
||||||
|
void saveRegionExpansionState() const;
|
||||||
|
bool isSearchActive() const;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // APICOUNTRYMODEL_H
|
#endif // APICOUNTRYMODEL_H
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ PageType {
|
|||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
model: ApiCountriesRegionModel
|
model: ApiCountryModel.regionRowsModel
|
||||||
|
|
||||||
currentIndex: 0
|
currentIndex: 0
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ PageType {
|
|||||||
const shouldRestoreFocus = activeFocus
|
const shouldRestoreFocus = activeFocus
|
||||||
const previousCursorPosition = cursorPosition
|
const previousCursorPosition = cursorPosition
|
||||||
|
|
||||||
ApiCountriesRegionModel.searchText = text
|
ApiCountryModel.searchText = text
|
||||||
|
|
||||||
if (shouldRestoreFocus) {
|
if (shouldRestoreFocus) {
|
||||||
Qt.callLater(function() {
|
Qt.callLater(function() {
|
||||||
@@ -197,7 +197,7 @@ PageType {
|
|||||||
|
|
||||||
footer: Item {
|
footer: Item {
|
||||||
width: menuContent.width
|
width: menuContent.width
|
||||||
height: ApiCountriesRegionModel.count === 0 ? emptyStateText.implicitHeight + 32 : 0
|
height: ApiCountryModel.hasVisibleRegions ? 0 : emptyStateText.implicitHeight + 32
|
||||||
|
|
||||||
CaptionTextType {
|
CaptionTextType {
|
||||||
id: emptyStateText
|
id: emptyStateText
|
||||||
@@ -209,7 +209,7 @@ PageType {
|
|||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.topMargin: 16
|
anchors.topMargin: 16
|
||||||
|
|
||||||
visible: ApiCountriesRegionModel.count === 0
|
visible: !ApiCountryModel.hasVisibleRegions
|
||||||
color: AmneziaStyle.color.mutedGray
|
color: AmneziaStyle.color.mutedGray
|
||||||
|
|
||||||
font.pixelSize: 15
|
font.pixelSize: 15
|
||||||
@@ -219,39 +219,33 @@ PageType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delegate: ColumnLayout {
|
delegate: Item {
|
||||||
id: regionContent
|
|
||||||
|
|
||||||
width: menuContent.width
|
width: menuContent.width
|
||||||
height: regionContent.implicitHeight
|
implicitHeight: rowType === "region" ? 44 : 88
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
Layout.fillWidth: true
|
anchors.fill: parent
|
||||||
Layout.leftMargin: 16
|
visible: rowType === "region"
|
||||||
Layout.rightMargin: 16
|
|
||||||
Layout.topMargin: 12
|
|
||||||
Layout.bottomMargin: 8
|
|
||||||
|
|
||||||
implicitHeight: 24
|
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 16
|
||||||
|
anchors.rightMargin: 16
|
||||||
|
anchors.topMargin: 12
|
||||||
|
anchors.bottomMargin: 8
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
||||||
CaptionTextType {
|
CaptionTextType {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
color: AmneziaStyle.color.mutedGray
|
color: AmneziaStyle.color.mutedGray
|
||||||
|
|
||||||
text: regionName
|
text: regionName
|
||||||
horizontalAlignment: Text.AlignLeft
|
horizontalAlignment: Text.AlignLeft
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
source: ApiCountriesRegionModel.isRegionExpanded(regionName)
|
source: isExpanded ? "qrc:/images/controls/chevron-up.svg"
|
||||||
? "qrc:/images/controls/chevron-up.svg"
|
: "qrc:/images/controls/chevron-down.svg"
|
||||||
: "qrc:/images/controls/chevron-down.svg"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,85 +253,75 @@ PageType {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
ApiCountriesRegionModel.toggleRegionExpanded(regionName)
|
ApiCountryModel.toggleRegionExpanded(regionName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Repeater {
|
ColumnLayout {
|
||||||
model: ApiCountriesRegionModel.isRegionExpanded(regionName) ? countries : []
|
anchors.fill: parent
|
||||||
|
visible: rowType === "country"
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
delegate: ColumnLayout {
|
RowLayout {
|
||||||
property var countryData: modelData
|
Layout.fillWidth: true
|
||||||
|
|
||||||
width: menuContent.width
|
VerticalRadioButton {
|
||||||
spacing: 0
|
id: containerRadioButton
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
VerticalRadioButton {
|
|
||||||
id: containerRadioButton
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.leftMargin: 16
|
|
||||||
|
|
||||||
text: countryData.countryName
|
|
||||||
|
|
||||||
ButtonGroup.group: containersRadioButtonGroup
|
|
||||||
|
|
||||||
imageSource: "qrc:/images/controls/download.svg"
|
|
||||||
|
|
||||||
checked: countryData.sourceIndex >= 0 && countryData.sourceIndex === ApiCountryModel.currentIndex
|
|
||||||
checkable: countryData.isAvailable && !ConnectionController.isConnected
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
if (!countryData.isAvailable) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
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 (countryData.sourceIndex !== ApiCountryModel.currentIndex) {
|
|
||||||
PageController.showBusyIndicator(true)
|
|
||||||
var prevIndex = ApiCountryModel.currentIndex
|
|
||||||
ApiCountryModel.currentIndex = countryData.sourceIndex
|
|
||||||
if (!ApiConfigsController.updateServiceFromGateway(ServersModel.defaultIndex, countryData.countryCode, countryData.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/" + countryData.countryImageCode + ".svg"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DividerType {
|
|
||||||
Layout.fillWidth: true
|
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