(rowCount()))
return QVariant();
- QJsonObject service = m_services.at(index.row()).toObject();
- QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject();
- auto serviceType = service.value(configKey::serviceType).toString();
+ auto apiServiceData = m_services.at(index.row());
+ auto serviceType = apiServiceData.type;
+ auto isServiceAvailable = apiServiceData.isServiceAvailable;
switch (role) {
case NameRole: {
- return serviceInfo.value(configKey::name).toString();
+ return apiServiceData.serviceInfo.name;
}
case CardDescriptionRole: {
- auto speed = serviceInfo.value(configKey::speed).toString();
+ auto speed = apiServiceData.serviceInfo.speed;
if (serviceType == serviceType::amneziaPremium) {
return tr("Classic VPN for comfortable work, downloading large files and watching videos. "
"Works for any sites. Speed up to %1 MBit/s")
.arg(speed);
} else if (serviceType == serviceType::amneziaFree){
QString description = tr("VPN to access blocked sites in regions with high levels of Internet censorship. ");
- if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) {
+ if (isServiceAvailable) {
description += tr("Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again.");
}
return description;
@@ -83,25 +86,24 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
}
case IsServiceAvailableRole: {
if (serviceType == serviceType::amneziaFree) {
- if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) {
+ if (isServiceAvailable) {
return false;
}
}
return true;
}
case SpeedRole: {
- auto speed = serviceInfo.value(configKey::speed).toString();
- return tr("%1 MBit/s").arg(speed);
+ return tr("%1 MBit/s").arg(apiServiceData.serviceInfo.speed);
}
- case WorkPeriodRole: {
- auto timelimit = serviceInfo.value(configKey::timelimit).toString();
- if (timelimit == "0") {
+ case TimeLimitRole: {
+ auto timeLimit = apiServiceData.serviceInfo.timeLimit;
+ if (timeLimit == "0") {
return "";
}
- return tr("%1 days").arg(timelimit);
+ return tr("%1 days").arg(timeLimit);
}
case RegionRole: {
- return serviceInfo.value(configKey::region).toString();
+ return apiServiceData.serviceInfo.region;
}
case FeaturesRole: {
if (serviceType == serviceType::amneziaPremium) {
@@ -113,12 +115,15 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
}
}
case PriceRole: {
- auto price = serviceInfo.value(configKey::price).toString();
+ auto price = apiServiceData.serviceInfo.price;
if (price == "free") {
return tr("Free");
}
return tr("%1 $/month").arg(price);
}
+ case EndDateRole: {
+ return QDateTime::fromString(apiServiceData.subscription.endDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy");
+ }
}
return QVariant();
@@ -128,15 +133,18 @@ void ApiServicesModel::updateModel(const QJsonObject &data)
{
beginResetModel();
- m_countryCode = data.value(configKey::userCountryCode).toString();
- m_services = data.value(configKey::services).toArray();
- if (m_services.isEmpty()) {
- QJsonObject service;
- service.insert(configKey::serviceInfo, data.value(configKey::serviceInfo));
- service.insert(configKey::serviceType, data.value(configKey::serviceType));
+ m_services.clear();
- m_services.push_back(service);
+ m_countryCode = data.value(configKey::userCountryCode).toString();
+ auto services = data.value(configKey::services).toArray();
+
+ if (services.isEmpty()) {
+ m_services.push_back(getApiServicesData(data));
m_selectedServiceIndex = 0;
+ } else {
+ for (const auto &service : services) {
+ m_services.push_back(getApiServicesData(service.toObject()));
+ }
}
endResetModel();
@@ -149,32 +157,32 @@ void ApiServicesModel::setServiceIndex(const int index)
QJsonObject ApiServicesModel::getSelectedServiceInfo()
{
- QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
- return service.value(configKey::serviceInfo).toObject();
+ auto service = m_services.at(m_selectedServiceIndex);
+ return service.serviceInfo.object;
}
QString ApiServicesModel::getSelectedServiceType()
{
- QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
- return service.value(configKey::serviceType).toString();
+ auto service = m_services.at(m_selectedServiceIndex);
+ return service.type;
}
QString ApiServicesModel::getSelectedServiceProtocol()
{
- QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
- return service.value(configKey::serviceProtocol).toString();
+ auto service = m_services.at(m_selectedServiceIndex);
+ return service.protocol;
}
QString ApiServicesModel::getSelectedServiceName()
{
- auto modelIndex = index(m_selectedServiceIndex, 0);
- return data(modelIndex, ApiServicesModel::Roles::NameRole).toString();
+ auto service = m_services.at(m_selectedServiceIndex);
+ return service.serviceInfo.name;
}
QJsonArray ApiServicesModel::getSelectedServiceCountries()
{
- QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
- return service.value(configKey::availableCountries).toArray();
+ auto service = m_services.at(m_selectedServiceIndex);
+ return service.availableCountries;
}
QString ApiServicesModel::getCountryCode()
@@ -184,8 +192,8 @@ QString ApiServicesModel::getCountryCode()
QString ApiServicesModel::getStoreEndpoint()
{
- QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
- return service.value(configKey::storeEndpoint).toString();
+ auto service = m_services.at(m_selectedServiceIndex);
+ return service.storeEndpoint;
}
QVariant ApiServicesModel::getSelectedServiceData(const QString roleString)
@@ -209,10 +217,46 @@ QHash ApiServicesModel::roleNames() const
roles[ServiceDescriptionRole] = "serviceDescription";
roles[IsServiceAvailableRole] = "isServiceAvailable";
roles[SpeedRole] = "speed";
- roles[WorkPeriodRole] = "workPeriod";
+ roles[TimeLimitRole] = "timeLimit";
roles[RegionRole] = "region";
roles[FeaturesRole] = "features";
roles[PriceRole] = "price";
+ roles[EndDateRole] = "endDate";
return roles;
}
+
+ApiServicesModel::ApiServicesData ApiServicesModel::getApiServicesData(const QJsonObject &data)
+{
+ auto serviceInfo = data.value(configKey::serviceInfo).toObject();
+ auto serviceType = data.value(configKey::serviceType).toString();
+ auto serviceProtocol = data.value(configKey::serviceProtocol).toString();
+ auto availableCountries = data.value(configKey::availableCountries).toArray();
+
+ auto subscriptionObject = data.value(configKey::subscription).toObject();
+
+ ApiServicesData serviceData;
+ serviceData.serviceInfo.name = serviceInfo.value(configKey::name).toString();
+ serviceData.serviceInfo.price = serviceInfo.value(configKey::price).toString();
+ serviceData.serviceInfo.region = serviceInfo.value(configKey::region).toString();
+ serviceData.serviceInfo.speed = serviceInfo.value(configKey::speed).toString();
+ serviceData.serviceInfo.timeLimit = serviceInfo.value(configKey::timelimit).toString();
+
+ serviceData.type = serviceType;
+ serviceData.protocol = serviceProtocol;
+
+ serviceData.storeEndpoint = serviceInfo.value(configKey::storeEndpoint).toString();
+
+ if (serviceInfo.value(configKey::isAvailable).isBool()) {
+ serviceData.isServiceAvailable = data.value(configKey::isAvailable).toBool();
+ } else {
+ serviceData.isServiceAvailable = true;
+ }
+
+ serviceData.serviceInfo.object = serviceInfo;
+ serviceData.availableCountries = availableCountries;
+
+ serviceData.subscription.endDate = subscriptionObject.value(configKey::endDate).toString();
+
+ return serviceData;
+}
diff --git a/client/ui/models/apiServicesModel.h b/client/ui/models/apiServicesModel.h
index 49918940..c96a49ab 100644
--- a/client/ui/models/apiServicesModel.h
+++ b/client/ui/models/apiServicesModel.h
@@ -3,6 +3,7 @@
#include
#include
+#include
class ApiServicesModel : public QAbstractListModel
{
@@ -15,10 +16,11 @@ public:
ServiceDescriptionRole,
IsServiceAvailableRole,
SpeedRole,
- WorkPeriodRole,
+ TimeLimitRole,
RegionRole,
FeaturesRole,
- PriceRole
+ PriceRole,
+ EndDateRole
};
explicit ApiServicesModel(QObject *parent = nullptr);
@@ -48,8 +50,40 @@ protected:
QHash roleNames() const override;
private:
+ struct ServiceInfo
+ {
+ QString name;
+ QString speed;
+ QString timeLimit;
+ QString region;
+ QString price;
+
+ QJsonObject object;
+ };
+
+ struct Subscription
+ {
+ QString endDate;
+ };
+
+ struct ApiServicesData
+ {
+ bool isServiceAvailable;
+
+ QString type;
+ QString protocol;
+ QString storeEndpoint;
+
+ ServiceInfo serviceInfo;
+ Subscription subscription;
+
+ QJsonArray availableCountries;
+ };
+
+ ApiServicesData getApiServicesData(const QJsonObject &data);
+
QString m_countryCode;
- QJsonArray m_services;
+ QVector m_services;
int m_selectedServiceIndex;
};
diff --git a/client/ui/models/servers_model.cpp b/client/ui/models/servers_model.cpp
index c87499a7..b72b10c3 100644
--- a/client/ui/models/servers_model.cpp
+++ b/client/ui/models/servers_model.cpp
@@ -22,7 +22,7 @@ namespace
constexpr char serviceProtocol[] = "service_protocol";
constexpr char publicKeyInfo[] = "public_key";
- constexpr char endDate[] = "end_date";
+ constexpr char expiresAt[] = "expires_at";
}
}
@@ -39,6 +39,9 @@ ServersModel::ServersModel(std::shared_ptr settings, QObject *parent)
emit ServersModel::defaultServerNameChanged();
updateDefaultServerContainersModel();
});
+
+ connect(this, &ServersModel::processedServerIndexChanged, this, &ServersModel::processedServerChanged);
+ connect(this, &ServersModel::dataChanged, this, &ServersModel::processedServerChanged);
}
int ServersModel::rowCount(const QModelIndex &parent) const
@@ -79,6 +82,12 @@ bool ServersModel::setData(const QModelIndex &index, const QVariant &value, int
return true;
}
+bool ServersModel::setData(const int index, const QVariant &value, int role)
+{
+ QModelIndex modelIndex = this->index(index);
+ return setData(modelIndex, value, role);
+}
+
QVariant ServersModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(m_servers.size())) {
@@ -679,6 +688,18 @@ QVariant ServersModel::getProcessedServerData(const QString roleString)
return {};
}
+bool ServersModel::setProcessedServerData(const QString &roleString, const QVariant &value)
+{
+ const auto roles = roleNames();
+ for (auto it = roles.begin(); it != roles.end(); it++) {
+ if (QString(it.value()) == roleString) {
+ return setData(m_processedServerIndex, value, it.key());
+ }
+ }
+
+ return false;
+}
+
bool ServersModel::isDefaultServerDefaultContainerHasSplitTunneling()
{
auto server = m_servers.at(m_defaultServerIndex).toObject();
@@ -718,9 +739,9 @@ bool ServersModel::isApiKeyExpired(const int serverIndex)
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
auto publicKeyInfo = apiConfig.value(configKey::publicKeyInfo).toObject();
- const QString endDate = publicKeyInfo.value(configKey::endDate).toString();
- if (endDate.isEmpty()) {
- publicKeyInfo.insert(configKey::endDate, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate));
+ const QString expiresAt = publicKeyInfo.value(configKey::expiresAt).toString();
+ if (expiresAt.isEmpty()) {
+ publicKeyInfo.insert(configKey::expiresAt, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate));
apiConfig.insert(configKey::publicKeyInfo, publicKeyInfo);
serverConfig.insert(configKey::apiConfig, apiConfig);
editServer(serverConfig, serverIndex);
@@ -728,8 +749,8 @@ bool ServersModel::isApiKeyExpired(const int serverIndex)
return false;
}
- auto endDateDateTime = QDateTime::fromString(endDate, Qt::ISODate).toUTC();
- if (endDateDateTime < QDateTime::currentDateTimeUtc()) {
+ auto expiresAtDateTime = QDateTime::fromString(expiresAt, Qt::ISODate).toUTC();
+ if (expiresAtDateTime < QDateTime::currentDateTimeUtc()) {
return true;
}
return false;
diff --git a/client/ui/models/servers_model.h b/client/ui/models/servers_model.h
index 0f18ea30..78bc22cc 100644
--- a/client/ui/models/servers_model.h
+++ b/client/ui/models/servers_model.h
@@ -46,6 +46,7 @@ public:
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
+ bool setData(const int index, const QVariant &value, int role = Qt::EditRole);
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant data(const int index, int role = Qt::DisplayRole) const;
@@ -115,6 +116,7 @@ public slots:
QVariant getDefaultServerData(const QString roleString);
QVariant getProcessedServerData(const QString roleString);
+ bool setProcessedServerData(const QString &roleString, const QVariant &value);
bool isDefaultServerDefaultContainerHasSplitTunneling();
@@ -127,6 +129,9 @@ protected:
signals:
void processedServerIndexChanged(const int index);
+ // emitted when the processed server index or processed server data is changed
+ void processedServerChanged();
+
void defaultServerIndexChanged(const int index);
void defaultServerNameChanged();
void defaultServerDescriptionChanged();
diff --git a/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml b/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml
index 120313cd..600db85d 100644
--- a/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml
+++ b/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml
@@ -54,8 +54,14 @@ PageType {
imageSource: "qrc:/images/controls/download.svg"
checked: index === ApiCountryModel.currentIndex
+ checkable: !ConnectionController.isConnected
onClicked: {
+ if (ConnectionController.isConnected) {
+ PageController.showNotificationMessage(qsTr("Unable change server location while there is an active connection"))
+ return
+ }
+
if (index !== ApiCountryModel.currentIndex) {
PageController.showBusyIndicator(true)
var prevIndex = ApiCountryModel.currentIndex
diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml
index 2d6c1d9b..167e56e5 100644
--- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml
+++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml
@@ -56,12 +56,15 @@ PageType {
}
LabelWithImageType {
+ property bool showSubscriptionEndDate: ServersModel.getProcessedServerData("isCountrySelectionAvailable")
+
Layout.fillWidth: true
Layout.margins: 16
imageSource: "qrc:/images/controls/history.svg"
- leftText: qsTr("Work period")
- rightText: ApiServicesModel.getSelectedServiceData("workPeriod")
+ leftText: showSubscriptionEndDate ? qsTr("Valid until") : qsTr("Work period")
+ rightText: showSubscriptionEndDate ? ApiServicesModel.getSelectedServiceData("endDate")
+ : ApiServicesModel.getSelectedServiceData("workPeriod")
visible: rightText !== ""
}
diff --git a/client/ui/qml/Pages2/PageSettingsServerInfo.qml b/client/ui/qml/Pages2/PageSettingsServerInfo.qml
index 95ae5c8a..ffcfb441 100644
--- a/client/ui/qml/Pages2/PageSettingsServerInfo.qml
+++ b/client/ui/qml/Pages2/PageSettingsServerInfo.qml
@@ -25,6 +25,8 @@ PageType {
property int pageSettingsApiServerInfo: 3
property int pageSettingsApiLanguageList: 4
+ property var processedServer
+
defaultActiveFocusItem: focusItem
Connections {
@@ -35,8 +37,18 @@ PageType {
}
}
+ Connections {
+ target: ServersModel
+
+ function onProcessedServerChanged() {
+ root.processedServer = proxyServersModel.get(0)
+ }
+ }
+
SortFilterProxyModel {
id: proxyServersModel
+ objectName: "proxyServersModel"
+
sourceModel: ServersModel
filters: [
ValueFilter {
@@ -44,147 +56,139 @@ PageType {
value: true
}
]
+
+ Component.onCompleted: {
+ root.processedServer = proxyServersModel.get(0)
+ }
}
Item {
id: focusItem
- KeyNavigation.tab: header
+ //KeyNavigation.tab: header
}
ColumnLayout {
anchors.fill: parent
- spacing: 16
+ spacing: 4
- Repeater {
- id: header
- model: proxyServersModel
+ BackButtonType {
+ id: backButton
- activeFocusOnTab: true
- onFocusChanged: {
- header.itemAt(0).focusItem.forceActiveFocus()
+ Layout.topMargin: 20
+ KeyNavigation.tab: headerContent.actionButton
+
+ backButtonFunction: function() {
+ if (nestedStackView.currentIndex === root.pageSettingsApiServerInfo &&
+ root.processedServer.isCountrySelectionAvailable) {
+ nestedStackView.currentIndex = root.pageSettingsApiLanguageList
+ } else {
+ PageController.closePage()
+ }
+ }
+ }
+
+ HeaderType {
+ id: headerContent
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+
+ actionButtonImage: nestedStackView.currentIndex === root.pageSettingsApiLanguageList ? "qrc:/images/controls/settings.svg"
+ : "qrc:/images/controls/edit-3.svg"
+
+ headerText: root.processedServer.name
+ descriptionText: {
+ if (root.processedServer.isServerFromGatewayApi) {
+ if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) {
+ return qsTr("Subscription is valid until ") + ApiServicesModel.getSelectedServiceData("endDate")
+ } else {
+ return ApiServicesModel.getSelectedServiceData("serviceDescription")
+ }
+ } else if (root.processedServer.isServerFromTelegramApi) {
+ return root.processedServer.serverDescription
+ } else if (root.processedServer.hasWriteAccess) {
+ return root.processedServer.credentialsLogin + " · " + root.processedServer.hostName
+ } else {
+ return root.processedServer.hostName
+ }
}
- delegate: ColumnLayout {
+ KeyNavigation.tab: tabBar
- property alias focusItem: backButton
+ actionButtonFunction: function() {
+ if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) {
+ nestedStackView.currentIndex = root.pageSettingsApiServerInfo
+ } else {
+ serverNameEditDrawer.open()
+ }
+ }
+ }
- id: content
+ DrawerType2 {
+ id: serverNameEditDrawer
- Layout.topMargin: 20
+ parent: root
- BackButtonType {
- id: backButton
- KeyNavigation.tab: headerContent.actionButton
+ anchors.fill: parent
+ expandedHeight: root.height * 0.35
- backButtonFunction: function() {
- if (nestedStackView.currentIndex === root.pageSettingsApiServerInfo &&
- ServersModel.getProcessedServerData("isCountrySelectionAvailable")) {
- nestedStackView.currentIndex = root.pageSettingsApiLanguageList
- } else {
- PageController.closePage()
- }
+ onClosed: {
+ if (!GC.isMobile()) {
+ headerContent.actionButton.forceActiveFocus()
+ }
+ }
+
+ expandedContent: ColumnLayout {
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.topMargin: 32
+ anchors.leftMargin: 16
+ anchors.rightMargin: 16
+
+ Connections {
+ target: serverNameEditDrawer
+ enabled: !GC.isMobile()
+ function onOpened() {
+ serverName.textField.forceActiveFocus()
}
}
- HeaderType {
- id: headerContent
+ Item {
+ id: focusItem1
+ KeyNavigation.tab: serverName.textField
+ }
+
+ TextFieldWithHeaderType {
+ id: serverName
+
Layout.fillWidth: true
- Layout.leftMargin: 16
- Layout.rightMargin: 16
+ headerText: qsTr("Server name")
+ textFieldText: root.processedServer.name
+ textField.maximumLength: 30
+ checkEmptyText: true
- actionButtonImage: nestedStackView.currentIndex === root.pageSettingsApiLanguageList ? "qrc:/images/controls/settings.svg" : "qrc:/images/controls/edit-3.svg"
-
- headerText: name
- descriptionText: {
- if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) {
- return ApiServicesModel.getSelectedServiceData("serviceDescription")
- } else if (ServersModel.getProcessedServerData("isServerFromTelegramApi")) {
- return serverDescription
- } else if (ServersModel.isProcessedServerHasWriteAccess()) {
- return credentialsLogin + " · " + hostName
- } else {
- return hostName
- }
- }
-
- KeyNavigation.tab: tabBar
-
- actionButtonFunction: function() {
- if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) {
- nestedStackView.currentIndex = root.pageSettingsApiServerInfo
- } else {
- serverNameEditDrawer.open()
- }
- }
+ KeyNavigation.tab: saveButton
}
- DrawerType2 {
- id: serverNameEditDrawer
+ BasicButtonType {
+ id: saveButton
- parent: root
+ Layout.fillWidth: true
- anchors.fill: parent
- expandedHeight: root.height * 0.35
+ text: qsTr("Save")
+ KeyNavigation.tab: focusItem1
- onClosed: {
- if (!GC.isMobile()) {
- headerContent.actionButton.forceActiveFocus()
- }
- }
-
- expandedContent: ColumnLayout {
- anchors.top: parent.top
- anchors.left: parent.left
- anchors.right: parent.right
- anchors.topMargin: 32
- anchors.leftMargin: 16
- anchors.rightMargin: 16
-
- Connections {
- target: serverNameEditDrawer
- enabled: !GC.isMobile()
- function onOpened() {
- serverName.textField.forceActiveFocus()
- }
+ clickedFunc: function() {
+ if (serverName.textFieldText === "") {
+ return
}
- Item {
- id: focusItem1
- KeyNavigation.tab: serverName.textField
- }
-
- TextFieldWithHeaderType {
- id: serverName
-
- Layout.fillWidth: true
- headerText: qsTr("Server name")
- textFieldText: name
- textField.maximumLength: 30
- checkEmptyText: true
-
- KeyNavigation.tab: saveButton
- }
-
- BasicButtonType {
- id: saveButton
-
- Layout.fillWidth: true
-
- text: qsTr("Save")
- KeyNavigation.tab: focusItem1
-
- clickedFunc: function() {
- if (serverName.textFieldText === "") {
- return
- }
-
- if (serverName.textFieldText !== name) {
- name = serverName.textFieldText
- }
- serverNameEditDrawer.close()
- }
+ if (serverName.textFieldText !== root.processedServer.name) {
+ ServersModel.setProcessedServerData("name", serverName.textFieldText);
}
+ serverNameEditDrawer.close()
}
}
}
@@ -257,8 +261,7 @@ PageType {
StackLayout {
id: nestedStackView
- Layout.preferredWidth: root.width
- Layout.preferredHeight: root.height - tabBar.implicitHeight - header.implicitHeight
+ Layout.fillWidth: true
currentIndex: ServersModel.getProcessedServerData("isServerFromGatewayApi") ?
(ServersModel.getProcessedServerData("isCountrySelectionAvailable") ?
diff --git a/metadata/img-readme/download-website-ru.svg b/metadata/img-readme/download-website-ru.svg
new file mode 100644
index 00000000..386ae4fe
--- /dev/null
+++ b/metadata/img-readme/download-website-ru.svg
@@ -0,0 +1,8 @@
+