Add news and notifications
This commit is contained in:
parent
2605978889
commit
470ce0f9c8
17 changed files with 546 additions and 1 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -9,6 +9,7 @@ deploy/build_32/*
|
||||||
deploy/build_64/*
|
deploy/build_64/*
|
||||||
winbuild*.bat
|
winbuild*.bat
|
||||||
.cache/
|
.cache/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
|
||||||
# Qt-es
|
# Qt-es
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include <QDirIterator>
|
#include <QDirIterator>
|
||||||
#include <QTranslator>
|
#include <QTranslator>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
|
||||||
#if defined(Q_OS_ANDROID)
|
#if defined(Q_OS_ANDROID)
|
||||||
#include "core/installedAppsImageProvider.h"
|
#include "core/installedAppsImageProvider.h"
|
||||||
|
@ -100,6 +101,11 @@ void CoreController::initModels()
|
||||||
|
|
||||||
m_apiDevicesModel.reset(new ApiDevicesModel(m_settings, this));
|
m_apiDevicesModel.reset(new ApiDevicesModel(m_settings, this));
|
||||||
m_engine->rootContext()->setContextProperty("ApiDevicesModel", m_apiDevicesModel.get());
|
m_engine->rootContext()->setContextProperty("ApiDevicesModel", m_apiDevicesModel.get());
|
||||||
|
|
||||||
|
m_newsModel.reset(new NewsModel(this));
|
||||||
|
m_engine->rootContext()->setContextProperty("NewsModel", m_newsModel.get());
|
||||||
|
QObject::connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit,
|
||||||
|
m_newsModel.get(), &NewsModel::saveLocalNews);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CoreController::initControllers()
|
void CoreController::initControllers()
|
||||||
|
@ -151,6 +157,10 @@ void CoreController::initControllers()
|
||||||
|
|
||||||
m_apiPremV1MigrationController.reset(new ApiPremV1MigrationController(m_serversModel, m_settings, this));
|
m_apiPremV1MigrationController.reset(new ApiPremV1MigrationController(m_serversModel, m_settings, this));
|
||||||
m_engine->rootContext()->setContextProperty("ApiPremV1MigrationController", m_apiPremV1MigrationController.get());
|
m_engine->rootContext()->setContextProperty("ApiPremV1MigrationController", m_apiPremV1MigrationController.get());
|
||||||
|
|
||||||
|
m_apiNewsController.reset(new ApiNewsController(m_newsModel, m_settings));
|
||||||
|
m_engine->rootContext()->setContextProperty("ApiNewsController", m_apiNewsController.get());
|
||||||
|
m_apiNewsController->fetchNews();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CoreController::initAndroidController()
|
void CoreController::initAndroidController()
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
#include "ui/controllers/api/apiConfigsController.h"
|
#include "ui/controllers/api/apiConfigsController.h"
|
||||||
#include "ui/controllers/api/apiSettingsController.h"
|
#include "ui/controllers/api/apiSettingsController.h"
|
||||||
#include "ui/controllers/api/apiPremV1MigrationController.h"
|
#include "ui/controllers/api/apiPremV1MigrationController.h"
|
||||||
|
#include "ui/controllers/api/apiNewsController.h"
|
||||||
#include "ui/controllers/appSplitTunnelingController.h"
|
#include "ui/controllers/appSplitTunnelingController.h"
|
||||||
#include "ui/controllers/allowedDnsController.h"
|
#include "ui/controllers/allowedDnsController.h"
|
||||||
#include "ui/controllers/connectionController.h"
|
#include "ui/controllers/connectionController.h"
|
||||||
|
@ -43,6 +44,7 @@
|
||||||
#include "ui/models/services/sftpConfigModel.h"
|
#include "ui/models/services/sftpConfigModel.h"
|
||||||
#include "ui/models/services/socks5ProxyConfigModel.h"
|
#include "ui/models/services/socks5ProxyConfigModel.h"
|
||||||
#include "ui/models/sites_model.h"
|
#include "ui/models/sites_model.h"
|
||||||
|
#include "ui/models/newsmodel.h"
|
||||||
|
|
||||||
#ifndef Q_OS_ANDROID
|
#ifndef Q_OS_ANDROID
|
||||||
#include "ui/notificationhandler.h"
|
#include "ui/notificationhandler.h"
|
||||||
|
@ -113,6 +115,7 @@ private:
|
||||||
QScopedPointer<ApiSettingsController> m_apiSettingsController;
|
QScopedPointer<ApiSettingsController> m_apiSettingsController;
|
||||||
QScopedPointer<ApiConfigsController> m_apiConfigsController;
|
QScopedPointer<ApiConfigsController> m_apiConfigsController;
|
||||||
QScopedPointer<ApiPremV1MigrationController> m_apiPremV1MigrationController;
|
QScopedPointer<ApiPremV1MigrationController> m_apiPremV1MigrationController;
|
||||||
|
QScopedPointer<ApiNewsController> m_apiNewsController;
|
||||||
|
|
||||||
QSharedPointer<ContainersModel> m_containersModel;
|
QSharedPointer<ContainersModel> m_containersModel;
|
||||||
QSharedPointer<ContainersModel> m_defaultServerContainersModel;
|
QSharedPointer<ContainersModel> m_defaultServerContainersModel;
|
||||||
|
@ -120,6 +123,7 @@ private:
|
||||||
QSharedPointer<LanguageModel> m_languageModel;
|
QSharedPointer<LanguageModel> m_languageModel;
|
||||||
QSharedPointer<ProtocolsModel> m_protocolsModel;
|
QSharedPointer<ProtocolsModel> m_protocolsModel;
|
||||||
QSharedPointer<SitesModel> m_sitesModel;
|
QSharedPointer<SitesModel> m_sitesModel;
|
||||||
|
QSharedPointer<NewsModel> m_newsModel;
|
||||||
QSharedPointer<AllowedDnsModel> m_allowedDnsModel;
|
QSharedPointer<AllowedDnsModel> m_allowedDnsModel;
|
||||||
QSharedPointer<AppSplitTunnelingModel> m_appSplitTunnelingModel;
|
QSharedPointer<AppSplitTunnelingModel> m_appSplitTunnelingModel;
|
||||||
QSharedPointer<ClientManagementModel> m_clientManagementModel;
|
QSharedPointer<ClientManagementModel> m_clientManagementModel;
|
||||||
|
|
14
client/images/controls/news-unread.svg
Normal file
14
client/images/controls/news-unread.svg
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 74 74" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_4_34)">
|
||||||
|
<path d="M55.5 12.3333H18.5C15.0942 12.3333 12.3333 15.0943 12.3333 18.5V55.5C12.3333 58.9058 15.0942 61.6667 18.5 61.6667H55.5C58.9057 61.6667 61.6666 58.9058 61.6666 55.5V18.5C61.6666 15.0943 58.9057 12.3333 55.5 12.3333Z" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M21.5833 24.6667H52.4167" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M21.5833 37H52.4167" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M21.5833 49.3333H40.0833" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<circle cx="61.5" cy="12.5" r="15" fill="#FBB36B" stroke="#1C1D21" stroke-width="5"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_4_34">
|
||||||
|
<rect width="74" height="74" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 982 B |
8
client/images/controls/news.svg
Normal file
8
client/images/controls/news.svg
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#CBCAC8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<!-- Основа газеты -->
|
||||||
|
<rect x="4" y="4" width="16" height="16" rx="2"/>
|
||||||
|
<!-- Линии текста -->
|
||||||
|
<line x1="7" y1="8" x2="17" y2="8"/>
|
||||||
|
<line x1="7" y1="12" x2="17" y2="12"/>
|
||||||
|
<line x1="7" y1="16" x2="13" y2="16"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 410 B |
5
client/images/controls/settings-news.svg
Normal file
5
client/images/controls/settings-news.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.9 KiB |
3
client/images/controls/unread-dot.svg
Normal file
3
client/images/controls/unread-dot.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="17.5" cy="17.5" r="15" fill="#FBB36B" stroke="#1C1D21" stroke-width="5"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 188 B |
|
@ -35,6 +35,9 @@
|
||||||
<file>images/controls/mail.svg</file>
|
<file>images/controls/mail.svg</file>
|
||||||
<file>images/controls/map-pin.svg</file>
|
<file>images/controls/map-pin.svg</file>
|
||||||
<file>images/controls/more-vertical.svg</file>
|
<file>images/controls/more-vertical.svg</file>
|
||||||
|
<file>images/controls/news.svg</file>
|
||||||
|
<file>images/controls/news-unread.svg</file>
|
||||||
|
<file>images/controls/unread-dot.svg</file>
|
||||||
<file>images/controls/plus.svg</file>
|
<file>images/controls/plus.svg</file>
|
||||||
<file>images/controls/qr-code.svg</file>
|
<file>images/controls/qr-code.svg</file>
|
||||||
<file>images/controls/radio-button-inner-circle-pressed.png</file>
|
<file>images/controls/radio-button-inner-circle-pressed.png</file>
|
||||||
|
@ -49,6 +52,7 @@
|
||||||
<file>images/controls/server.svg</file>
|
<file>images/controls/server.svg</file>
|
||||||
<file>images/controls/settings-2.svg</file>
|
<file>images/controls/settings-2.svg</file>
|
||||||
<file>images/controls/settings.svg</file>
|
<file>images/controls/settings.svg</file>
|
||||||
|
<file>images/controls/settings-news.svg</file>
|
||||||
<file>images/controls/share-2.svg</file>
|
<file>images/controls/share-2.svg</file>
|
||||||
<file>images/controls/split-tunneling.svg</file>
|
<file>images/controls/split-tunneling.svg</file>
|
||||||
<file>images/controls/tag.svg</file>
|
<file>images/controls/tag.svg</file>
|
||||||
|
@ -212,6 +216,8 @@
|
||||||
<file>ui/qml/Pages2/PageSettingsServerServices.qml</file>
|
<file>ui/qml/Pages2/PageSettingsServerServices.qml</file>
|
||||||
<file>ui/qml/Pages2/PageSettingsServersList.qml</file>
|
<file>ui/qml/Pages2/PageSettingsServersList.qml</file>
|
||||||
<file>ui/qml/Pages2/PageSettingsSplitTunneling.qml</file>
|
<file>ui/qml/Pages2/PageSettingsSplitTunneling.qml</file>
|
||||||
|
<file>ui/qml/Pages2/PageSettingsNewsNotifications.qml</file>
|
||||||
|
<file>ui/qml/Pages2/PageSettingsNewsDetail.qml</file>
|
||||||
<file>ui/qml/Pages2/PageProtocolAwgClientSettings.qml</file>
|
<file>ui/qml/Pages2/PageProtocolAwgClientSettings.qml</file>
|
||||||
<file>ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml</file>
|
<file>ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml</file>
|
||||||
<file>ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml</file>
|
<file>ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml</file>
|
||||||
|
|
37
client/ui/controllers/api/apiNewsController.cpp
Normal file
37
client/ui/controllers/api/apiNewsController.cpp
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
#include "apiNewsController.h"
|
||||||
|
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
|
ApiNewsController::ApiNewsController(const QSharedPointer<NewsModel> &newsModel,
|
||||||
|
const std::shared_ptr<Settings> &settings,
|
||||||
|
QObject *parent)
|
||||||
|
: QObject(parent), m_newsModel(newsModel), m_settings(settings)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApiNewsController::fetchNews()
|
||||||
|
{
|
||||||
|
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(),
|
||||||
|
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
|
||||||
|
QByteArray responseBody;
|
||||||
|
ErrorCode errorCode = gatewayController.get(QString("%1v1/news"), responseBody);
|
||||||
|
qDebug() << "fetchNews" << errorCode;
|
||||||
|
if (errorCode != ErrorCode::NoError) {
|
||||||
|
emit errorOccurred(errorCode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(responseBody);
|
||||||
|
QJsonArray newsArray;
|
||||||
|
if (doc.isArray()) {
|
||||||
|
newsArray = doc.array();
|
||||||
|
} else if (doc.isObject()) {
|
||||||
|
QJsonObject obj = doc.object();
|
||||||
|
if (obj.value("news").isArray()) {
|
||||||
|
newsArray = obj.value("news").toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_newsModel->updateModel(newsArray);
|
||||||
|
}
|
32
client/ui/controllers/api/apiNewsController.h
Normal file
32
client/ui/controllers/api/apiNewsController.h
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
#ifndef APINEWSCONTROLLER_H
|
||||||
|
#define APINEWSCONTROLLER_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QSharedPointer>
|
||||||
|
#include <memory>
|
||||||
|
#include <QJsonArray>
|
||||||
|
|
||||||
|
#include "settings.h"
|
||||||
|
#include "ui/models/newsmodel.h"
|
||||||
|
#include "core/controllers/gatewayController.h"
|
||||||
|
#include "core/api/apiDefs.h"
|
||||||
|
|
||||||
|
class ApiNewsController : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit ApiNewsController(const QSharedPointer<NewsModel> &newsModel,
|
||||||
|
const std::shared_ptr<Settings> &settings,
|
||||||
|
QObject *parent = nullptr);
|
||||||
|
|
||||||
|
Q_INVOKABLE void fetchNews();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void errorOccurred(ErrorCode errorCode);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QSharedPointer<NewsModel> m_newsModel;
|
||||||
|
std::shared_ptr<Settings> m_settings;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // APINEWSCONTROLLER_H
|
|
@ -26,6 +26,8 @@ namespace PageLoader
|
||||||
PageSettingsConnection,
|
PageSettingsConnection,
|
||||||
PageSettingsDns,
|
PageSettingsDns,
|
||||||
PageSettingsApplication,
|
PageSettingsApplication,
|
||||||
|
PageSettingsNewsNotifications,
|
||||||
|
PageSettingsNewsDetail,
|
||||||
PageSettingsBackup,
|
PageSettingsBackup,
|
||||||
PageSettingsAbout,
|
PageSettingsAbout,
|
||||||
PageSettingsLogging,
|
PageSettingsLogging,
|
||||||
|
|
195
client/ui/models/newsmodel.cpp
Normal file
195
client/ui/models/newsmodel.cpp
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
#include "ui/models/newsmodel.h"
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonValue>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
NewsModel::NewsModel(QObject *parent)
|
||||||
|
: QAbstractListModel(parent)
|
||||||
|
{
|
||||||
|
loadLocalNews();
|
||||||
|
}
|
||||||
|
|
||||||
|
int NewsModel::rowCount(const QModelIndex &parent) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(parent);
|
||||||
|
return m_items.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant NewsModel::data(const QModelIndex &index, int role) const
|
||||||
|
{
|
||||||
|
if (!index.isValid() || index.row() < 0 || index.row() >= m_items.size())
|
||||||
|
return QVariant();
|
||||||
|
|
||||||
|
const NewsItem &item = m_items.at(index.row());
|
||||||
|
switch (role) {
|
||||||
|
case IdRole:
|
||||||
|
return item.id;
|
||||||
|
case TitleRole:
|
||||||
|
return item.title;
|
||||||
|
case ContentRole:
|
||||||
|
return item.content;
|
||||||
|
case TimestampRole:
|
||||||
|
return item.timestamp.toString(Qt::ISODate);
|
||||||
|
case IsReadRole:
|
||||||
|
return item.read;
|
||||||
|
case IsProcessedRole:
|
||||||
|
return index.row() == m_processedIndex;
|
||||||
|
default:
|
||||||
|
return QVariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray> NewsModel::roleNames() const
|
||||||
|
{
|
||||||
|
QHash<int, QByteArray> roles;
|
||||||
|
roles[IdRole] = "id";
|
||||||
|
roles[TitleRole] = "title";
|
||||||
|
roles[ContentRole] = "content";
|
||||||
|
roles[TimestampRole] = "timestamp";
|
||||||
|
roles[IsReadRole] = "read";
|
||||||
|
roles[IsProcessedRole] = "isProcessed";
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NewsModel::markAsRead(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= m_items.size())
|
||||||
|
return;
|
||||||
|
if (!m_items[index].read) {
|
||||||
|
m_items[index].read = true;
|
||||||
|
QModelIndex idx = createIndex(index, 0);
|
||||||
|
emit dataChanged(idx, idx, {IsReadRole});
|
||||||
|
emit hasUnreadChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int NewsModel::processedIndex() const
|
||||||
|
{
|
||||||
|
return m_processedIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NewsModel::setProcessedIndex(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= m_items.size() || m_processedIndex == index)
|
||||||
|
return;
|
||||||
|
m_processedIndex = index;
|
||||||
|
emit processedIndexChanged(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NewsModel::updateModel(const QJsonArray &serverItems)
|
||||||
|
{
|
||||||
|
QSet<QString> existingIds;
|
||||||
|
for (const NewsItem &item : m_items) {
|
||||||
|
existingIds.insert(item.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<NewsItem> newItems;
|
||||||
|
for (const QJsonValue &value : serverItems) {
|
||||||
|
if (!value.isObject()) continue;
|
||||||
|
const QJsonObject obj = value.toObject();
|
||||||
|
QString id = obj.value("id").toString();
|
||||||
|
|
||||||
|
if (!existingIds.contains(id)) {
|
||||||
|
NewsItem item;
|
||||||
|
item.id = id;
|
||||||
|
item.title = obj.value("header").toString();
|
||||||
|
item.content = obj.value("content").toString();
|
||||||
|
item.timestamp = QDateTime::fromString(obj.value("timestamp").toString(), Qt::ISODate);
|
||||||
|
item.read = false; // New news is always unread
|
||||||
|
newItems.append(item);
|
||||||
|
existingIds.insert(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newItems.isEmpty()) {
|
||||||
|
beginResetModel();
|
||||||
|
m_items.append(newItems);
|
||||||
|
// Sort descending by timestamp (newest first)
|
||||||
|
std::sort(m_items.begin(), m_items.end(), [](const NewsItem &a, const NewsItem &b) {
|
||||||
|
return a.timestamp > b.timestamp;
|
||||||
|
});
|
||||||
|
endResetModel();
|
||||||
|
emit hasUnreadChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NewsModel::hasUnread() const
|
||||||
|
{
|
||||||
|
for (const NewsItem &item : m_items) {
|
||||||
|
if (!item.read)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString NewsModel::localFilePath() const
|
||||||
|
{
|
||||||
|
QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
||||||
|
QDir dir(path);
|
||||||
|
if (!dir.exists()) {
|
||||||
|
dir.mkpath(".");
|
||||||
|
}
|
||||||
|
return path + "/news.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
void NewsModel::loadLocalNews()
|
||||||
|
{
|
||||||
|
QFile file(localFilePath());
|
||||||
|
if (!file.exists() || !file.open(QIODevice::ReadOnly)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
if (!doc.isArray()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
beginResetModel();
|
||||||
|
m_items.clear();
|
||||||
|
|
||||||
|
QJsonArray newsArray = doc.array();
|
||||||
|
for (const QJsonValue &value : newsArray) {
|
||||||
|
if (!value.isObject())
|
||||||
|
continue;
|
||||||
|
const QJsonObject obj = value.toObject();
|
||||||
|
NewsItem item;
|
||||||
|
item.id = obj.value("id").toString();
|
||||||
|
item.title = obj.value("header").toString();
|
||||||
|
item.content = obj.value("content").toString();
|
||||||
|
item.timestamp = QDateTime::fromString(obj.value("timestamp").toString(), Qt::ISODate);
|
||||||
|
item.read = obj.value("read").toBool();
|
||||||
|
m_items.append(item);
|
||||||
|
}
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NewsModel::saveLocalNews() const
|
||||||
|
{
|
||||||
|
QJsonArray newsArray;
|
||||||
|
for (const auto &item : m_items) {
|
||||||
|
QJsonObject obj;
|
||||||
|
obj["id"] = item.id;
|
||||||
|
obj["header"] = item.title;
|
||||||
|
obj["content"] = item.content;
|
||||||
|
obj["timestamp"] = item.timestamp.toString(Qt::ISODate);
|
||||||
|
obj["read"] = item.read;
|
||||||
|
newsArray.append(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonDocument doc(newsArray);
|
||||||
|
QFile file(localFilePath());
|
||||||
|
if (file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||||
|
file.write(doc.toJson());
|
||||||
|
file.close();
|
||||||
|
} else {
|
||||||
|
qWarning() << "Could not save news to" << localFilePath();
|
||||||
|
}
|
||||||
|
}
|
58
client/ui/models/newsmodel.h
Normal file
58
client/ui/models/newsmodel.h
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
#ifndef NEWSMODEL_H
|
||||||
|
#define NEWSMODEL_H
|
||||||
|
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QVector>
|
||||||
|
#include <QString>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QSet>
|
||||||
|
|
||||||
|
struct NewsItem {
|
||||||
|
QString id;
|
||||||
|
QString title;
|
||||||
|
QString content;
|
||||||
|
QDateTime timestamp;
|
||||||
|
bool read;
|
||||||
|
};
|
||||||
|
|
||||||
|
class NewsModel : public QAbstractListModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
enum Roles {
|
||||||
|
IdRole = Qt::UserRole + 1,
|
||||||
|
TitleRole,
|
||||||
|
ContentRole,
|
||||||
|
TimestampRole,
|
||||||
|
IsReadRole,
|
||||||
|
IsProcessedRole
|
||||||
|
};
|
||||||
|
explicit NewsModel(QObject *parent = nullptr);
|
||||||
|
Q_INVOKABLE void markAsRead(int index);
|
||||||
|
Q_INVOKABLE void saveLocalNews() const;
|
||||||
|
|
||||||
|
Q_PROPERTY(int processedIndex READ processedIndex WRITE setProcessedIndex NOTIFY processedIndexChanged)
|
||||||
|
Q_PROPERTY(bool hasUnread READ hasUnread NOTIFY hasUnreadChanged)
|
||||||
|
int processedIndex() const;
|
||||||
|
void setProcessedIndex(int index);
|
||||||
|
|
||||||
|
void updateModel(const QJsonArray &items);
|
||||||
|
bool hasUnread() const;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void processedIndexChanged(int index);
|
||||||
|
void hasUnreadChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QVector<NewsItem> m_items;
|
||||||
|
int m_processedIndex = -1;
|
||||||
|
void loadLocalNews();
|
||||||
|
QString localFilePath() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // NEWSMODEL_H
|
|
@ -85,6 +85,21 @@ PageType {
|
||||||
|
|
||||||
DividerType {}
|
DividerType {}
|
||||||
|
|
||||||
|
LabelWithButtonType {
|
||||||
|
id: news
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
text: qsTr("News & Notifications")
|
||||||
|
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||||
|
leftImageSource: NewsModel.hasUnread ? "qrc:/images/controls/news-unread.svg" : "qrc:/images/controls/news.svg"
|
||||||
|
|
||||||
|
clickedFunction: function() {
|
||||||
|
PageController.goToPage(PageEnum.PageSettingsNewsNotifications)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DividerType {}
|
||||||
|
|
||||||
LabelWithButtonType {
|
LabelWithButtonType {
|
||||||
id: backup
|
id: backup
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
68
client/ui/qml/Pages2/PageSettingsNewsDetail.qml
Normal file
68
client/ui/qml/Pages2/PageSettingsNewsDetail.qml
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
import PageEnum 1.0
|
||||||
|
import Style 1.0
|
||||||
|
|
||||||
|
import "./"
|
||||||
|
import "../Controls2"
|
||||||
|
import "../Controls2/TextTypes"
|
||||||
|
import "../Config"
|
||||||
|
import SortFilterProxyModel 0.2
|
||||||
|
|
||||||
|
PageType {
|
||||||
|
id: root
|
||||||
|
property var newsItem
|
||||||
|
|
||||||
|
SortFilterProxyModel {
|
||||||
|
id: proxyNews
|
||||||
|
sourceModel: NewsModel
|
||||||
|
filters: [ ValueFilter { roleName: "isProcessed"; value: true } ]
|
||||||
|
Component.onCompleted: root.newsItem = proxyNews.get(0)
|
||||||
|
}
|
||||||
|
Connections {
|
||||||
|
target: NewsModel
|
||||||
|
function onProcessedIndexChanged() {
|
||||||
|
root.newsItem = proxyNews.get(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BackButtonType {
|
||||||
|
id: backButton
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.topMargin: 20
|
||||||
|
}
|
||||||
|
|
||||||
|
FlickableType {
|
||||||
|
id: fl
|
||||||
|
anchors.top: backButton.bottom
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
contentHeight: content.height
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: content
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
BaseHeaderType {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: 16
|
||||||
|
Layout.rightMargin: 16
|
||||||
|
headerText: newsItem.title
|
||||||
|
}
|
||||||
|
|
||||||
|
ParagraphTextType {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
Layout.leftMargin: 16
|
||||||
|
Layout.rightMargin: 16
|
||||||
|
text: newsItem.content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
81
client/ui/qml/Pages2/PageSettingsNewsNotifications.qml
Normal file
81
client/ui/qml/Pages2/PageSettingsNewsNotifications.qml
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
import PageEnum 1.0
|
||||||
|
import Style 1.0
|
||||||
|
|
||||||
|
import "./"
|
||||||
|
import "../Controls2"
|
||||||
|
import "../Controls2/TextTypes"
|
||||||
|
import "../Config"
|
||||||
|
|
||||||
|
PageType {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: header
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
|
||||||
|
anchors.topMargin: 20
|
||||||
|
|
||||||
|
BackButtonType {
|
||||||
|
id: backButton
|
||||||
|
}
|
||||||
|
|
||||||
|
BaseHeaderType {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: 16
|
||||||
|
Layout.rightMargin: 16
|
||||||
|
|
||||||
|
headerText: qsTr("News & Notifications")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: newsList
|
||||||
|
width: parent.width
|
||||||
|
anchors.top: header.bottom
|
||||||
|
anchors.topMargin: 16
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
|
||||||
|
property bool isFocusable: true
|
||||||
|
|
||||||
|
model: NewsModel
|
||||||
|
|
||||||
|
clip: true
|
||||||
|
reuseItems: true
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
implicitWidth: newsList.width
|
||||||
|
implicitHeight: content.implicitHeight
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: content
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
|
||||||
|
LabelWithButtonType {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
leftImageSource: read ? "" : "qrc:/images/controls/unread-dot.svg"
|
||||||
|
isSmallLeftImage: !read
|
||||||
|
text: title
|
||||||
|
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||||
|
|
||||||
|
clickedFunction: function() {
|
||||||
|
NewsModel.markAsRead(index)
|
||||||
|
NewsModel.processedIndex = index
|
||||||
|
PageController.goToPage(PageEnum.PageSettingsNewsDetail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DividerType {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -367,7 +367,13 @@ PageType {
|
||||||
objectName: "settingsTabButton"
|
objectName: "settingsTabButton"
|
||||||
|
|
||||||
isSelected: tabBar.currentIndex === 2
|
isSelected: tabBar.currentIndex === 2
|
||||||
image: "qrc:/images/controls/settings.svg"
|
image: NewsModel.hasUnread ? "qrc:/images/controls/settings-news.svg" : "qrc:/images/controls/settings.svg"
|
||||||
|
Binding {
|
||||||
|
target: settingsTabButton
|
||||||
|
property: "defaultColor"
|
||||||
|
value: "transparent"
|
||||||
|
when: NewsModel.hasUnread
|
||||||
|
}
|
||||||
clickedFunc: function () {
|
clickedFunc: function () {
|
||||||
tabBarStackView.goToTabBarPage(PageEnum.PageSettings)
|
tabBarStackView.goToTabBarPage(PageEnum.PageSettings)
|
||||||
tabBar.currentIndex = 2
|
tabBar.currentIndex = 2
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue