From d0a9f6e4d59014f8f1b2f9e983868362b291f364 Mon Sep 17 00:00:00 2001 From: dranik Date: Wed, 13 May 2026 13:17:37 +0300 Subject: [PATCH] add ui Configuration Files --- client/translations/amneziavpn_ru_RU.ts | 10 +++++ client/ui/models/api/apiAccountInfoModel.cpp | 15 ++++++++ client/ui/models/api/apiAccountInfoModel.h | 4 +- .../ui/qml/Pages2/PageSettingsApiDevices.qml | 20 ++++++++++ tools/local_gateway/main.go | 37 ++++++++++++++++--- 5 files changed, 79 insertions(+), 7 deletions(-) diff --git a/client/translations/amneziavpn_ru_RU.ts b/client/translations/amneziavpn_ru_RU.ts index e813b40f1..cfdcc5113 100644 --- a/client/translations/amneziavpn_ru_RU.ts +++ b/client/translations/amneziavpn_ru_RU.ts @@ -1807,6 +1807,16 @@ Thank you for staying with us! Cancel Отменить + + + Configuration Files: %1 + Файлы конфигурации: %1 + + + + Generated configuration files also count towards the device limit + Сгенерированные файлы конфигурации тоже учитываются в лимите устройств + PageSettingsApiInstructions diff --git a/client/ui/models/api/apiAccountInfoModel.cpp b/client/ui/models/api/apiAccountInfoModel.cpp index 2f4691b83..d5abc6ca8 100644 --- a/client/ui/models/api/apiAccountInfoModel.cpp +++ b/client/ui/models/api/apiAccountInfoModel.cpp @@ -11,6 +11,8 @@ namespace { Logger logger("AccountInfoModel"); + + constexpr QLatin1String kCountryConfigSourceType("country_config"); } ApiAccountInfoModel::ApiAccountInfoModel(QObject *parent) : QAbstractListModel(parent) @@ -121,6 +123,9 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const const int spare = m_accountInfoData.maxDeviceCount - m_accountInfoData.activeDeviceCount; return qMax(0, spare); } + case ConfigurationFilesCountRole: { + return m_accountInfoData.configurationFilesCount; + } } return QVariant(); @@ -135,6 +140,15 @@ void ApiAccountInfoModel::updateModel(const QJsonObject &accountInfoObject, cons m_availableCountries = accountInfoObject.value(apiDefs::key::availableCountries).toArray(); m_issuedConfigsInfo = accountInfoObject.value(apiDefs::key::issuedConfigs).toArray(); + int configurationFilesCount = 0; + for (int i = 0; i < m_issuedConfigsInfo.size(); ++i) { + const QJsonObject issued = m_issuedConfigsInfo.at(i).toObject(); + if (issued.value(apiDefs::key::sourceType).toString() == kCountryConfigSourceType) { + ++configurationFilesCount; + } + } + accountInfoData.configurationFilesCount = configurationFilesCount; + accountInfoData.activeDeviceCount = accountInfoObject.value(apiDefs::key::activeDeviceCount).toInt(); accountInfoData.maxDeviceCount = accountInfoObject.value(apiDefs::key::maxDeviceCount).toInt(); accountInfoData.subscriptionEndDate = accountInfoObject.value(apiDefs::key::subscriptionEndDate).toString(); @@ -223,6 +237,7 @@ QHash ApiAccountInfoModel::roleNames() const roles[ActiveDeviceCountRole] = "activeDeviceCount"; roles[MaxDeviceCountRole] = "maxDeviceCount"; roles[AvailableDeviceSlotsRole] = "availableDeviceSlots"; + roles[ConfigurationFilesCountRole] = "configurationFilesCount"; return roles; } diff --git a/client/ui/models/api/apiAccountInfoModel.h b/client/ui/models/api/apiAccountInfoModel.h index cd8dfdb6d..479e8aabf 100644 --- a/client/ui/models/api/apiAccountInfoModel.h +++ b/client/ui/models/api/apiAccountInfoModel.h @@ -28,7 +28,8 @@ public: IsInAppPurchaseRole, ActiveDeviceCountRole, MaxDeviceCountRole, - AvailableDeviceSlotsRole + AvailableDeviceSlotsRole, + ConfigurationFilesCountRole }; explicit ApiAccountInfoModel(QObject *parent = nullptr); @@ -67,6 +68,7 @@ private: bool isInAppPurchase = false; bool isRenewalAvailable = false; + int configurationFilesCount = 0; }; AccountInfoData m_accountInfoData; diff --git a/client/ui/qml/Pages2/PageSettingsApiDevices.qml b/client/ui/qml/Pages2/PageSettingsApiDevices.qml index e799fa043..d0ce5a22c 100644 --- a/client/ui/qml/Pages2/PageSettingsApiDevices.qml +++ b/client/ui/qml/Pages2/PageSettingsApiDevices.qml @@ -241,6 +241,26 @@ PageType { DividerType {} } + + footer: ColumnLayout { + width: listView.width + + LabelWithButtonType { + Layout.fillWidth: true + Layout.topMargin: 8 + + text: qsTr("Configuration Files: %1").arg(ApiAccountInfoModel.data("configurationFilesCount")) + descriptionText: qsTr("Generated configuration files also count towards the device limit") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + SubscriptionUiController.updateApiCountryModel() + PageController.goToPage(PageEnum.PageSettingsApiNativeConfigs) + } + } + + DividerType {} + } } function deactivateExternalDevice(serverIndex, supportTag, countryCode) { diff --git a/tools/local_gateway/main.go b/tools/local_gateway/main.go index c437c8aed..4606f9b7e 100644 --- a/tools/local_gateway/main.go +++ b/tools/local_gateway/main.go @@ -521,26 +521,51 @@ func handleAccountInfo(w http.ResponseWriter, r *http.Request) { drainBody(r) mu.Lock() - issuedConfigs := make([]issuedConfigInfo, 0, len(issued)) + gatewayConfigs := make([]issuedConfigInfo, 0, len(issued)) for _, cfg := range issued { - issuedConfigs = append(issuedConfigs, cfg) + gatewayConfigs = append(gatewayConfigs, cfg) } mu.Unlock() - sort.Slice(issuedConfigs, func(i, j int) bool { - return issuedConfigs[i].InstallationUUID < issuedConfigs[j].InstallationUUID + sort.Slice(gatewayConfigs, func(i, j int) bool { + return gatewayConfigs[i].InstallationUUID < gatewayConfigs[j].InstallationUUID }) + // Seed country_config rows so the client can verify "Configuration Files: N" (ApiAccountInfoModel counts these). + // active_device_count must reflect gateway devices only, not these synthetic file rows. + nowISO := time.Now().UTC().Format(time.RFC3339) + mockCountryConfigs := []issuedConfigInfo{ + { + InstallationUUID: "mock-country-config-de", + WorkerLastUpdated: nowISO, + LastDownloaded: nowISO, + SourceType: "country_config", + OSVersion: "", + ServerCountryCode: "de", + ServerCountryName: "Germany", + }, + { + InstallationUUID: "mock-country-config-nl", + WorkerLastUpdated: nowISO, + LastDownloaded: nowISO, + SourceType: "country_config", + OSVersion: "", + ServerCountryCode: "nl", + ServerCountryName: "Netherlands", + }, + } + allIssued := append(append([]issuedConfigInfo{}, gatewayConfigs...), mockCountryConfigs...) + // Keys match client/core/utils/constants/apiKeys.h (snake_case). endDate := time.Now().UTC().AddDate(1, 0, 0).Format(time.RFC3339) resp := map[string]any{ - "active_device_count": len(issuedConfigs), + "active_device_count": len(gatewayConfigs), "max_device_count": 5, "subscription_end_date": endDate, "subscription_description": "Local mock (tools/local_gateway)", "is_renewal_available": false, "supported_protocols": []string{"awg", "vless"}, "available_countries": []any{}, - "issued_configs": issuedConfigs, + "issued_configs": allIssued, "support_info": map[string]any{ "telegram": "amnezia_support", "email": "support@example.com",