From a231bf9ab7d1dab1634fdd49a667b36be35f2ade Mon Sep 17 00:00:00 2001 From: vkamn Date: Thu, 26 Mar 2026 17:16:41 +0800 Subject: [PATCH] refactor: move plan and benefits into separate models --- client/core/controllers/coreController.cpp | 9 +- client/core/controllers/coreController.h | 4 + .../controllers/api/apiConfigsController.cpp | 15 +- .../ui/controllers/api/apiConfigsController.h | 17 +- client/ui/models/api/apiBenefitsModel.cpp | 166 ++++++++++++++++++ client/ui/models/api/apiBenefitsModel.h | 49 ++++++ client/ui/models/api/apiServicesModel.cpp | 156 +++------------- client/ui/models/api/apiServicesModel.h | 105 ++++++----- .../models/api/apiSubscriptionPlansModel.cpp | 135 ++++++++++++++ .../ui/models/api/apiSubscriptionPlansModel.h | 51 ++++++ client/ui/qml/Components/BenefitsPanel.qml | 16 +- client/ui/qml/Modules/Style/AmneziaStyle.qml | 1 + .../qml/Pages2/PageSetupWizardApiFreeInfo.qml | 22 +-- .../Pages2/PageSetupWizardApiPremiumInfo.qml | 55 ++---- 14 files changed, 548 insertions(+), 253 deletions(-) create mode 100644 client/ui/models/api/apiBenefitsModel.cpp create mode 100644 client/ui/models/api/apiBenefitsModel.h create mode 100644 client/ui/models/api/apiSubscriptionPlansModel.cpp create mode 100644 client/ui/models/api/apiSubscriptionPlansModel.h diff --git a/client/core/controllers/coreController.cpp b/client/core/controllers/coreController.cpp index dde6ffb29..42a8e2037 100644 --- a/client/core/controllers/coreController.cpp +++ b/client/core/controllers/coreController.cpp @@ -91,6 +91,12 @@ void CoreController::initModels() m_apiServicesModel.reset(new ApiServicesModel(this)); m_engine->rootContext()->setContextProperty("ApiServicesModel", m_apiServicesModel.get()); + m_apiSubscriptionPlansModel.reset(new ApiSubscriptionPlansModel(this)); + m_engine->rootContext()->setContextProperty("ApiSubscriptionPlansModel", m_apiSubscriptionPlansModel.get()); + + m_apiBenefitsModel.reset(new ApiBenefitsModel(this)); + m_engine->rootContext()->setContextProperty("ApiBenefitsModel", m_apiBenefitsModel.get()); + m_apiCountryModel.reset(new ApiCountryModel(this)); m_engine->rootContext()->setContextProperty("ApiCountryModel", m_apiCountryModel.get()); @@ -151,7 +157,8 @@ void CoreController::initControllers() new ApiSettingsController(m_serversModel, m_apiAccountInfoModel, m_apiCountryModel, m_apiDevicesModel, m_settings)); m_engine->rootContext()->setContextProperty("ApiSettingsController", m_apiSettingsController.get()); - m_apiConfigsController.reset(new ApiConfigsController(m_serversModel, m_apiServicesModel, m_settings)); + m_apiConfigsController.reset( + new ApiConfigsController(m_serversModel, m_apiServicesModel, m_apiSubscriptionPlansModel, m_apiBenefitsModel, m_settings)); m_engine->rootContext()->setContextProperty("ApiConfigsController", m_apiConfigsController.get()); connect(m_apiConfigsController.get(), &ApiConfigsController::subscriptionRefreshNeeded, this, [this]() { m_apiSettingsController->getAccountInfo(false); }); diff --git a/client/core/controllers/coreController.h b/client/core/controllers/coreController.h index 998e7d8d0..fd2e88cad 100644 --- a/client/core/controllers/coreController.h +++ b/client/core/controllers/coreController.h @@ -32,9 +32,11 @@ #include "ui/models/protocols/ikev2ConfigModel.h" #endif #include "ui/models/api/apiAccountInfoModel.h" +#include "ui/models/api/apiBenefitsModel.h" #include "ui/models/api/apiCountryModel.h" #include "ui/models/api/apiDevicesModel.h" #include "ui/models/api/apiServicesModel.h" +#include "ui/models/api/apiSubscriptionPlansModel.h" #include "ui/models/appSplitTunnelingModel.h" #include "ui/models/clientManagementModel.h" #include "ui/models/protocols/awgConfigModel.h" @@ -133,6 +135,8 @@ private: QSharedPointer m_clientManagementModel; QSharedPointer m_apiServicesModel; + QSharedPointer m_apiSubscriptionPlansModel; + QSharedPointer m_apiBenefitsModel; QSharedPointer m_apiCountryModel; QSharedPointer m_apiAccountInfoModel; QSharedPointer m_apiDevicesModel; diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index d24d23d66..ce1839783 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -242,9 +242,22 @@ namespace ApiConfigsController::ApiConfigsController(const QSharedPointer &serversModel, const QSharedPointer &apiServicesModel, + const QSharedPointer &subscriptionPlansModel, + const QSharedPointer &benefitsModel, const std::shared_ptr &settings, QObject *parent) - : QObject(parent), m_serversModel(serversModel), m_apiServicesModel(apiServicesModel), m_settings(settings) + : QObject(parent) + , m_serversModel(serversModel) + , m_apiServicesModel(apiServicesModel) + , m_subscriptionPlansModel(subscriptionPlansModel) + , m_benefitsModel(benefitsModel) + , m_settings(settings) { + connect(m_apiServicesModel.data(), &ApiServicesModel::serviceSelectionChanged, this, [this]() { + const ApiServicesModel::ApiServicesData serviceData = m_apiServicesModel->selectedServiceData(); + m_subscriptionPlansModel->updateModel(serviceData.subscriptionPlansJson); + m_benefitsModel->updateModel(serviceData.benefits, serviceData.serviceInfo.region, serviceData.serviceInfo.speed, + serviceData.serviceInfo.price, serviceData.supportInfo); + }); } bool ApiConfigsController::exportVpnKey(const QString &fileName) diff --git a/client/ui/controllers/api/apiConfigsController.h b/client/ui/controllers/api/apiConfigsController.h index 94824c085..8841d337f 100644 --- a/client/ui/controllers/api/apiConfigsController.h +++ b/client/ui/controllers/api/apiConfigsController.h @@ -4,7 +4,9 @@ #include #include "configurators/openvpn_configurator.h" +#include "ui/models/api/apiBenefitsModel.h" #include "ui/models/api/apiServicesModel.h" +#include "ui/models/api/apiSubscriptionPlansModel.h" #include "ui/models/servers_model.h" class ApiConfigsController : public QObject @@ -12,7 +14,12 @@ class ApiConfigsController : public QObject Q_OBJECT public: ApiConfigsController(const QSharedPointer &serversModel, const QSharedPointer &apiServicesModel, - const std::shared_ptr &settings, QObject *parent = nullptr); + const QSharedPointer &subscriptionPlansModel, + const QSharedPointer &benefitsModel, const std::shared_ptr &settings, + QObject *parent = nullptr); + + Q_PROPERTY(ApiSubscriptionPlansModel *subscriptionPlansModel READ subscriptionPlansModel CONSTANT) + Q_PROPERTY(ApiBenefitsModel *benefitsModel READ benefitsModel CONSTANT) Q_PROPERTY(QList qrCodes READ getQrCodes NOTIFY vpnKeyExportReady) Q_PROPERTY(int qrCodesCount READ getQrCodesCount NOTIFY vpnKeyExportReady) @@ -38,6 +45,9 @@ public slots: bool isConfigValid(); + ApiSubscriptionPlansModel *subscriptionPlansModel() const { return m_subscriptionPlansModel.get(); } + ApiBenefitsModel *benefitsModel() const { return m_benefitsModel.get(); } + void setCurrentProtocol(const QString &protocolName); bool isVlessProtocol(); @@ -67,6 +77,9 @@ private: QSharedPointer m_serversModel; QSharedPointer m_apiServicesModel; std::shared_ptr m_settings; + + QSharedPointer m_subscriptionPlansModel; + QSharedPointer m_benefitsModel; }; -#endif // APICONFIGSCONTROLLER_H +#endif diff --git a/client/ui/models/api/apiBenefitsModel.cpp b/client/ui/models/api/apiBenefitsModel.cpp new file mode 100644 index 000000000..a2bd73757 --- /dev/null +++ b/client/ui/models/api/apiBenefitsModel.cpp @@ -0,0 +1,166 @@ +#include "apiBenefitsModel.h" + +#include +#include +#include +#include + +#include "core/api/apiDefs.h" + +namespace +{ +namespace configKey +{ + constexpr char title[] = "title"; + constexpr char body[] = "body"; + constexpr char icon[] = "icon"; + constexpr char injectKey[] = "inject_key"; + constexpr char accent[] = "accent"; + + constexpr char region[] = "region"; + constexpr char speed[] = "speed"; + constexpr char price[] = "price"; +} + +QString gatewayIconKeyToUrl(const QString &iconKey) +{ + if (iconKey.startsWith(QLatin1String("qrc:"))) { + return iconKey; + } + static const QHash map = { + { QStringLiteral("globe-2"), QStringLiteral("qrc:/images/controls/globe-2.svg") }, + { QStringLiteral("smartphone"), QStringLiteral("qrc:/images/controls/smartphone.svg") }, + { QStringLiteral("gauge"), QStringLiteral("qrc:/images/controls/gauge.svg") }, + { QStringLiteral("infinity"), QStringLiteral("qrc:/images/controls/infinity.svg") }, + { QStringLiteral("tag"), QStringLiteral("qrc:/images/controls/tag.svg") }, + { QStringLiteral("history"), QStringLiteral("qrc:/images/controls/history.svg") }, + { QStringLiteral("info"), QStringLiteral("qrc:/images/controls/info.svg") }, + { QStringLiteral("app"), QStringLiteral("qrc:/images/controls/app.svg") }, + { QStringLiteral("download"), QStringLiteral("qrc:/images/controls/download.svg") }, + { QStringLiteral("help-circle"), QStringLiteral("qrc:/images/controls/help-circle.svg") }, + }; + return map.value(iconKey, QStringLiteral("qrc:/images/controls/info.svg")); +} +} + +ApiBenefitsModel::ApiBenefitsModel(QObject *parent) + : QAbstractListModel(parent) +{ +} + +int ApiBenefitsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_serviceBenefits.size(); +} + +QVariant ApiBenefitsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_serviceBenefits.size()) { + return {}; + } + const ServiceBenefitItem &item = m_serviceBenefits.at(index.row()); + switch (role) { + case IconRole: + return item.icon; + case TitleRole: + return item.title; + case BodyRole: + return item.body; + case AccentRole: + return item.accent; + default: + return {}; + } +} + +QHash ApiBenefitsModel::roleNames() const +{ + return { + { IconRole, "icon" }, + { TitleRole, "title" }, + { BodyRole, "body" }, + { AccentRole, "accent" }, + }; +} + +void ApiBenefitsModel::updateModel(const QJsonArray &benefits, const QString ®ion, const QString &speed, + const QString &price, const QJsonObject &supportInfo) +{ + beginResetModel(); + m_serviceBenefits.clear(); + for (const QJsonValue &benefitValue : benefits) { + if (!benefitValue.isObject()) { + continue; + } + const QJsonObject benefitObject = benefitValue.toObject(); + QString title = benefitObject.value(configKey::title).toString(); + QString body = benefitObject.value(configKey::body).toString(); + const QString iconKey = benefitObject.value(configKey::icon).toString(); + const QString injectKey = benefitObject.value(configKey::injectKey).toString(); + if (body.contains(QLatin1String("%1")) && !injectKey.isEmpty()) { + QString injected = benefitInjectValue(injectKey, region, speed, price, supportInfo); + if (injected.isEmpty()) { + injected = QStringLiteral("—"); + } + body = body.arg(injected); + } + if (title.isEmpty() && body.isEmpty()) { + continue; + } + ServiceBenefitItem item; + item.icon = gatewayIconKeyToUrl(iconKey); + item.title = std::move(title); + item.body = std::move(body); + item.accent = benefitObject.value(configKey::accent).toBool(); + m_serviceBenefits.append(std::move(item)); + } + endResetModel(); +} + +void ApiBenefitsModel::clear() +{ + beginResetModel(); + m_serviceBenefits.clear(); + endResetModel(); +} + +QString ApiBenefitsModel::formatPriceForBenefit(const QString &rawPrice) const +{ + if (rawPrice == QStringLiteral("free")) { + return tr("Free"); + } +#if defined(Q_OS_IOS) || defined(MACOS_NE) + return tr("%1 $").arg(rawPrice); +#else + return tr("%1 $/month").arg(rawPrice); +#endif +} + +QString ApiBenefitsModel::benefitInjectValue(const QString &injectKey, const QString ®ion, const QString &speed, + const QString &price, const QJsonObject &supportInfo) const +{ + if (injectKey == QLatin1String(configKey::region)) { + return region.isEmpty() ? QStringLiteral("—") : region; + } + if (injectKey == QLatin1String(configKey::speed)) { + return speed.isEmpty() ? QStringLiteral("—") : speed; + } + if (injectKey == QLatin1String(configKey::price)) { + return formatPriceForBenefit(price); + } + + if (injectKey == apiDefs::key::telegram) { + const QString handle = supportInfo.value(apiDefs::key::telegram).toString().trimmed(); + if (handle.isEmpty()) { + return QStringLiteral("—"); + } + if (handle.startsWith(QLatin1Char('@'))) { + return handle; + } + return QLatin1Char('@') + handle; + } + return QString(); +} diff --git a/client/ui/models/api/apiBenefitsModel.h b/client/ui/models/api/apiBenefitsModel.h new file mode 100644 index 000000000..d6347e383 --- /dev/null +++ b/client/ui/models/api/apiBenefitsModel.h @@ -0,0 +1,49 @@ +#ifndef APIBENEFITSMODEL_H +#define APIBENEFITSMODEL_H + +#include +#include +#include +#include +#include + +class ApiBenefitsModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles { + IconRole = Qt::UserRole + 1, + TitleRole, + BodyRole, + AccentRole + }; + Q_ENUM(Roles) + + explicit ApiBenefitsModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + void updateModel(const QJsonArray &benefits, const QString ®ion, const QString &speed, const QString &price, + const QJsonObject &supportInfo); + void clear(); + +private: + struct ServiceBenefitItem + { + QString icon; + QString title; + QString body; + bool accent = false; + }; + + QVector m_serviceBenefits; + + QString formatPriceForBenefit(const QString &rawPrice) const; + QString benefitInjectValue(const QString &injectKey, const QString ®ion, const QString &speed, const QString &price, + const QJsonObject &supportInfo) const; +}; + +#endif diff --git a/client/ui/models/api/apiServicesModel.cpp b/client/ui/models/api/apiServicesModel.cpp index 53edcf412..deaef9b40 100644 --- a/client/ui/models/api/apiServicesModel.cpp +++ b/client/ui/models/api/apiServicesModel.cpp @@ -41,39 +41,6 @@ namespace constexpr char benefits[] = "benefits"; } - QString iconUrlFromGatewayBenefitIcon(const QString &iconKey) - { - if (iconKey.startsWith(QLatin1String("qrc:"))) { - return iconKey; - } - static const QHash map = { - { QStringLiteral("globe-2"), QStringLiteral("qrc:/images/controls/globe-2.svg") }, - { QStringLiteral("smartphone"), QStringLiteral("qrc:/images/controls/smartphone.svg") }, - { QStringLiteral("gauge"), QStringLiteral("qrc:/images/controls/gauge.svg") }, - { QStringLiteral("infinity"), QStringLiteral("qrc:/images/controls/infinity.svg") }, - { QStringLiteral("tag"), QStringLiteral("qrc:/images/controls/tag.svg") }, - { QStringLiteral("history"), QStringLiteral("qrc:/images/controls/history.svg") }, - { QStringLiteral("info"), QStringLiteral("qrc:/images/controls/info.svg") }, - { QStringLiteral("app"), QStringLiteral("qrc:/images/controls/app.svg") }, - { QStringLiteral("download"), QStringLiteral("qrc:/images/controls/download.svg") }, - { QStringLiteral("help-circle"), QStringLiteral("qrc:/images/controls/help-circle.svg") }, - }; - return map.value(iconKey, QStringLiteral("qrc:/images/controls/info.svg")); - } - - QVariantList jsonObjectArrayToVariantList(const QJsonArray &arr) - { - QVariantList list; - list.reserve(arr.size()); - for (const QJsonValue &v : arr) { - if (!v.isObject()) { - continue; - } - list.append(v.toObject().toVariantMap()); - } - return list; - } - namespace serviceType { constexpr char amneziaFree[] = "amnezia-free"; @@ -82,7 +49,9 @@ namespace } } -ApiServicesModel::ApiServicesModel(QObject *parent) : QAbstractListModel(parent) +ApiServicesModel::ApiServicesModel(QObject *parent) + : QAbstractListModel(parent) + , m_selectedServiceIndex(0) { } @@ -171,12 +140,6 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const } return QVariant(); } - case SubscriptionPlansRole: { - return apiServiceData.subscriptionPlans; - } - case BenefitRowsRole: { - return buildBenefitRows(apiServiceData); - } } return QVariant(); @@ -201,12 +164,27 @@ void ApiServicesModel::updateModel(const QJsonObject &data) } } + if (!m_services.isEmpty() && m_selectedServiceIndex >= m_services.size()) { + m_selectedServiceIndex = 0; + } + endResetModel(); + + emit serviceSelectionChanged(); } void ApiServicesModel::setServiceIndex(const int index) { m_selectedServiceIndex = index; + emit serviceSelectionChanged(); +} + +ApiServicesModel::ApiServicesData ApiServicesModel::selectedServiceData() const +{ + if (m_services.isEmpty() || m_selectedServiceIndex < 0 || m_selectedServiceIndex >= m_services.size()) { + return {}; + } + return m_services.at(m_selectedServiceIndex); } QJsonObject ApiServicesModel::getSelectedServiceInfo() @@ -265,30 +243,14 @@ QVariant ApiServicesModel::getSelectedServiceData(const QString roleString) int ApiServicesModel::serviceIndexForType(const QString &type) const { - for (int i = 0; i < m_services.size(); ++i) { - if (m_services.at(i).type == type) { - return i; + for (int serviceIndex = 0; serviceIndex < m_services.size(); ++serviceIndex) { + if (m_services.at(serviceIndex).type == type) { + return serviceIndex; } } return -1; } -QVariant ApiServicesModel::getServiceFieldForType(const QString &type, const QString &roleString) const -{ - const int row = serviceIndexForType(type); - if (row < 0) { - return {}; - } - const QModelIndex modelIndex = index(row); - const auto roles = roleNames(); - for (auto it = roles.begin(); it != roles.end(); ++it) { - if (QString(it.value()) == roleString) { - return data(modelIndex, it.key()); - } - } - return {}; -} - QHash ApiServicesModel::roleNames() const { QHash roles; @@ -303,8 +265,6 @@ QHash ApiServicesModel::roleNames() const roles[PriceRole] = "price"; roles[EndDateRole] = "endDate"; roles[OrderRole] = "order"; - roles[SubscriptionPlansRole] = "subscriptionPlans"; - roles[BenefitRowsRole] = "benefitRows"; return roles; } @@ -330,7 +290,7 @@ ApiServicesModel::ApiServicesData ApiServicesModel::getApiServicesData(const QJs serviceData.serviceInfo.description = serviceDescription.value(configKey::description).toString(); serviceData.serviceInfo.features = serviceDescription.value(configKey::features).toString(); - serviceData.subscriptionPlans = jsonObjectArrayToVariantList(serviceDescription.value(configKey::subscriptionPlans).toArray()); + serviceData.subscriptionPlansJson = serviceDescription.value(configKey::subscriptionPlans).toArray(); serviceData.benefits = serviceDescription.value(configKey::benefits).toArray(); serviceData.supportInfo = data.value(apiDefs::key::supportInfo).toObject(); @@ -353,75 +313,3 @@ ApiServicesModel::ApiServicesData ApiServicesModel::getApiServicesData(const QJs return serviceData; } - -QString ApiServicesModel::formatPriceForBenefit(const QString &rawPrice) const -{ - if (rawPrice == QStringLiteral("free")) { - return tr("Free"); - } -#if defined(Q_OS_IOS) || defined(MACOS_NE) - return tr("%1 $").arg(rawPrice); -#else - return tr("%1 $/month").arg(rawPrice); -#endif -} - -QString ApiServicesModel::benefitInjectValue(const QString &injectKey, const ServiceInfo &info, - const QJsonObject &supportInfo) const -{ - if (injectKey == QLatin1String("region")) { - return info.region.isEmpty() ? QStringLiteral("—") : info.region; - } - if (injectKey == QLatin1String("speed")) { - return info.speed.isEmpty() ? QStringLiteral("—") : info.speed; - } - if (injectKey == QLatin1String("price")) { - return formatPriceForBenefit(info.price); - } - - if (injectKey == apiDefs::key::telegram) { - const QString handle = supportInfo.value(apiDefs::key::telegram).toString().trimmed(); - if (handle.isEmpty()) { - return QStringLiteral("—"); - } - if (handle.startsWith(QLatin1Char('@'))) { - return handle; - } - return QLatin1Char('@') + handle; - } - return QString(); -} - -QVariantList ApiServicesModel::buildBenefitRows(const ApiServicesData &service) const -{ - QVariantList out; - for (const QJsonValue &v : service.benefits) { - if (!v.isObject()) { - continue; - } - const QJsonObject o = v.toObject(); - QString title = o.value(QStringLiteral("title")).toString(); - QString body = o.value(QStringLiteral("body")).toString(); - const QString iconKey = o.value(QStringLiteral("icon")).toString(); - const QString injectKey = o.value(QStringLiteral("inject_key")).toString(); - if (body.contains(QLatin1String("%1")) && !injectKey.isEmpty()) { - QString injected = benefitInjectValue(injectKey, service.serviceInfo, service.supportInfo); - if (injected.isEmpty()) { - injected = QStringLiteral("—"); - } - body = body.arg(injected); - } - if (title.isEmpty() && body.isEmpty()) { - continue; - } - QVariantMap m; - m.insert(QStringLiteral("icon"), iconUrlFromGatewayBenefitIcon(iconKey)); - m.insert(QStringLiteral("title"), title); - m.insert(QStringLiteral("body"), body); - if (o.value(QStringLiteral("accent")).toBool()) { - m.insert(QStringLiteral("accent"), true); - } - out.append(m); - } - return out; -} diff --git a/client/ui/models/api/apiServicesModel.h b/client/ui/models/api/apiServicesModel.h index d1f57c0ec..7d61fbf14 100644 --- a/client/ui/models/api/apiServicesModel.h +++ b/client/ui/models/api/apiServicesModel.h @@ -4,59 +4,13 @@ #include #include #include -#include +#include class ApiServicesModel : public QAbstractListModel { Q_OBJECT public: - enum Roles { - NameRole = Qt::UserRole + 1, - CardDescriptionRole, - ServiceDescriptionRole, - IsServiceAvailableRole, - SpeedRole, - TimeLimitRole, - RegionRole, - FeaturesRole, - PriceRole, - EndDateRole, - OrderRole, - SubscriptionPlansRole, - BenefitRowsRole - }; - - explicit ApiServicesModel(QObject *parent = nullptr); - - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - -public slots: - void updateModel(const QJsonObject &data); - - void setServiceIndex(const int index); - - QJsonObject getSelectedServiceInfo(); - QString getSelectedServiceType(); - QString getSelectedServiceProtocol(); - QString getSelectedServiceName(); - QJsonArray getSelectedServiceCountries(); - - QString getCountryCode(); - - QString getStoreEndpoint(); - - QVariant getSelectedServiceData(const QString roleString); - - Q_INVOKABLE int serviceIndexForType(const QString &type) const; - Q_INVOKABLE QVariant getServiceFieldForType(const QString &type, const QString &roleString) const; - -protected: - QHash roleNames() const override; - -private: struct ServiceInfo { QString name; @@ -91,16 +45,59 @@ private: QJsonArray availableCountries; - QVariantList subscriptionPlans; + QJsonArray subscriptionPlansJson; QJsonArray benefits; }; - ApiServicesData getApiServicesData(const QJsonObject &data); + enum Roles { + NameRole = Qt::UserRole + 1, + CardDescriptionRole, + ServiceDescriptionRole, + IsServiceAvailableRole, + SpeedRole, + TimeLimitRole, + RegionRole, + FeaturesRole, + PriceRole, + EndDateRole, + OrderRole + }; - QVariantList buildBenefitRows(const ApiServicesData &service) const; - QString benefitInjectValue(const QString &injectKey, const ServiceInfo &info, - const QJsonObject &supportInfo) const; - QString formatPriceForBenefit(const QString &rawPrice) const; + explicit ApiServicesModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + ApiServicesData selectedServiceData() const; + +public slots: + void updateModel(const QJsonObject &data); + + void setServiceIndex(const int index); + + QJsonObject getSelectedServiceInfo(); + QString getSelectedServiceType(); + QString getSelectedServiceProtocol(); + QString getSelectedServiceName(); + QJsonArray getSelectedServiceCountries(); + + QString getCountryCode(); + + QString getStoreEndpoint(); + + QVariant getSelectedServiceData(const QString roleString); + + Q_INVOKABLE int serviceIndexForType(const QString &type) const; + +signals: + void serviceSelectionChanged(); + +protected: + QHash roleNames() const override; + +private: + ApiServicesData getApiServicesData(const QJsonObject &data); QString m_countryCode; QVector m_services; @@ -108,4 +105,4 @@ private: int m_selectedServiceIndex; }; -#endif // APISERVICESMODEL_H +#endif diff --git a/client/ui/models/api/apiSubscriptionPlansModel.cpp b/client/ui/models/api/apiSubscriptionPlansModel.cpp new file mode 100644 index 000000000..89af34dee --- /dev/null +++ b/client/ui/models/api/apiSubscriptionPlansModel.cpp @@ -0,0 +1,135 @@ +#include "apiSubscriptionPlansModel.h" + +#include +#include +#include +#include + +namespace +{ +namespace configKey +{ + constexpr char primaryLeft[] = "primary_left"; + constexpr char primaryRight[] = "primary_right"; + constexpr char subtitle[] = "subtitle"; + constexpr char recommended[] = "recommended"; + constexpr char checkoutUrl[] = "checkout_url"; + constexpr char serviceType[] = "service_type"; + constexpr char serviceProtocol[] = "service_protocol"; + + constexpr char primaryLeftCamel[] = "primaryLeft"; + constexpr char primaryRightCamel[] = "primaryRight"; + constexpr char checkoutUrlCamel[] = "checkoutUrl"; + constexpr char serviceTypeCamel[] = "serviceType"; + constexpr char serviceProtocolCamel[] = "serviceProtocol"; +} +} + +ApiSubscriptionPlansModel::ApiSubscriptionPlansModel(QObject *parent) + : QAbstractListModel(parent) +{ +} + +int ApiSubscriptionPlansModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_subscriptionPlans.size(); +} + +QVariant ApiSubscriptionPlansModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_subscriptionPlans.size()) { + return {}; + } + const SubscriptionPlanItem &plan = m_subscriptionPlans.at(index.row()); + switch (role) { + case PrimaryLeftRole: + return plan.primaryLeft; + case PrimaryRightRole: + return plan.primaryRight; + case SubtitleRole: + return plan.subtitle; + case RecommendedRole: + return plan.recommended; + case CheckoutUrlRole: + return plan.checkoutUrl; + case ServiceTypeRole: + return plan.serviceType; + case ServiceProtocolRole: + return plan.serviceProtocol; + default: + return {}; + } +} + +QHash ApiSubscriptionPlansModel::roleNames() const +{ + return { + { PrimaryLeftRole, "primaryLeft" }, + { PrimaryRightRole, "primaryRight" }, + { SubtitleRole, "subtitle" }, + { RecommendedRole, "recommended" }, + { CheckoutUrlRole, "checkoutUrl" }, + { ServiceTypeRole, "serviceType" }, + { ServiceProtocolRole, "serviceProtocol" }, + }; +} + +void ApiSubscriptionPlansModel::updateModel(const QJsonArray &arr) +{ + beginResetModel(); + m_subscriptionPlans.clear(); + m_subscriptionPlans.reserve(arr.size()); + for (const QJsonValue &planValue : arr) { + if (!planValue.isObject()) { + continue; + } + const QJsonObject planObject = planValue.toObject(); + SubscriptionPlanItem subscriptionPlan; + subscriptionPlan.primaryLeft = planObject.value(configKey::primaryLeft).toString(); + subscriptionPlan.primaryRight = planObject.value(configKey::primaryRight).toString(); + subscriptionPlan.subtitle = planObject.value(configKey::subtitle).toString(); + subscriptionPlan.recommended = planObject.value(configKey::recommended).toBool(); + subscriptionPlan.checkoutUrl = planObject.value(configKey::checkoutUrl).toString(); + subscriptionPlan.serviceType = planObject.value(configKey::serviceType).toString(); + subscriptionPlan.serviceProtocol = planObject.value(configKey::serviceProtocol).toString(); + m_subscriptionPlans.append(std::move(subscriptionPlan)); + } + endResetModel(); +} + +void ApiSubscriptionPlansModel::clear() +{ + beginResetModel(); + m_subscriptionPlans.clear(); + endResetModel(); +} + +QVariantMap ApiSubscriptionPlansModel::planAt(int row) const +{ + if (row < 0 || row >= m_subscriptionPlans.size()) { + return {}; + } + const SubscriptionPlanItem &plan = m_subscriptionPlans.at(row); + QVariantMap planMap; + planMap.insert(QLatin1String(configKey::primaryLeftCamel), plan.primaryLeft); + planMap.insert(QLatin1String(configKey::primaryRightCamel), plan.primaryRight); + planMap.insert(QLatin1String(configKey::subtitle), plan.subtitle); + planMap.insert(QLatin1String(configKey::recommended), plan.recommended); + planMap.insert(QLatin1String(configKey::checkoutUrlCamel), plan.checkoutUrl); + planMap.insert(QLatin1String(configKey::serviceTypeCamel), plan.serviceType); + planMap.insert(QLatin1String(configKey::serviceProtocolCamel), plan.serviceProtocol); + return planMap; +} + +int ApiSubscriptionPlansModel::recommendedRowIndex() const +{ + for (int planIndex = 0; planIndex < m_subscriptionPlans.size(); ++planIndex) { + if (m_subscriptionPlans.at(planIndex).recommended) { + return planIndex; + } + } + return 0; +} diff --git a/client/ui/models/api/apiSubscriptionPlansModel.h b/client/ui/models/api/apiSubscriptionPlansModel.h new file mode 100644 index 000000000..3ba31d137 --- /dev/null +++ b/client/ui/models/api/apiSubscriptionPlansModel.h @@ -0,0 +1,51 @@ +#ifndef APISUBSCRIPTIONPLANSMODEL_H +#define APISUBSCRIPTIONPLANSMODEL_H + +#include +#include +#include + +class ApiSubscriptionPlansModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles { + PrimaryLeftRole = Qt::UserRole + 1, + PrimaryRightRole, + SubtitleRole, + RecommendedRole, + CheckoutUrlRole, + ServiceTypeRole, + ServiceProtocolRole + }; + Q_ENUM(Roles) + + explicit ApiSubscriptionPlansModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + void updateModel(const QJsonArray &arr); + void clear(); + + Q_INVOKABLE QVariantMap planAt(int row) const; + Q_INVOKABLE int recommendedRowIndex() const; + +private: + struct SubscriptionPlanItem + { + QString primaryLeft; + QString primaryRight; + QString subtitle; + bool recommended = false; + QString checkoutUrl; + QString serviceType; + QString serviceProtocol; + }; + + QVector m_subscriptionPlans; +}; + +#endif diff --git a/client/ui/qml/Components/BenefitsPanel.qml b/client/ui/qml/Components/BenefitsPanel.qml index 86ed1fd52..bb1d3a23e 100644 --- a/client/ui/qml/Components/BenefitsPanel.qml +++ b/client/ui/qml/Components/BenefitsPanel.qml @@ -8,12 +8,12 @@ import Style 1.0 Rectangle { id: root - property var benefitItems: [] + property var benefitsModel: null - visible: benefitItems && benefitItems.length > 0 + visible: benefitsModel && benefitsModel.rowCount() > 0 radius: 16 - color: "#1C1C1E" + color: AmneziaStyle.color.benefitsPanelBackground implicitHeight: inner.implicitHeight + 24 ColumnLayout { @@ -26,14 +26,14 @@ Rectangle { spacing: 20 Repeater { - model: root.benefitItems ? root.benefitItems.length : 0 + model: benefitsModel delegate: BenefitRow { Layout.fillWidth: true - iconSource: root.benefitItems[index].icon - titleText: root.benefitItems[index].title - bodyText: root.benefitItems[index].body - accent: !!root.benefitItems[index].accent + iconSource: model.icon + titleText: model.title + bodyText: model.body + accent: !!model.accent } } } diff --git a/client/ui/qml/Modules/Style/AmneziaStyle.qml b/client/ui/qml/Modules/Style/AmneziaStyle.qml index 12c204291..d6b3f9081 100644 --- a/client/ui/qml/Modules/Style/AmneziaStyle.qml +++ b/client/ui/qml/Modules/Style/AmneziaStyle.qml @@ -13,6 +13,7 @@ QtObject { readonly property color onyxBlack: '#1C1D21' readonly property color midnightBlack: '#0E0E11' readonly property color goldenApricot: '#FBB26A' + readonly property color benefitsPanelBackground: '#1C1C1E' readonly property color softViolet: '#A87BE2' readonly property color burntOrange: '#A85809' readonly property color mutedBrown: '#84603D' diff --git a/client/ui/qml/Pages2/PageSetupWizardApiFreeInfo.qml b/client/ui/qml/Pages2/PageSetupWizardApiFreeInfo.qml index 2728de3f2..7759fc347 100644 --- a/client/ui/qml/Pages2/PageSetupWizardApiFreeInfo.qml +++ b/client/ui/qml/Pages2/PageSetupWizardApiFreeInfo.qml @@ -16,28 +16,16 @@ PageType { property string freeHeaderName: "" property string freeHeaderDescription: "" property string freeFeaturesHtml: "" - property var benefitRows: [] function syncFromModel() { root.freeHeaderName = String(ApiServicesModel.getSelectedServiceData("name")) root.freeHeaderDescription = String(ApiServicesModel.getSelectedServiceData("serviceDescription")) var text = ApiServicesModel.getSelectedServiceData("features") root.freeFeaturesHtml = String(text).replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") - - var rows = ApiServicesModel.getSelectedServiceData("benefitRows") - root.benefitRows = rows !== undefined && rows !== null ? rows : [] } Component.onCompleted: syncFromModel() - Connections { - target: ApiServicesModel - - function onModelReset() { - root.syncFromModel() - } - } - BackButtonType { id: backButton @@ -97,7 +85,7 @@ PageType { Layout.rightMargin: 16 Layout.bottomMargin: 24 - benefitItems: root.benefitRows + benefitsModel: ApiConfigsController.benefitsModel } ParagraphTextType { @@ -106,7 +94,7 @@ PageType { Layout.rightMargin: 16 Layout.bottomMargin: 16 - visible: root.freeFeaturesHtml.length > 0 && (!root.benefitRows || root.benefitRows.length === 0) + visible: root.freeFeaturesHtml.length > 0 && ApiConfigsController.benefitsModel.rowCount() === 0 textFormat: Text.RichText text: root.freeFeaturesHtml @@ -138,7 +126,8 @@ PageType { text: { var termsUrl = LanguageModel.getCurrentSiteUrl() var privacyUrl = LanguageModel.getCurrentSiteUrl("policy") - return qsTr("By continuing, you agree to the Terms of Use and Privacy Policy").arg(termsUrl).arg(privacyUrl) + return qsTr("By continuing, you agree to the Terms of Use and Privacy Policy") + .arg(termsUrl).arg(privacyUrl).arg(Qt.colorToString(AmneziaStyle.color.goldenApricot)) } onLinkActivated: function(link) { @@ -168,7 +157,8 @@ PageType { text: { var termsUrl = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/" var privacyUrl = LanguageModel.getCurrentSiteUrl("policy") - return qsTr("By continuing, you agree to the Terms of Use and Privacy Policy").arg(termsUrl).arg(privacyUrl) + return qsTr("By continuing, you agree to the Terms of Use and Privacy Policy") + .arg(termsUrl).arg(privacyUrl).arg(Qt.colorToString(AmneziaStyle.color.goldenApricot)) } onLinkActivated: function(link) { diff --git a/client/ui/qml/Pages2/PageSetupWizardApiPremiumInfo.qml b/client/ui/qml/Pages2/PageSetupWizardApiPremiumInfo.qml index 15f22fb79..a2767bdd7 100644 --- a/client/ui/qml/Pages2/PageSetupWizardApiPremiumInfo.qml +++ b/client/ui/qml/Pages2/PageSetupWizardApiPremiumInfo.qml @@ -13,26 +13,15 @@ import "../Components" PageType { id: root - property var subscriptionPlans: [] - property var benefitRows: [] property int selectedPlanIndex: 0 property string premiumFeaturesHtml: "" property string premiumHeaderName: "" property string premiumHeaderDescription: "" - readonly property var currentPlan: subscriptionPlans[selectedPlanIndex] + readonly property var currentPlan: ApiConfigsController.subscriptionPlansModel.planAt(selectedPlanIndex) function syncFromModel() { - root.subscriptionPlans = ApiServicesModel.getSelectedServiceData("subscriptionPlans") - root.benefitRows = ApiServicesModel.getSelectedServiceData("benefitRows") - - root.selectedPlanIndex = 0 - for (var i = 0; i < root.subscriptionPlans.length; ++i) { - if (root.subscriptionPlans[i].recommended) { - root.selectedPlanIndex = i - break - } - } + root.selectedPlanIndex = ApiConfigsController.subscriptionPlansModel.recommendedRowIndex() root.premiumFeaturesHtml = String(ApiServicesModel.getSelectedServiceData("features")).replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") root.premiumHeaderName = String(ApiServicesModel.getSelectedServiceData("name")) @@ -41,14 +30,6 @@ PageType { Component.onCompleted: syncFromModel() - Connections { - target: ApiServicesModel - - function onModelReset() { - root.syncFromModel() - } - } - BackButtonType { id: backButton @@ -103,23 +84,21 @@ PageType { } Repeater { - model: subscriptionPlans.length + model: ApiConfigsController.subscriptionPlansModel delegate: SubscriptionPlanCard { required property int index - readonly property var plan: root.subscriptionPlans[index] - Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 - Layout.bottomMargin: index === root.subscriptionPlans.length - 1 ? 24 : 12 + Layout.bottomMargin: index === ApiConfigsController.subscriptionPlansModel.rowCount() - 1 ? 24 : 12 selected: root.selectedPlanIndex === index - primaryLeft: String(plan.primary_left) - primaryRight: String(plan.primary_right) - subtitle: String(plan.subtitle) - showRecommendedBadge: !!plan.recommended + primaryLeft: String(model.primaryLeft) + primaryRight: String(model.primaryRight) + subtitle: String(model.subtitle) + showRecommendedBadge: !!model.recommended recommendedText: qsTr("Recommended") onSelectRequested: root.selectedPlanIndex = index @@ -162,7 +141,7 @@ PageType { Layout.rightMargin: 16 Layout.bottomMargin: 24 - benefitItems: root.benefitRows + benefitsModel: ApiConfigsController.benefitsModel } ParagraphTextType { @@ -197,7 +176,8 @@ PageType { text: { var termsUrl = LanguageModel.getCurrentSiteUrl() var privacyUrl = LanguageModel.getCurrentSiteUrl("policy") - return qsTr("By continuing, you agree to the Terms of Use and Privacy Policy").arg(termsUrl).arg(privacyUrl) + return qsTr("By continuing, you agree to the Terms of Use and Privacy Policy") + .arg(termsUrl).arg(privacyUrl).arg(Qt.colorToString(AmneziaStyle.color.goldenApricot)) } onLinkActivated: function(link) { @@ -227,7 +207,8 @@ PageType { text: { var termsUrl = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/" var privacyUrl = LanguageModel.getCurrentSiteUrl("policy") - return qsTr("By continuing, you agree to the Terms of Use and Privacy Policy").arg(termsUrl).arg(privacyUrl) + return qsTr("By continuing, you agree to the Terms of Use and Privacy Policy") + .arg(termsUrl).arg(privacyUrl).arg(Qt.colorToString(AmneziaStyle.color.goldenApricot)) } onLinkActivated: function(link) { @@ -259,7 +240,7 @@ PageType { if (!plan) { return qsTr("Continue") } - return qsTr("Subscribe — %1 for %2").arg(String(plan.primary_left)).arg(String(plan.primary_right)) + return qsTr("Subscribe — %1 for %2").arg(String(plan.primaryLeft)).arg(String(plan.primaryRight)) } clickedFunc: function() { @@ -267,14 +248,14 @@ PageType { if (!plan) { return } - if (plan.checkout_url) { - Qt.openUrlExternally(plan.checkout_url) + if (plan.checkoutUrl) { + Qt.openUrlExternally(plan.checkoutUrl) PageController.closePage() PageController.closePage() return } - if (plan.service_type) { - var idx = ApiServicesModel.serviceIndexForType(plan.service_type) + if (plan.serviceType) { + var idx = ApiServicesModel.serviceIndexForType(plan.serviceType) if (idx < 0) { return }