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/*
|
||||
winbuild*.bat
|
||||
.cache/
|
||||
.vscode/
|
||||
|
||||
|
||||
# Qt-es
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <QDirIterator>
|
||||
#include <QTranslator>
|
||||
#include <QCoreApplication>
|
||||
|
||||
#if defined(Q_OS_ANDROID)
|
||||
#include "core/installedAppsImageProvider.h"
|
||||
|
@ -100,6 +101,11 @@ void CoreController::initModels()
|
|||
|
||||
m_apiDevicesModel.reset(new ApiDevicesModel(m_settings, this));
|
||||
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()
|
||||
|
@ -151,6 +157,10 @@ void CoreController::initControllers()
|
|||
|
||||
m_apiPremV1MigrationController.reset(new ApiPremV1MigrationController(m_serversModel, m_settings, this));
|
||||
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()
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#include "ui/controllers/api/apiConfigsController.h"
|
||||
#include "ui/controllers/api/apiSettingsController.h"
|
||||
#include "ui/controllers/api/apiPremV1MigrationController.h"
|
||||
#include "ui/controllers/api/apiNewsController.h"
|
||||
#include "ui/controllers/appSplitTunnelingController.h"
|
||||
#include "ui/controllers/allowedDnsController.h"
|
||||
#include "ui/controllers/connectionController.h"
|
||||
|
@ -43,6 +44,7 @@
|
|||
#include "ui/models/services/sftpConfigModel.h"
|
||||
#include "ui/models/services/socks5ProxyConfigModel.h"
|
||||
#include "ui/models/sites_model.h"
|
||||
#include "ui/models/newsmodel.h"
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include "ui/notificationhandler.h"
|
||||
|
@ -113,6 +115,7 @@ private:
|
|||
QScopedPointer<ApiSettingsController> m_apiSettingsController;
|
||||
QScopedPointer<ApiConfigsController> m_apiConfigsController;
|
||||
QScopedPointer<ApiPremV1MigrationController> m_apiPremV1MigrationController;
|
||||
QScopedPointer<ApiNewsController> m_apiNewsController;
|
||||
|
||||
QSharedPointer<ContainersModel> m_containersModel;
|
||||
QSharedPointer<ContainersModel> m_defaultServerContainersModel;
|
||||
|
@ -120,6 +123,7 @@ private:
|
|||
QSharedPointer<LanguageModel> m_languageModel;
|
||||
QSharedPointer<ProtocolsModel> m_protocolsModel;
|
||||
QSharedPointer<SitesModel> m_sitesModel;
|
||||
QSharedPointer<NewsModel> m_newsModel;
|
||||
QSharedPointer<AllowedDnsModel> m_allowedDnsModel;
|
||||
QSharedPointer<AppSplitTunnelingModel> m_appSplitTunnelingModel;
|
||||
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/map-pin.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/qr-code.svg</file>
|
||||
<file>images/controls/radio-button-inner-circle-pressed.png</file>
|
||||
|
@ -49,6 +52,7 @@
|
|||
<file>images/controls/server.svg</file>
|
||||
<file>images/controls/settings-2.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/split-tunneling.svg</file>
|
||||
<file>images/controls/tag.svg</file>
|
||||
|
@ -212,6 +216,8 @@
|
|||
<file>ui/qml/Pages2/PageSettingsServerServices.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsServersList.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/PageProtocolWireGuardClientSettings.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,
|
||||
PageSettingsDns,
|
||||
PageSettingsApplication,
|
||||
PageSettingsNewsNotifications,
|
||||
PageSettingsNewsDetail,
|
||||
PageSettingsBackup,
|
||||
PageSettingsAbout,
|
||||
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 {}
|
||||
|
||||
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 {
|
||||
id: backup
|
||||
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"
|
||||
|
||||
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 () {
|
||||
tabBarStackView.goToTabBarPage(PageEnum.PageSettings)
|
||||
tabBar.currentIndex = 2
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue