refactor: move plan and benefits into separate models

This commit is contained in:
vkamn
2026-03-26 17:16:41 +08:00
parent c29984ce60
commit a231bf9ab7
14 changed files with 548 additions and 253 deletions

View File

@@ -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); });

View File

@@ -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<ClientManagementModel> m_clientManagementModel;
QSharedPointer<ApiServicesModel> m_apiServicesModel;
QSharedPointer<ApiSubscriptionPlansModel> m_apiSubscriptionPlansModel;
QSharedPointer<ApiBenefitsModel> m_apiBenefitsModel;
QSharedPointer<ApiCountryModel> m_apiCountryModel;
QSharedPointer<ApiAccountInfoModel> m_apiAccountInfoModel;
QSharedPointer<ApiDevicesModel> m_apiDevicesModel;

View File

@@ -242,9 +242,22 @@ namespace
ApiConfigsController::ApiConfigsController(const QSharedPointer<ServersModel> &serversModel,
const QSharedPointer<ApiServicesModel> &apiServicesModel,
const QSharedPointer<ApiSubscriptionPlansModel> &subscriptionPlansModel,
const QSharedPointer<ApiBenefitsModel> &benefitsModel,
const std::shared_ptr<Settings> &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)

View File

@@ -4,7 +4,9 @@
#include <QObject>
#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> &serversModel, const QSharedPointer<ApiServicesModel> &apiServicesModel,
const std::shared_ptr<Settings> &settings, QObject *parent = nullptr);
const QSharedPointer<ApiSubscriptionPlansModel> &subscriptionPlansModel,
const QSharedPointer<ApiBenefitsModel> &benefitsModel, const std::shared_ptr<Settings> &settings,
QObject *parent = nullptr);
Q_PROPERTY(ApiSubscriptionPlansModel *subscriptionPlansModel READ subscriptionPlansModel CONSTANT)
Q_PROPERTY(ApiBenefitsModel *benefitsModel READ benefitsModel CONSTANT)
Q_PROPERTY(QList<QString> 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<ServersModel> m_serversModel;
QSharedPointer<ApiServicesModel> m_apiServicesModel;
std::shared_ptr<Settings> m_settings;
QSharedPointer<ApiSubscriptionPlansModel> m_subscriptionPlansModel;
QSharedPointer<ApiBenefitsModel> m_benefitsModel;
};
#endif // APICONFIGSCONTROLLER_H
#endif

View File

@@ -0,0 +1,166 @@
#include "apiBenefitsModel.h"
#include <QHash>
#include <utility>
#include <QJsonObject>
#include <QJsonValue>
#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<QString, QString> 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<int, QByteArray> ApiBenefitsModel::roleNames() const
{
return {
{ IconRole, "icon" },
{ TitleRole, "title" },
{ BodyRole, "body" },
{ AccentRole, "accent" },
};
}
void ApiBenefitsModel::updateModel(const QJsonArray &benefits, const QString &region, 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 &region, 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();
}

View File

@@ -0,0 +1,49 @@
#ifndef APIBENEFITSMODEL_H
#define APIBENEFITSMODEL_H
#include <QAbstractListModel>
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <QVector>
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<int, QByteArray> roleNames() const override;
void updateModel(const QJsonArray &benefits, const QString &region, const QString &speed, const QString &price,
const QJsonObject &supportInfo);
void clear();
private:
struct ServiceBenefitItem
{
QString icon;
QString title;
QString body;
bool accent = false;
};
QVector<ServiceBenefitItem> m_serviceBenefits;
QString formatPriceForBenefit(const QString &rawPrice) const;
QString benefitInjectValue(const QString &injectKey, const QString &region, const QString &speed, const QString &price,
const QJsonObject &supportInfo) const;
};
#endif

View File

@@ -41,39 +41,6 @@ namespace
constexpr char benefits[] = "benefits";
}
QString iconUrlFromGatewayBenefitIcon(const QString &iconKey)
{
if (iconKey.startsWith(QLatin1String("qrc:"))) {
return iconKey;
}
static const QHash<QString, QString> 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<int, QByteArray> ApiServicesModel::roleNames() const
{
QHash<int, QByteArray> roles;
@@ -303,8 +265,6 @@ QHash<int, QByteArray> 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;
}

View File

@@ -4,59 +4,13 @@
#include <QAbstractListModel>
#include <QJsonArray>
#include <QJsonObject>
#include <QVariantList>
#include <QVector>
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<int, QByteArray> 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<int, QByteArray> roleNames() const override;
private:
ApiServicesData getApiServicesData(const QJsonObject &data);
QString m_countryCode;
QVector<ApiServicesData> m_services;
@@ -108,4 +105,4 @@ private:
int m_selectedServiceIndex;
};
#endif // APISERVICESMODEL_H
#endif

View File

@@ -0,0 +1,135 @@
#include "apiSubscriptionPlansModel.h"
#include <QJsonObject>
#include <QJsonValue>
#include <QVariantMap>
#include <utility>
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<int, QByteArray> 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;
}

View File

@@ -0,0 +1,51 @@
#ifndef APISUBSCRIPTIONPLANSMODEL_H
#define APISUBSCRIPTIONPLANSMODEL_H
#include <QAbstractListModel>
#include <QJsonArray>
#include <QVector>
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<int, QByteArray> 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<SubscriptionPlanItem> m_subscriptionPlans;
};
#endif

View File

@@ -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
}
}
}

View File

@@ -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'

View File

@@ -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 <a href=\"%1\" style=\"color: #FBB26A;\">Terms of Use</a> and <a href=\"%2\" style=\"color: #FBB26A;\">Privacy Policy</a>").arg(termsUrl).arg(privacyUrl)
return qsTr("By continuing, you agree to the <a href=\"%1\" style=\"color: %3;\">Terms of Use</a> and <a href=\"%2\" style=\"color: %3;\">Privacy Policy</a>")
.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 <a href=\"%1\" style=\"color: #FBB26A;\">Terms of Use</a> and <a href=\"%2\" style=\"color: #FBB26A;\">Privacy Policy</a>").arg(termsUrl).arg(privacyUrl)
return qsTr("By continuing, you agree to the <a href=\"%1\" style=\"color: %3;\">Terms of Use</a> and <a href=\"%2\" style=\"color: %3;\">Privacy Policy</a>")
.arg(termsUrl).arg(privacyUrl).arg(Qt.colorToString(AmneziaStyle.color.goldenApricot))
}
onLinkActivated: function(link) {

View File

@@ -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 <a href=\"%1\" style=\"color: #FBB26A;\">Terms of Use</a> and <a href=\"%2\" style=\"color: #FBB26A;\">Privacy Policy</a>").arg(termsUrl).arg(privacyUrl)
return qsTr("By continuing, you agree to the <a href=\"%1\" style=\"color: %3;\">Terms of Use</a> and <a href=\"%2\" style=\"color: %3;\">Privacy Policy</a>")
.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 <a href=\"%1\" style=\"color: #FBB26A;\">Terms of Use</a> and <a href=\"%2\" style=\"color: #FBB26A;\">Privacy Policy</a>").arg(termsUrl).arg(privacyUrl)
return qsTr("By continuing, you agree to the <a href=\"%1\" style=\"color: %3;\">Terms of Use</a> and <a href=\"%2\" style=\"color: %3;\">Privacy Policy</a>")
.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
}