Files
qBittorrent/src/webui/www/private/views/rss.html
Chocobo1 6b52a04ff1 WebUI: avoid queuing up requests
`setInterval()` will always fire a new timeout regardless previous `updateRssFeedList()` has
completed or not. This patch will now wait for previous request to complete before another
timeout.
2024-07-25 14:20:57 +08:00

834 lines
36 KiB
HTML

<style>
#rssView {
padding: 20px 20px 0 20px;
height: calc(100% - 20px);
}
#rssContentView {
display: table;
width: 100%;
height: calc(100% - 30px);
vertical-align: top;
}
#rssFeedFixedHeaderDiv .dynamicTableHeader,
#rssArticleFixedHeaderDiv .dynamicTableHeader {
cursor: default;
}
.alignRight {
float: right;
}
.unreadArticle {
color: blue;
}
#rssFetchingDisabled {
color: red;
font-style: italic;
margin-bottom: 10px;
}
#centerRssColumn {
margin: 0 10px 0 10px;
}
#leftRssColumn,
#centerRssColumn,
#rightRssColumn {
float: left;
/* should be 20 px but due to rounding differences some browsers don't render that properly */
width: calc(calc(100% - 21px) / 3);
border: none;
}
#rightRssColumn {
overflow: auto;
}
#rssFeedTableDiv,
#rssArticleTableDiv {
height: calc(100vh - 180px);
}
#rssTorrentDetailsName {
background-color: #678db2;
padding: 0;
color: white;
}
#rssTorrentDetailsDate {
background-color: #EFEFEF;
}
#rssDetailsView {
height: calc(100vh - 135px);
overflow: auto;
}
#rssButtonBar {
overflow: hidden;
height: 30px;
}
#rssContentView table {
width: 100%;
}
#rssDescription {
width: 100%;
border: none;
}
</style>
<div id="rssView">
<div id="rssTopBar">
<div id="rssFetchingDisabled" class="invisible">
QBT_TR(Fetching of RSS feeds is disabled now! You can enable it in application settings.)QBT_TR[CONTEXT=RSSWidget]
</div>
<div id="rssButtonBar">
<button type="button" id="newSubscriptionButton" onclick="qBittorrent.Rss.addRSSFeed()">QBT_TR(New subscription)QBT_TR[CONTEXT=RSSWidget]</button>
<button type="button" id="markReadButton" onclick="qBittorrent.Rss.markSelectedAsRead()">QBT_TR(Mark items read)QBT_TR[CONTEXT=RSSWidget]</button>
<button type="button" id="updateAllButton" onclick="qBittorrent.Rss.refreshAllFeeds()">QBT_TR(Update all)QBT_TR[CONTEXT=RSSWidget]</button>
<button type="button" id="rssDownloaderButton" class="alignRight" onclick="qBittorrent.Rss.openRssDownloader()">QBT_TR(RSS Downloader...)QBT_TR[CONTEXT=RSSWidget]</button>
</div>
</div>
<div id="rssContentView">
<div id="leftRssColumn">
<div id="rssFeedFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
</table>
</div>
<div id="rssFeedTableDiv" class="dynamicTableDiv">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div id="centerRssColumn">
<div id="rssArticleFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
</table>
</div>
<div id="rssArticleTableDiv" class="dynamicTableDiv">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div id="rightRssColumn">
<div id="rssDetailsView"></div>
</div>
</div>
</div>
<ul id="rssFeedMenu" class="contextMenu">
<li><a href="#update"><img src="images/view-refresh.svg" alt="QBT_TR(Update)QBT_TR[CONTEXT=RSSWidget]"> QBT_TR(Update)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li><a href="#markRead"><img src="images/task-complete.svg" alt="QBT_TR(Mark items read)QBT_TR[CONTEXT=RSSWidget]"> QBT_TR(Mark items read)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li class="separator"><a href="#rename"><img src="images/edit-rename.svg" alt="QBT_TR(Rename...)QBT_TR[CONTEXT=RSSWidget]"> QBT_TR(Rename...)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li><a href="#delete"><img src="images/edit-clear.svg" alt="QBT_TR(Delete)QBT_TR[CONTEXT=RSSWidget]"> QBT_TR(Delete)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li class="separator"><a href="#newSubscription"><img src="images/list-add.svg" alt="QBT_TR(New subscription...)QBT_TR[CONTEXT=RSSWidget]"> QBT_TR(New subscription...)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li><a href="#newFolder"><img src="images/folder-new.svg" alt="QBT_TR(New folder...)QBT_TR[CONTEXT=RSSWidget]"> QBT_TR(New folder...)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li class="separator"><a href="#updateAll"><img src="images/view-refresh.svg" alt="QBT_TR(Update all feeds)QBT_TR[CONTEXT=RSSWidget]"> QBT_TR(Update all feeds)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li class="separator"><a href="#copyFeedURL" id="CopyFeedURL"><img src="images/edit-copy.svg" alt="QBT_TR(Copy feed URL)QBT_TR[CONTEXT=RSSWidget]"> QBT_TR(Copy feed URL)QBT_TR[CONTEXT=RSSWidget]</a></li>
</ul>
<ul id="rssArticleMenu" class="contextMenu">
<li><a href="#Download"><img src="images/downloading.svg" alt="QBT_TR(Download torrent)QBT_TR[CONTEXT=RSSWidget]"> QBT_TR(Download torrent)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li><a href="#OpenNews"><img src="images/application-url.svg" alt="QBT_TR(Open news URL)QBT_TR[CONTEXT=RSSWidget]"> QBT_TR(Open news URL)QBT_TR[CONTEXT=RSSWidget]</a></li>
</ul>
<script>
"use strict";
window.qBittorrent ??= {};
window.qBittorrent.Rss ??= (() => {
const exports = () => {
return {
init: init,
unload: unload,
load: load,
showRssFeed: showRssFeed,
showDetails: showDetails,
updateRssFeedList: updateRssFeedList,
refreshAllFeeds: refreshAllFeeds,
moveItem: moveItem,
addRSSFeed: addRSSFeed,
markSelectedAsRead: markSelectedAsRead,
openRssDownloader: openRssDownloader,
rssFeedTable: rssFeedTable
};
};
const serverSyncRssDataInterval = 1500;
let feedData = {};
let pathByFeedId = new Map();
let feedRefreshTimer = -1;
const rssFeedTable = new window.qBittorrent.DynamicTable.RssFeedTable();
const rssArticleTable = new window.qBittorrent.DynamicTable.RssArticleTable();
const init = () => {
const pref = window.parent.qBittorrent.Cache.preferences.get();
if (!pref.rss_processing_enabled)
$("rssFetchingDisabled").removeClass("invisible");
// recalculate heights
const nonPageHeight = $("rssTopBar").getBoundingClientRect().height
+ $("desktopHeader").getBoundingClientRect().height
+ $("desktopFooterWrapper").getBoundingClientRect().height + 20;
$("rssDetailsView").style.height = "calc(100vh - " + nonPageHeight + "px)";
const nonTableHeight = nonPageHeight + $("rssFeedFixedHeaderDiv").getBoundingClientRect().height;
$("rssFeedTableDiv").style.height = "calc(100vh - " + nonTableHeight + "px)";
$("rssArticleTableDiv").style.height = "calc(100vh - " + nonTableHeight + "px)";
$("rssContentView").style.height = "calc(100% - " + $("rssTopBar").getBoundingClientRect().height + "px)";
const rssFeedContextMenu = new window.qBittorrent.ContextMenu.RssFeedContextMenu({
targets: ".rssFeedContextMenuTarget",
menu: "rssFeedMenu",
actions: {
update: (el) => {
const feedsToUpdate = new Set();
rssFeedTable.selectedRows.each((rowId) => {
const selectedPath = rssFeedTable.rows[rowId].full_data.dataPath;
rssFeedTable.rows.filter((row) => row.full_data.dataPath.slice(0, selectedPath.length) === selectedPath)
.filter((row) => row.full_data.dataUid !== "")
.each((row) => feedsToUpdate.add(row));
});
feedsToUpdate.forEach((feed) => refreshFeed(feed.full_data.dataUid));
},
markRead: markSelectedAsRead,
rename: (el) => {
moveItem(rssFeedTable.rows[rssFeedTable.selectedRows[0]].full_data.dataPath);
},
delete: (el) => {
const selectedDatapaths = rssFeedTable.selectedRows
.filter((e) => e !== 0)
.map((sRow) => rssFeedTable.rows[sRow].full_data.dataPath);
// filter children
const reducedDatapaths = selectedDatapaths.filter((path) =>
selectedDatapaths.filter((innerPath) => path.slice(0, innerPath.length) === innerPath).length === 1
);
removeItem(reducedDatapaths);
},
newSubscription: addRSSFeed,
newFolder: addFolder,
updateAll: refreshAllFeeds
},
offsets: {
x: -16,
y: -57
}
});
rssFeedContextMenu.addTarget($("rssFeedTableDiv"));
// deselect feed when clicking on empty part of table
$("rssFeedTableDiv").addEventListener("click", (e) => {
rssFeedTable.deselectAll();
rssFeedTable.deselectRow();
});
$("rssFeedTableDiv").addEventListener("contextmenu", (e) => {
if (e.toElement.nodeName === "DIV") {
rssFeedTable.deselectAll();
rssFeedTable.deselectRow();
rssFeedContextMenu.updateMenuItems();
}
});
new ClipboardJS("#CopyFeedURL", {
text: () => {
let joined = "";
rssFeedTable.selectedRows
.filter((row) => rssFeedTable.rows[row].full_data.dataUid !== "")
.each((row) => joined += rssFeedTable.rows[row].full_data.dataUrl + "\n");
return joined.slice(0, -1);
}
});
rssFeedTable.setup("rssFeedTableDiv", "rssFeedFixedHeaderDiv", rssFeedContextMenu);
const rssArticleContextMenu = new window.qBittorrent.ContextMenu.RssArticleContextMenu({
targets: ".rssArticleElement",
menu: "rssArticleMenu",
actions: {
Download: (el) => {
let dlString = "";
rssArticleTable.selectedRows.each((row) => {
dlString += rssArticleTable.rows[row].full_data.torrentURL + "\n";
});
showDownloadPage([dlString]);
},
OpenNews: (el) => {
rssArticleTable.selectedRows.each((row) => {
window.open(rssArticleTable.rows[row].full_data.link);
});
}
},
offsets: {
x: -16,
y: -57
}
});
rssArticleTable.setup("rssArticleTableDiv", "rssArticleFixedHeaderDiv", rssArticleContextMenu);
updateRssFeedList();
load();
};
const unload = () => {
clearTimeout(feedRefreshTimer);
feedRefreshTimer = -1;
};
const load = () => {
feedRefreshTimer = setTimeout(() => {
updateRssFeedList();
load();
}, serverSyncRssDataInterval);
};
const addRSSFeed = () => {
let path = "";
if (rssFeedTable.selectedRows.length !== 0) {
const row = rssFeedTable.rows[rssFeedTable.selectedRows[0]];
if (row.full_data.dataUid === "") {
path = row.full_data.dataPath;
}
else {
const lastIndex = row.full_data.dataPath.lastIndexOf("\\");
if (lastIndex !== -1)
path = row.full_data.dataPath.slice(0, lastIndex);
}
}
new MochaUI.Window({
id: "newFeed",
title: "QBT_TR(Please type a RSS feed URL)QBT_TR[CONTEXT=RSSWidget]",
loadMethod: "iframe",
contentURL: "newfeed.html?path=" + encodeURIComponent(path),
scrollbars: false,
resizable: false,
maximizable: false,
width: 350,
height: 100
});
};
const addFolder = () => {
let path = "";
if (rssFeedTable.selectedRows.length !== 0) {
const row = rssFeedTable.rows[rssFeedTable.selectedRows[0]];
if (row.full_data.dataUid === "") {
path = row.full_data.dataPath;
}
else {
const lastIndex = row.full_data.dataPath.lastIndexOf("\\");
if (lastIndex !== -1)
path = row.full_data.dataPath.slice(0, lastIndex);
}
}
new MochaUI.Window({
id: "newFolder",
title: "QBT_TR(Please choose a folder name)QBT_TR[CONTEXT=RSSWidget]",
loadMethod: "iframe",
contentURL: "newfolder.html?path=" + encodeURIComponent(path),
scrollbars: false,
resizable: false,
maximizable: false,
width: 350,
height: 100
});
};
const showRssFeed = (path) => {
rssArticleTable.clear();
let rowCount = 0;
const childFeeds = new Set();
rssFeedTable.rows.filter((row) => row.full_data.dataPath.slice(0, path.length) === path)
.filter((row) => row.full_data.dataUid !== "")
.each((row) => childFeeds.add(row.full_data.dataUid));
let visibleArticles = [];
for (const feedEntry in feedData) {
if (childFeeds.has(feedEntry)) {
visibleArticles.append(feedData[feedEntry]
.map((a) => {
a.feedUid = feedEntry;
return a;
}));
}
}
// filter read articles if "Unread" feed is selected
if (path === "")
visibleArticles = visibleArticles.filter((a) => !a.isRead);
visibleArticles.sort((e1, e2) => new Date(e2.date) - new Date(e1.date))
.each((torrentEntry) => {
rssArticleTable.updateRowData({
rowId: rowCount++,
name: torrentEntry.title,
link: torrentEntry.link,
torrentURL: torrentEntry.torrentURL,
feedUid: torrentEntry.feedUid,
dataId: torrentEntry.id,
isRead: torrentEntry.isRead
});
});
$("rssDetailsView").getChildren().each(c => c.destroy());
rssArticleTable.updateTable(false);
};
const showDetails = (feedUid, articleID) => {
markArticleAsRead(pathByFeedId.get(feedUid), articleID);
$("rssDetailsView").getChildren().each(c => c.destroy());
const article = feedData[feedUid].filter((article) => article.id === articleID)[0];
if (article) {
$("rssDetailsView").append((() => {
const torrentName = document.createElement("p");
torrentName.textContent = article.title;
torrentName.id = "rssTorrentDetailsName";
return torrentName;
})());
$("rssDetailsView").append((() => {
const torrentDate = document.createElement("div");
torrentDate.id = "rssTorrentDetailsDate";
const torrentDateDesc = document.createElement("b");
torrentDateDesc.textContent = "QBT_TR(Date: )QBT_TR[CONTEXT=RSSWidget]";
torrentDate.append(torrentDateDesc);
const torrentDateData = document.createElement("span");
torrentDateData.textContent = new Date(article.date).toLocaleString();
torrentDate.append(torrentDateData);
return torrentDate;
})());
// Place in iframe with sandbox attribute to prevent js execution
const torrentDescription = document.createRange().createContextualFragment('<iframe sandbox id="rssDescription"></iframe>');
$("rssDetailsView").append(torrentDescription);
document.getElementById("rssDescription").srcdoc = '<html><head><link rel="stylesheet" type="text/css" href="css/style.css"></head><body>' + article.description + "</body></html>";
// calculate height to fill screen
document.getElementById("rssDescription").style.height =
"calc(100% - " + document.getElementById("rssTorrentDetailsName").offsetHeight + "px - "
+ document.getElementById("rssTorrentDetailsDate").offsetHeight + "px - 5px)";
}
};
const updateRssFeedList = () => {
new Request.JSON({
url: "api/v2/rss/items",
method: "get",
noCache: true,
data: {
withData: true
},
onSuccess: (response) => {
// flatten folder structure
const flattenedResp = [];
const recFlatten = (current, name = "", depth = 0, fullName = "") => {
for (const child in current) {
if (!Object.hasOwn(current, child))
continue;
const currentFullName = fullName ? (fullName + "\\" + child) : child;
if (current[child].uid !== undefined) {
current[child].name = child;
current[child].isFolder = false;
current[child].depth = depth;
current[child].fullName = currentFullName;
flattenedResp.push(current[child]);
}
else {
flattenedResp.push({
name: child,
isFolder: true,
depth: depth,
fullName: currentFullName
});
recFlatten(current[child], child, depth + 1, currentFullName);
}
}
};
recFlatten(response);
// check if rows matches flattened response
let match = false;
if ((rssFeedTable.rows.getLength() - 1) === flattenedResp.length) {
match = true;
for (let i = 0; i < flattenedResp.length; ++i) {
if (((flattenedResp[i].uid ? flattenedResp[i].uid : "") !== rssFeedTable.rows[i + 1].full_data.dataUid)
|| (flattenedResp[i].fullName !== rssFeedTable.rows[i + 1].full_data.dataPath)) {
match = false;
break;
}
}
}
if (match) {
// partial refresh
// update status
let statusDiffers = false;
for (let i = 0; i < flattenedResp.length; ++i) {
const oldStatus = rssFeedTable.rows[i + 1].full_data.status;
let status = "default";
if (flattenedResp[i].hasError)
status = "hasError";
if (flattenedResp[i].isLoading)
status = "isLoading";
if (flattenedResp[i].isFolder)
status = "isFolder";
if (oldStatus !== status) {
statusDiffers = true;
rssFeedTable.updateRowData({
rowId: i + 1,
status: status
});
}
}
if (statusDiffers)
rssFeedTable.updateIcons();
// get currently opened feed
let openedFeedPath = undefined;
if (rssFeedTable.selectedRows.length !== 0) {
const lastSelectedRow = rssFeedTable.selectedRows[rssFeedTable.selectedRows.length - 1];
openedFeedPath = rssFeedTable.rows[lastSelectedRow].full_data.dataPath;
}
// check if list of articles differs
let needsUpdate = false;
flattenedResp.filter((r) => !r.isFolder)
.each((r) => {
let articlesDiffer = true;
if (r.articles.length === feedData[r.uid].length) {
articlesDiffer = false;
for (let i = 0; i < r.articles.length; ++i) {
if (feedData[r.uid][i].id !== r.articles[i].id)
articlesDiffer = true;
}
}
if (articlesDiffer) {
// update unread count
const oldUnread = feedData[r.uid].map((art) => !art.isRead).filter((v) => v).length;
const newUnread = r.articles.map((art) => !art.isRead).filter((v) => v).length;
const unreadDifference = newUnread - oldUnread;
// find all parents (and self) and add unread difference
rssFeedTable.rows.filter((row) => r.fullName.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
.each((row) => row.full_data.unread += unreadDifference);
needsUpdate = true;
// update data
feedData[r.uid] = r.articles;
// if feed that is open changed, reload
if ((openedFeedPath !== undefined) && (r.fullName.slice(0, openedFeedPath.length) === openedFeedPath))
showRssFeed(r.fullName);
}
else {
// calculate read difference and update feed data
let readDifference = 0;
let readChanged = false;
for (let i = 0; i < r.articles.length; ++i) {
const oldRead = feedData[r.uid][i].isRead ? 1 : 0;
const newRead = r.articles[i].isRead ? 1 : 0;
feedData[r.uid][i].isRead = r.articles[i].isRead;
readDifference += oldRead - newRead;
if (readDifference !== 0)
readChanged = true;
}
// if read on article changed
if (readChanged) {
needsUpdate = true;
// find all items that contain this rss feed and add read difference
rssFeedTable.rows.filter((row) => r.fullName.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
.each((row) => row.full_data.unread += readDifference);
// if feed that is opened changed update dynamically
if ((openedFeedPath !== undefined) && (r.fullName.slice(0, openedFeedPath.length) === openedFeedPath)) {
for (let i = 0; i < r.articles.length; ++i) {
const matchingRow = rssArticleTable.rows.filter((row) => row.full_data.feedUid === r.uid)
.filter((row) => row.full_data.dataId === r.articles[i].id);
matchingRow[Object.keys(matchingRow)[0]].full_data.isRead = r.articles[i].isRead;
}
}
}
}
});
if (needsUpdate) {
rssFeedTable.updateTable(true);
rssArticleTable.updateTable(true);
}
}
else {
// full refresh
rssFeedTable.clear();
feedData = {};
pathByFeedId = new Map();
// Unread entry at top
rssFeedTable.updateRowData({
rowId: 0,
name: "QBT_TR(Unread)QBT_TR[CONTEXT=FeedListWidget]",
unread: 0,
status: "unread",
indentation: 0,
dataUid: "",
dataUrl: "",
dataPath: ""
});
let rowCount = 1;
for (const dataEntry of flattenedResp) {
if (dataEntry.isFolder) {
rssFeedTable.updateRowData({
rowId: rowCount,
name: dataEntry.name,
unread: 0,
status: "isFolder",
indentation: dataEntry.depth,
dataUid: "",
dataUrl: "",
dataPath: dataEntry.fullName
});
}
else {
let status = "default";
if (dataEntry.hasError)
status = "hasError";
if (dataEntry.isLoading)
status = "isLoading";
rssFeedTable.updateRowData({
rowId: rowCount,
name: dataEntry.name,
unread: 0,
status: status,
indentation: dataEntry.depth,
dataUid: dataEntry.uid,
dataUrl: dataEntry.url,
dataPath: dataEntry.fullName
});
// calculate number of unread
const numberOfUnread = dataEntry.articles.map((art) => !art.isRead).filter((v) => v).length;
// find all items that contain this rss feed and add unread count
rssFeedTable.rows.filter((row) => dataEntry.fullName.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
.each((row) => row.full_data.unread += numberOfUnread);
pathByFeedId.set(dataEntry.uid, dataEntry.fullName);
feedData[dataEntry.uid] = dataEntry.articles;
}
++rowCount;
}
rssFeedTable.updateTable(false);
rssFeedTable.updateIcons();
}
}
}).send();
};
const refreshFeed = (feedUid) => {
// set icon to loading
rssFeedTable.rows.forEach((row) => {
if (row.full_data.dataUid === feedUid)
row.full_data.status = "isLoading";
});
rssFeedTable.updateIcons();
new Request({
url: "api/v2/rss/refreshItem",
method: "post",
data: {
itemPath: pathByFeedId.get(feedUid)
},
onFailure: (response) => {
if (response.status === 409)
alert(response.responseText);
}
}).send();
};
const refreshAllFeeds = () => {
for (const feedEntry in feedData) {
if (!Object.hasOwn(feedData, feedEntry))
continue;
refreshFeed(feedEntry);
}
};
const moveItem = (oldPath) => {
new MochaUI.Window({
id: "renamePage",
title: "QBT_TR(Please choose a new name for this RSS feed)QBT_TR[CONTEXT=RSSWidget]",
loadMethod: "iframe",
contentURL: "rename_feed.html?oldPath=" + encodeURIComponent(oldPath),
scrollbars: false,
resizable: false,
maximizable: false,
width: 350,
height: 100
});
};
const removeItem = (paths) => {
const encodedPaths = paths.map((path) => encodeURIComponent(path));
new MochaUI.Window({
id: "confirmFeedDeletionPage",
title: "QBT_TR(Deletion confirmation)QBT_TR[CONTEXT=RSSWidget]",
loadMethod: "iframe",
contentURL: "confirmfeeddeletion.html?paths=" + encodeURIComponent(encodedPaths.join("|")),
scrollbars: false,
resizable: false,
maximizable: false,
width: 350,
height: 70
});
};
const markItemAsRead = (path) => {
// feed data mark as read
for (const feedID in feedData) {
if (pathByFeedId.get(feedID).slice(0, path.length) === path)
feedData[feedID].each((el) => el.isRead = true);
}
// mark rows as read
rssArticleTable.rows.each((el) => el.full_data.isRead = true);
// find all children and set unread count to 0
rssFeedTable.rows.filter((row) => (row.full_data.dataPath.slice(0, path.length) === path) && (path !== row.full_data.dataPath))
.each((row) => row.full_data.unread = 0);
// find selected row
let rowId, prevUnreadCount;
rssFeedTable.rows.forEach((row) => {
if (row.full_data.dataPath === path) {
rowId = row.full_data.rowId;
prevUnreadCount = row.full_data.unread;
}
});
// find all parents (and self) and subtract previous unread count
rssFeedTable.rows.filter((row) => path.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
.each((row) => row.full_data.unread -= prevUnreadCount);
rssArticleTable.updateTable(false);
rssFeedTable.updateTable(true);
// send request
new Request({
url: "api/v2/rss/markAsRead",
method: "post",
data: {
itemPath: path
},
onFailure: (response) => {
if (response.status === 409)
alert(response.responseText);
}
}).send();
};
const markArticleAsRead = (path, id) => {
// find row
let rowId, name, uid, unread;
rssFeedTable.rows.forEach((row) => {
if (row.full_data.dataPath === path) {
rowId = row.full_data.rowId;
name = row.full_data.dataPath;
uid = row.full_data.dataUid;
unread = row.full_data.unread;
}
});
// update feed data
let prevReadState = true;
feedData[uid].each((article) => {
if (article.id === id) {
prevReadState = article.isRead;
article.isRead = true;
}
});
if (!prevReadState) {
// find all items that contain this feed and subtract 1
rssFeedTable.rows.filter((row) => path.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
.each((row) => row.full_data.unread -= 1);
rssFeedTable.updateTable(true);
new Request({
url: "api/v2/rss/markAsRead",
method: "post",
data: {
itemPath: path,
articleId: id
},
onFailure: (response) => {
if (response.status === 409)
alert(response.responseText);
}
}).send();
}
};
const markSelectedAsRead = () => {
const selectedDatapaths = rssFeedTable.selectedRows
.map((sRow) => rssFeedTable.rows[sRow].full_data.dataPath);
// filter children
const reducedDatapaths = selectedDatapaths.filter((path) =>
selectedDatapaths.filter((innerPath) => path.slice(0, innerPath.length) === innerPath).length === 1
);
reducedDatapaths.each((path) => markItemAsRead(path));
};
const openRssDownloader = () => {
const id = "rssdownloaderpage";
new MochaUI.Window({
id: id,
title: "QBT_TR(Rss Downloader)QBT_TR[CONTEXT=AutomatedRssDownloader]",
loadMethod: "xhr",
contentURL: "views/rssDownloader.html",
maximizable: false,
width: loadWindowWidth(id, 800),
height: loadWindowHeight(id, 650),
onResize: () => {
saveWindowSize(id);
},
resizeLimit: {
"x": [800, 2500],
"y": [500, 2000]
}
});
};
return exports();
})();
Object.freeze(window.qBittorrent.Rss);
</script>