fix #695
This commit is contained in:
parent
b935f58e18
commit
e9e97786bb
7 changed files with 274 additions and 16 deletions
|
@ -1,6 +1,25 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<!-- sqlite3 -json "C:\Users\J\AppData\Local\RSS Guard 4\database\database.temp.delete.db" "SELECT * FROM Messages WHERE account_id = 2 ORDER BY date_created DESC LIMIT 10;" > render.json -->
|
<!--
|
||||||
|
features todo:
|
||||||
|
- optional queries ✔
|
||||||
|
- accountId ✔, feedId ✔
|
||||||
|
- pageSize ✔
|
||||||
|
- read ✔, starred ✔, ascending ✔
|
||||||
|
- request next page ✔
|
||||||
|
- sanity check url params ✔
|
||||||
|
- mark read - page
|
||||||
|
- mark read/unread - individual
|
||||||
|
- mark star/unstar - individual
|
||||||
|
- render read/unread/starred
|
||||||
|
- missing thumbnail
|
||||||
|
- search/filter?
|
||||||
|
- UI to select unead only/starred only/all?
|
||||||
|
- Loading spinners
|
||||||
|
- feed selector
|
||||||
|
- error management
|
||||||
|
- cleaner code
|
||||||
|
-->
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
body {
|
body {
|
||||||
font-family: Roboto, sans-serif;
|
font-family: Roboto, sans-serif;
|
||||||
|
@ -13,7 +32,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.entry {
|
.entry {
|
||||||
max-width: 90%;
|
max-width: 720px;
|
||||||
|
smax-width: 100%;
|
||||||
xborder: 1px solid red;
|
xborder: 1px solid red;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin: 3px auto;
|
margin: 3px auto;
|
||||||
|
@ -92,6 +112,22 @@
|
||||||
.entry .date {
|
.entry .date {
|
||||||
color: #888888;
|
color: #888888;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.articles_post {
|
||||||
|
max-width: 720px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 3px auto;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: #262626;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.next_page {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -114,23 +150,103 @@
|
||||||
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<section class="articles">
|
<div>
|
||||||
|
<section class="articles">
|
||||||
|
|
||||||
|
</section>
|
||||||
|
<section class="articles_post">
|
||||||
|
<button class="next_page">Next Page</button>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
</section>
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
function fetchMessages(feedId, accountId) {
|
function attachHandlers() {
|
||||||
|
document.querySelector("button.next_page").addEventListener("click", onNextPageClicked);
|
||||||
|
}
|
||||||
|
|
||||||
|
function processParams() {
|
||||||
|
let params = new URLSearchParams(window.location.search);
|
||||||
|
let elemArticles = document.querySelector("section.articles");
|
||||||
|
|
||||||
|
if (params.has("feed"))
|
||||||
|
elemArticles.setAttribute("data-rssg-feed", params.get("feed"));
|
||||||
|
|
||||||
|
if (params.has("account")) {
|
||||||
|
let accountId = helpers.checkIntOrThrow(
|
||||||
|
params.get("account"), "account"
|
||||||
|
);
|
||||||
|
elemArticles.setAttribute("data-rssg-account", accountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.has("row_limit")) {
|
||||||
|
let row_limit = helpers.checkIntOrThrow(
|
||||||
|
params.get("row_limit"), "row_limit"
|
||||||
|
);
|
||||||
|
elemArticles.setAttribute("data-rssg-row-limit", row_limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let newest_first = params.get("newest_first") || "true";
|
||||||
|
newest_first = helpers.checkBoolOrThrow(newest_first, "newest_first");
|
||||||
|
elemArticles.setAttribute("data-rssg-newest-first", newest_first);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let unread_only = params.get("unread_only") || "true";
|
||||||
|
unread_only = helpers.checkBoolOrThrow(unread_only, "unread_only");
|
||||||
|
elemArticles.setAttribute("data-rssg-unread-only", unread_only);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let starred_only = params.get("starred_only") || "false";
|
||||||
|
starred_only = helpers.checkBoolOrThrow(starred_only, "starred_only");
|
||||||
|
elemArticles.setAttribute("data-rssg-starred-only", starred_only);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function assembleRpcCall() {
|
||||||
let jsonRpcCall = {
|
let jsonRpcCall = {
|
||||||
method: "ArticlesFromFeed",
|
method: "ArticlesFromFeed",
|
||||||
data: {
|
data: {
|
||||||
feed: feedId.toString(),
|
|
||||||
account: accountId,
|
|
||||||
newest_first: true,
|
newest_first: true,
|
||||||
unread_only: true,
|
unread_only: true,
|
||||||
row_offset: 0,
|
row_offset: 0,
|
||||||
row_limit: 10000
|
row_limit: 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let elemArticles = document.querySelector("section.articles");
|
||||||
|
|
||||||
|
if (elemArticles.hasAttribute("data-rssg-row-offset"))
|
||||||
|
jsonRpcCall.data.row_offset = +elemArticles.getAttribute("data-rssg-row-offset");
|
||||||
|
|
||||||
|
if (elemArticles.hasAttribute("data-rssg-after-date")) {
|
||||||
|
delete jsonRpcCall.data.row_offset;
|
||||||
|
jsonRpcCall.data.start_after_article_date = +elemArticles.getAttribute("data-rssg-after-date");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elemArticles.hasAttribute("data-rssg-feed"))
|
||||||
|
jsonRpcCall.data.feed = elemArticles.getAttribute("data-rssg-feed");
|
||||||
|
|
||||||
|
if (elemArticles.hasAttribute("data-rssg-account"))
|
||||||
|
jsonRpcCall.data.account = +elemArticles.getAttribute("data-rssg-account");
|
||||||
|
|
||||||
|
if (elemArticles.hasAttribute("data-rssg-row-limit"))
|
||||||
|
jsonRpcCall.data.row_limit = +elemArticles.getAttribute("data-rssg-row-limit");
|
||||||
|
|
||||||
|
jsonRpcCall.data.newest_first = elemArticles.getAttribute("data-rssg-newest-first") == "true";
|
||||||
|
|
||||||
|
jsonRpcCall.data.unread_only = elemArticles.getAttribute("data-rssg-unread-only") == "true";
|
||||||
|
|
||||||
|
jsonRpcCall.data.starred_only = elemArticles.getAttribute("data-rssg-starred-only") == "true";
|
||||||
|
|
||||||
|
return jsonRpcCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchMessages() {
|
||||||
|
let jsonRpcCall = assembleRpcCall();
|
||||||
|
|
||||||
let urlApi = "http://localhost:54123";
|
let urlApi = "http://localhost:54123";
|
||||||
let jsonPayload = {
|
let jsonPayload = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
@ -150,17 +266,21 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMessages(jsonMessages) {
|
function renderMessages(jsonMessages) {
|
||||||
|
let elemArticles = document.querySelector("section.articles");
|
||||||
|
elemArticles.innerHTML = "";
|
||||||
|
window.scrollTo(0,0);
|
||||||
|
|
||||||
jsonMessages.forEach(function(jsonMessage) {
|
jsonMessages.forEach(function(jsonMessage) {
|
||||||
transformMessage(jsonMessage);
|
transformMessage(jsonMessage);
|
||||||
|
|
||||||
let article = fillTemplate(jsonMessage);
|
let article = fillTemplate(jsonMessage);
|
||||||
document.querySelector("section.articles").appendChild(article);
|
elemArticles.appendChild(article);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function transformMessage(jsonMessage) {
|
function transformMessage(jsonMessage) {
|
||||||
jsonMessage.date_created = new Date(jsonMessage.date_created);
|
jsonMessage.date = new Date(jsonMessage.date_created);
|
||||||
jsonMessage.dateHuman = helpers.getHumanDate(jsonMessage.date_created);
|
jsonMessage.dateHuman = helpers.getHumanDate(jsonMessage.date);
|
||||||
jsonMessage.enclosedUrlA = helpers.getEnclosedImageUrl(jsonMessage.enclosures);
|
jsonMessage.enclosedUrlA = helpers.getEnclosedImageUrl(jsonMessage.enclosures);
|
||||||
|
|
||||||
let elemContent = document.createElement("div");
|
let elemContent = document.createElement("div");
|
||||||
|
@ -175,10 +295,19 @@
|
||||||
let article = template.cloneNode(true);
|
let article = template.cloneNode(true);
|
||||||
article.classList.remove("template");
|
article.classList.remove("template");
|
||||||
|
|
||||||
|
article.setAttribute("data-rssg-id", jsonMessage.id);
|
||||||
|
article.setAttribute("data-rssg-customid", jsonMessage.custom_id);
|
||||||
|
article.setAttribute("data-rssg-date", jsonMessage.date_created);
|
||||||
|
article.setAttribute("data-rssg-feed", jsonMessage.feed_custom_id);
|
||||||
|
article.setAttribute("data-rssg-account", jsonMessage.account_id);
|
||||||
|
article.setAttribute("data-rssg-read", jsonMessage.is_read);
|
||||||
|
article.setAttribute("data-rssg-important", jsonMessage.is_important);
|
||||||
|
article.setAttribute("data-rssg-rtl", jsonMessage.is_rtl);
|
||||||
|
|
||||||
article.querySelectorAll(".title").forEach(e => e.textContent = jsonMessage.title);
|
article.querySelectorAll(".title").forEach(e => e.textContent = jsonMessage.title);
|
||||||
article.querySelectorAll(".author").forEach(e => e.textContent = jsonMessage.author);
|
article.querySelectorAll(".author").forEach(e => e.textContent = jsonMessage.author);
|
||||||
article.querySelectorAll(".content").forEach(e => e.textContent = jsonMessage.contentShort);
|
article.querySelectorAll(".content").forEach(e => e.textContent = jsonMessage.contentShort);
|
||||||
article.querySelectorAll(".feed").forEach(e => e.textContent = jsonMessage.feed_custom_id);
|
article.querySelectorAll(".feed").forEach(e => e.textContent = jsonMessage.feed_title);
|
||||||
article.querySelectorAll(".author").forEach(e => e.textContent = jsonMessage.author);
|
article.querySelectorAll(".author").forEach(e => e.textContent = jsonMessage.author);
|
||||||
article.querySelectorAll(".date").forEach(e => e.textContent = jsonMessage.dateHuman);
|
article.querySelectorAll(".date").forEach(e => e.textContent = jsonMessage.dateHuman);
|
||||||
|
|
||||||
|
@ -190,7 +319,39 @@
|
||||||
return article;
|
return article;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onNextPageClicked() {
|
||||||
|
let elemArticles = document.querySelector("section.articles");
|
||||||
|
|
||||||
|
let articleEntryLast = elemArticles.querySelector("article.entry:last-child");
|
||||||
|
|
||||||
|
if (!articleEntryLast) return;
|
||||||
|
if (!articleEntryLast.hasAttribute("data-rssg-date")) return;
|
||||||
|
|
||||||
|
let dateAfter = +articleEntryLast.getAttribute("data-rssg-date");
|
||||||
|
|
||||||
|
elemArticles.removeAttribute("data-rssg-row-offset");
|
||||||
|
elemArticles.setAttribute("data-rssg-after-date", dateAfter);
|
||||||
|
|
||||||
|
fetchMessages();
|
||||||
|
}
|
||||||
|
|
||||||
let helpers = {
|
let helpers = {
|
||||||
|
checkIntOrThrow: function(value, name) {
|
||||||
|
value = +value;
|
||||||
|
if (Number.isInteger(value)) return value;
|
||||||
|
let message = "Invalid parameter, must be an integer: " + name;
|
||||||
|
alert(message);
|
||||||
|
throw message;
|
||||||
|
},
|
||||||
|
|
||||||
|
checkBoolOrThrow: function(value, name) {
|
||||||
|
value = value.toLowerCase();
|
||||||
|
if (value == "true" || value == "false") return value == "true";
|
||||||
|
let message = "Invalid parameter, must be boolean: " + name;
|
||||||
|
alert(message);
|
||||||
|
throw message;
|
||||||
|
},
|
||||||
|
|
||||||
getHumanDate: function(date) {
|
getHumanDate: function(date) {
|
||||||
let dateHuman = date.toDateString();
|
let dateHuman = date.toDateString();
|
||||||
dateHuman += " " + date.getHours();
|
dateHuman += " " + date.getHours();
|
||||||
|
@ -206,11 +367,10 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let params = new URLSearchParams(window.location.search);
|
processParams();
|
||||||
let feedId = +params.get("feedId");
|
attachHandlers();
|
||||||
let accountId = +params.get("accountId");
|
fetchMessages();
|
||||||
|
|
||||||
fetchMessages(feedId, accountId);
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -47,6 +47,10 @@ QIcon IconFactory::fromByteArray(QByteArray array) {
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray IconFactory::toByteArray(const QIcon& icon) {
|
QByteArray IconFactory::toByteArray(const QIcon& icon) {
|
||||||
|
if (icon.isNull()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
QByteArray array;
|
QByteArray array;
|
||||||
QBuffer buffer(&array);
|
QBuffer buffer(&array);
|
||||||
|
|
||||||
|
|
|
@ -23,11 +23,14 @@ void FormEditStandardAccount::loadAccountData() {
|
||||||
else {
|
else {
|
||||||
m_standardDetails->m_ui.m_txtTitle->setText(m_account->title());
|
m_standardDetails->m_ui.m_txtTitle->setText(m_account->title());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_standardDetails->m_ui.m_btnIcon->setIcon(m_account->fullIcon());
|
||||||
}
|
}
|
||||||
|
|
||||||
void FormEditStandardAccount::apply() {
|
void FormEditStandardAccount::apply() {
|
||||||
FormAccountDetails::apply();
|
FormAccountDetails::apply();
|
||||||
|
|
||||||
|
m_account->setIcon(m_standardDetails->m_ui.m_btnIcon->icon());
|
||||||
m_account->setTitle(m_standardDetails->m_ui.m_txtTitle->text());
|
m_account->setTitle(m_standardDetails->m_ui.m_txtTitle->text());
|
||||||
|
|
||||||
m_account->saveAccountDataToDatabase();
|
m_account->saveAccountDataToDatabase();
|
||||||
|
|
|
@ -2,6 +2,63 @@
|
||||||
|
|
||||||
#include "services/standard/gui/standardaccountdetails.h"
|
#include "services/standard/gui/standardaccountdetails.h"
|
||||||
|
|
||||||
|
#include "3rd-party/boolinq/boolinq.h"
|
||||||
|
#include "miscellaneous/iconfactory.h"
|
||||||
|
#include "services/standard/standardserviceentrypoint.h"
|
||||||
|
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QImageReader>
|
||||||
|
|
||||||
StandardAccountDetails::StandardAccountDetails(QWidget* parent) : QWidget(parent) {
|
StandardAccountDetails::StandardAccountDetails(QWidget* parent) : QWidget(parent) {
|
||||||
m_ui.setupUi(this);
|
m_ui.setupUi(this);
|
||||||
|
|
||||||
|
QMenu* icon_menu = new QMenu(tr("Icon selection"), this);
|
||||||
|
auto* action_load_icon_from_file =
|
||||||
|
new QAction(qApp->icons()->fromTheme(QSL("image-x-generic")), tr("Load icon from file..."), this);
|
||||||
|
auto* action_default_icon =
|
||||||
|
new QAction(qApp->icons()->fromTheme(QSL("application-rss+xml")), tr("Use default icon from icon theme"), this);
|
||||||
|
|
||||||
|
connect(action_load_icon_from_file, &QAction::triggered, this, &StandardAccountDetails::onLoadIconFromFile);
|
||||||
|
connect(action_default_icon, &QAction::triggered, this, &StandardAccountDetails::onUseDefaultIcon);
|
||||||
|
|
||||||
|
icon_menu->addAction(action_load_icon_from_file);
|
||||||
|
icon_menu->addAction(action_default_icon);
|
||||||
|
|
||||||
|
m_ui.m_btnIcon->setMenu(icon_menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StandardAccountDetails::onLoadIconFromFile() {
|
||||||
|
auto supported_formats = QImageReader::supportedImageFormats();
|
||||||
|
auto prefixed_formats = boolinq::from(supported_formats)
|
||||||
|
.select([](const QByteArray& frmt) {
|
||||||
|
return QSL("*.%1").arg(QString::fromLocal8Bit(frmt));
|
||||||
|
})
|
||||||
|
.toStdList();
|
||||||
|
|
||||||
|
QStringList list_formats = FROM_STD_LIST(QStringList, prefixed_formats);
|
||||||
|
|
||||||
|
QFileDialog dialog(this,
|
||||||
|
tr("Select icon file for the account"),
|
||||||
|
qApp->homeFolder(),
|
||||||
|
tr("Images (%1)").arg(list_formats.join(QL1C(' '))));
|
||||||
|
|
||||||
|
dialog.setFileMode(QFileDialog::FileMode::ExistingFile);
|
||||||
|
dialog.setWindowIcon(qApp->icons()->fromTheme(QSL("image-x-generic")));
|
||||||
|
dialog.setOptions(QFileDialog::Option::DontUseNativeDialog | QFileDialog::Option::ReadOnly);
|
||||||
|
dialog.setViewMode(QFileDialog::ViewMode::Detail);
|
||||||
|
dialog.setLabelText(QFileDialog::DialogLabel::Accept, tr("Select icon"));
|
||||||
|
dialog.setLabelText(QFileDialog::DialogLabel::Reject, tr("Cancel"));
|
||||||
|
|
||||||
|
//: Label for field with icon file name textbox for selection dialog.
|
||||||
|
dialog.setLabelText(QFileDialog::DialogLabel::LookIn, tr("Look in:"));
|
||||||
|
dialog.setLabelText(QFileDialog::DialogLabel::FileName, tr("Icon name:"));
|
||||||
|
dialog.setLabelText(QFileDialog::DialogLabel::FileType, tr("Icon type:"));
|
||||||
|
|
||||||
|
if (dialog.exec() == QDialog::DialogCode::Accepted) {
|
||||||
|
m_ui.m_btnIcon->setIcon(QIcon(dialog.selectedFiles().value(0)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StandardAccountDetails::onUseDefaultIcon() {
|
||||||
|
m_ui.m_btnIcon->setIcon(StandardServiceEntryPoint().icon());
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,10 @@ class StandardAccountDetails : public QWidget {
|
||||||
public:
|
public:
|
||||||
explicit StandardAccountDetails(QWidget* parent = nullptr);
|
explicit StandardAccountDetails(QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onLoadIconFromFile();
|
||||||
|
void onUseDefaultIcon();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Ui::StandardAccountDetails m_ui;
|
Ui::StandardAccountDetails m_ui;
|
||||||
};
|
};
|
||||||
|
|
|
@ -24,6 +24,29 @@
|
||||||
<item row="0" column="1">
|
<item row="0" column="1">
|
||||||
<widget class="QLineEdit" name="m_txtTitle"/>
|
<widget class="QLineEdit" name="m_txtTitle"/>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="text">
|
||||||
|
<string>Icon</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QToolButton" name="m_btnIcon">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Select icon for your account.</string>
|
||||||
|
</property>
|
||||||
|
<property name="popupMode">
|
||||||
|
<enum>QToolButton::InstantPopup</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<resources/>
|
<resources/>
|
||||||
|
|
|
@ -355,6 +355,7 @@ QVariantHash StandardServiceRoot::customDatabaseData() const {
|
||||||
QVariantHash data = ServiceRoot::customDatabaseData();
|
QVariantHash data = ServiceRoot::customDatabaseData();
|
||||||
|
|
||||||
data[QSL("title")] = title();
|
data[QSL("title")] = title();
|
||||||
|
data[QSL("icon")] = IconFactory::toByteArray(icon());
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
@ -363,6 +364,12 @@ void StandardServiceRoot::setCustomDatabaseData(const QVariantHash& data) {
|
||||||
ServiceRoot::setCustomDatabaseData(data);
|
ServiceRoot::setCustomDatabaseData(data);
|
||||||
|
|
||||||
setTitle(data.value(QSL("title"), defaultTitle()).toString());
|
setTitle(data.value(QSL("title"), defaultTitle()).toString());
|
||||||
|
|
||||||
|
QByteArray icon_data = data.value(QSL("icon")).toByteArray();
|
||||||
|
|
||||||
|
if (!icon_data.isEmpty()) {
|
||||||
|
setIcon(IconFactory::fromByteArray(icon_data));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString StandardServiceRoot::defaultTitle() {
|
QString StandardServiceRoot::defaultTitle() {
|
||||||
|
|
Loading…
Add table
Reference in a new issue