This commit is contained in:
Martin Rotter 2023-12-15 09:02:26 +01:00
parent b935f58e18
commit e9e97786bb
7 changed files with 274 additions and 16 deletions

View File

@ -1,6 +1,25 @@
<html>
<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">
body {
font-family: Roboto, sans-serif;
@ -13,7 +32,8 @@
}
.entry {
max-width: 90%;
max-width: 720px;
smax-width: 100%;
xborder: 1px solid red;
padding: 20px;
margin: 3px auto;
@ -92,6 +112,22 @@
.entry .date {
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>
</head>
<body>
@ -114,23 +150,103 @@
</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">
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 = {
method: "ArticlesFromFeed",
data: {
feed: feedId.toString(),
account: accountId,
newest_first: true,
unread_only: true,
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 jsonPayload = {
method: 'POST',
@ -150,17 +266,21 @@
}
function renderMessages(jsonMessages) {
let elemArticles = document.querySelector("section.articles");
elemArticles.innerHTML = "";
window.scrollTo(0,0);
jsonMessages.forEach(function(jsonMessage) {
transformMessage(jsonMessage);
let article = fillTemplate(jsonMessage);
document.querySelector("section.articles").appendChild(article);
elemArticles.appendChild(article);
});
}
function transformMessage(jsonMessage) {
jsonMessage.date_created = new Date(jsonMessage.date_created);
jsonMessage.dateHuman = helpers.getHumanDate(jsonMessage.date_created);
jsonMessage.date = new Date(jsonMessage.date_created);
jsonMessage.dateHuman = helpers.getHumanDate(jsonMessage.date);
jsonMessage.enclosedUrlA = helpers.getEnclosedImageUrl(jsonMessage.enclosures);
let elemContent = document.createElement("div");
@ -175,10 +295,19 @@
let article = template.cloneNode(true);
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(".author").forEach(e => e.textContent = jsonMessage.author);
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(".date").forEach(e => e.textContent = jsonMessage.dateHuman);
@ -190,7 +319,39 @@
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 = {
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) {
let dateHuman = date.toDateString();
dateHuman += " " + date.getHours();
@ -206,11 +367,10 @@
}
};
let params = new URLSearchParams(window.location.search);
let feedId = +params.get("feedId");
let accountId = +params.get("accountId");
processParams();
attachHandlers();
fetchMessages();
fetchMessages(feedId, accountId);
</script>
</body>
</html>

View File

@ -47,6 +47,10 @@ QIcon IconFactory::fromByteArray(QByteArray array) {
}
QByteArray IconFactory::toByteArray(const QIcon& icon) {
if (icon.isNull()) {
return {};
}
QByteArray array;
QBuffer buffer(&array);

View File

@ -23,11 +23,14 @@ void FormEditStandardAccount::loadAccountData() {
else {
m_standardDetails->m_ui.m_txtTitle->setText(m_account->title());
}
m_standardDetails->m_ui.m_btnIcon->setIcon(m_account->fullIcon());
}
void FormEditStandardAccount::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->saveAccountDataToDatabase();

View File

@ -2,6 +2,63 @@
#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) {
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());
}

View File

@ -19,6 +19,10 @@ class StandardAccountDetails : public QWidget {
public:
explicit StandardAccountDetails(QWidget* parent = nullptr);
private slots:
void onLoadIconFromFile();
void onUseDefaultIcon();
private:
Ui::StandardAccountDetails m_ui;
};

View File

@ -24,6 +24,29 @@
<item row="0" column="1">
<widget class="QLineEdit" name="m_txtTitle"/>
</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>
</widget>
<resources/>

View File

@ -355,6 +355,7 @@ QVariantHash StandardServiceRoot::customDatabaseData() const {
QVariantHash data = ServiceRoot::customDatabaseData();
data[QSL("title")] = title();
data[QSL("icon")] = IconFactory::toByteArray(icon());
return data;
}
@ -363,6 +364,12 @@ void StandardServiceRoot::setCustomDatabaseData(const QVariantHash& data) {
ServiceRoot::setCustomDatabaseData(data);
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() {