mirror of
https://github.com/qbittorrent/qBittorrent.git
synced 2025-12-17 06:01:33 -06:00
* WebUI: prefer `classList.toggle()` over other pattern Addresses: https://github.com/qbittorrent/qBittorrent/pull/23231#discussion_r2328647152 * WebUI: prefer using built-in objects Constructor * WebUI: combine function calls PR #23261.
968 lines
41 KiB
HTML
968 lines
41 KiB
HTML
<style>
|
|
#rssView {
|
|
padding: 20px 20px 0 20px;
|
|
height: calc(100% - 20px);
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
#rssTopBar {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
#rssContentView {
|
|
width: 100%;
|
|
flex-grow: 1;
|
|
overflow: auto;
|
|
}
|
|
|
|
#rssFeedFixedHeaderDiv .dynamicTableHeader,
|
|
#rssArticleFixedHeaderDiv .dynamicTableHeader {
|
|
cursor: default;
|
|
}
|
|
|
|
.alignRight {
|
|
float: right;
|
|
}
|
|
|
|
.unreadArticle {
|
|
color: var(--color-text-blue);
|
|
}
|
|
|
|
#rssFetchingDisabled {
|
|
color: var(--color-text-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((100% - 21px) / 3);
|
|
border: none;
|
|
}
|
|
|
|
#rightRssColumn {
|
|
overflow: auto;
|
|
height: 100%;
|
|
}
|
|
|
|
#rssFeedTableDiv,
|
|
#rssArticleTableDiv {
|
|
height: calc(100vh - 180px);
|
|
}
|
|
|
|
#rssTorrentDetailsName {
|
|
flex-shrink: 0;
|
|
font-weight: bold;
|
|
background-color: var(--color-background-blue);
|
|
padding: 0;
|
|
color: var(--color-text-white);
|
|
}
|
|
|
|
#rssTorrentDetailsAuthor,
|
|
#rssTorrentDetailsDate,
|
|
#rssTorrentDetailsLink {
|
|
flex-shrink: 1;
|
|
background-color: var(--color-background-default);
|
|
}
|
|
|
|
#rssDetailsView {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
overflow: auto;
|
|
}
|
|
|
|
#rssButtonBar {
|
|
overflow: hidden;
|
|
height: 30px;
|
|
}
|
|
|
|
#rssButtonBar button {
|
|
padding: 3px 6px;
|
|
}
|
|
|
|
#rssButtonBar button img {
|
|
margin: 0 5px -3px 0;
|
|
}
|
|
|
|
#rssContentView table {
|
|
width: 100%;
|
|
}
|
|
|
|
#rssDescription {
|
|
flex-grow: 2;
|
|
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()">
|
|
<img alt="QBT_TR(New subscription)QBT_TR[CONTEXT=RSSWidget]" src="images/list-add.svg" width="16" height="16">QBT_TR(New subscription)QBT_TR[CONTEXT=RSSWidget]
|
|
</button>
|
|
<button type="button" id="markReadButton" onclick="qBittorrent.Rss.markSelectedAsRead()">
|
|
<img alt="QBT_TR(Mark items read)QBT_TR[CONTEXT=RSSWidget]" src="images/task-complete.svg" width="16" height="16">QBT_TR(Mark items read)QBT_TR[CONTEXT=RSSWidget]
|
|
</button>
|
|
<button type="button" id="updateAllButton" onclick="qBittorrent.Rss.refreshAllFeeds()">
|
|
<img alt="QBT_TR(Update all)QBT_TR[CONTEXT=RSSWidget]" src="images/view-refresh.svg" width="16" height="16">QBT_TR(Update all)QBT_TR[CONTEXT=RSSWidget]
|
|
</button>
|
|
<button type="button" id="rssDownloaderButton" class="alignRight" onclick="qBittorrent.Rss.openRssDownloader()">
|
|
<img alt="QBT_TR(RSS Downloader...)QBT_TR[CONTEXT=RSSWidget]" src="images/downloading.svg" width="16" height="16">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="#edit"><img src="images/edit-rename.svg" alt="QBT_TR(Edit feed URL...)QBT_TR[CONTEXT=RSSWidget]"> QBT_TR(Edit feed URL...)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)
|
|
document.getElementById("rssFetchingDisabled").classList.remove("invisible");
|
|
|
|
const rssFeedContextMenu = new window.qBittorrent.ContextMenu.RssFeedContextMenu({
|
|
targets: "#rssFeedTableDiv tbody tr",
|
|
menu: "rssFeedMenu",
|
|
actions: {
|
|
update: (el) => {
|
|
const feedsToUpdate = new Set();
|
|
for (const rowId of rssFeedTable.selectedRows) {
|
|
const selectedPath = rssFeedTable.getRow(rowId).full_data.dataPath;
|
|
for (const row of rssFeedTable.getRowValues()) {
|
|
if ((row.full_data.dataPath.slice(0, selectedPath.length) === selectedPath) && (row.full_data.dataUid !== ""))
|
|
feedsToUpdate.add(row);
|
|
}
|
|
}
|
|
for (const feed of feedsToUpdate)
|
|
refreshFeed(feed.full_data.dataUid);
|
|
},
|
|
markRead: markSelectedAsRead,
|
|
rename: (el) => {
|
|
moveItem(rssFeedTable.getRow(rssFeedTable.selectedRows[0]).full_data.dataPath);
|
|
},
|
|
edit: (el) => {
|
|
const data = rssFeedTable.getRow(rssFeedTable.selectedRows[0]).full_data;
|
|
editUrl(data.dataPath, data.dataUrl);
|
|
},
|
|
delete: (el) => {
|
|
const selectedDatapaths = rssFeedTable.selectedRows
|
|
.filter((rowID) => rowID !== "0")
|
|
.map((rowID) => rssFeedTable.getRow(rowID).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: 0,
|
|
y: -60
|
|
}
|
|
});
|
|
|
|
rssFeedContextMenu.addTarget(document.getElementById("rssFeedTableDiv"));
|
|
// deselect feed when clicking on empty part of table
|
|
document.getElementById("rssFeedTableDiv").addEventListener("click", (e) => {
|
|
rssFeedTable.deselectAll();
|
|
rssFeedTable.deselectRow();
|
|
});
|
|
document.getElementById("rssFeedTableDiv").addEventListener("contextmenu", (e) => {
|
|
if (e.target.nodeName === "DIV") {
|
|
rssFeedTable.deselectAll();
|
|
rssFeedTable.deselectRow();
|
|
rssFeedContextMenu.updateMenuItems();
|
|
}
|
|
});
|
|
|
|
document.getElementById("CopyFeedURL").addEventListener("click", async (event) => {
|
|
let joined = "";
|
|
for (const rowID of rssFeedTable.selectedRows) {
|
|
const row = rssFeedTable.getRow(rowID);
|
|
if (row.full_data.dataUid !== "")
|
|
joined += `${row.full_data.dataUrl}\n`;
|
|
}
|
|
await clipboardCopy(joined.slice(0, -1));
|
|
});
|
|
|
|
rssFeedTable.setup("rssFeedTableDiv", "rssFeedFixedHeaderDiv", rssFeedContextMenu);
|
|
|
|
const rssArticleContextMenu = new window.qBittorrent.ContextMenu.RssArticleContextMenu({
|
|
targets: "#rssArticleTableDiv tbody tr",
|
|
menu: "rssArticleMenu",
|
|
actions: {
|
|
Download: (el) => {
|
|
for (const rowID of rssArticleTable.selectedRows) {
|
|
const { name, torrentURL } = rssArticleTable.getRow(rowID).full_data;
|
|
window.qBittorrent.Client.createAddTorrentWindow(name, torrentURL);
|
|
}
|
|
},
|
|
OpenNews: (el) => {
|
|
for (const rowID of rssArticleTable.selectedRows)
|
|
window.open(rssArticleTable.getRow(rowID).full_data.link);
|
|
}
|
|
},
|
|
offsets: {
|
|
x: 0,
|
|
y: -60
|
|
}
|
|
});
|
|
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.getRow(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",
|
|
icon: "images/qbittorrent-tray.svg",
|
|
title: "QBT_TR(Please type a RSS feed URL)QBT_TR[CONTEXT=RSSWidget]",
|
|
loadMethod: "iframe",
|
|
contentURL: `newfeed.html?v=${CACHEID}&path=${encodeURIComponent(path)}`,
|
|
scrollbars: false,
|
|
resizable: false,
|
|
maximizable: false,
|
|
width: window.qBittorrent.Dialog.limitWidthToViewport(350),
|
|
height: 100
|
|
});
|
|
};
|
|
|
|
const addFolder = () => {
|
|
let path = "";
|
|
if (rssFeedTable.selectedRows.length !== 0) {
|
|
const row = rssFeedTable.getRow(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",
|
|
icon: "images/qbittorrent-tray.svg",
|
|
title: "QBT_TR(Please choose a folder name)QBT_TR[CONTEXT=RSSWidget]",
|
|
loadMethod: "iframe",
|
|
contentURL: `newfolder.html?v=${CACHEID}&path=${encodeURIComponent(path)}`,
|
|
scrollbars: false,
|
|
resizable: false,
|
|
maximizable: false,
|
|
width: window.qBittorrent.Dialog.limitWidthToViewport(350),
|
|
height: 100
|
|
});
|
|
};
|
|
|
|
const showRssFeed = (path) => {
|
|
rssArticleTable.clear();
|
|
|
|
const childFeeds = new Set();
|
|
for (const row of rssFeedTable.getRowValues()) {
|
|
if ((row.full_data.dataPath.slice(0, path.length) === path) && (row.full_data.dataUid !== ""))
|
|
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);
|
|
|
|
let rowID = -1;
|
|
visibleArticles.sort((e1, e2) => new Date(e2.date) - new Date(e1.date))
|
|
.each((torrentEntry) => {
|
|
rssArticleTable.updateRowData({
|
|
rowId: ++rowID,
|
|
name: torrentEntry.title,
|
|
link: torrentEntry.link,
|
|
torrentURL: torrentEntry.torrentURL,
|
|
feedUid: torrentEntry.feedUid,
|
|
dataId: torrentEntry.id,
|
|
isRead: torrentEntry.isRead
|
|
});
|
|
});
|
|
|
|
clearDetails();
|
|
rssArticleTable.updateTable(false);
|
|
};
|
|
|
|
const clearDetails = () => {
|
|
for (const el of [...document.getElementById("rssDetailsView").children])
|
|
el.remove();
|
|
};
|
|
|
|
const showDetails = (feedUid, articleID) => {
|
|
markArticleAsRead(pathByFeedId.get(feedUid), articleID);
|
|
clearDetails();
|
|
|
|
const article = feedData[feedUid].find((article) => (article.id === articleID));
|
|
if (article === undefined)
|
|
return;
|
|
|
|
const detailsView = document.getElementById("rssDetailsView");
|
|
|
|
const articleTitle = article.title;
|
|
if (articleTitle !== undefined) {
|
|
const torrentName = document.createElement("p");
|
|
torrentName.textContent = articleTitle;
|
|
torrentName.id = "rssTorrentDetailsName";
|
|
|
|
detailsView.append(torrentName);
|
|
}
|
|
|
|
const articleDate = article.date;
|
|
if (articleDate !== undefined) {
|
|
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(articleDate).toLocaleString();
|
|
torrentDate.append(torrentDateData);
|
|
|
|
detailsView.append(torrentDate);
|
|
}
|
|
|
|
const articleAuthor = article.author;
|
|
if (articleAuthor !== undefined) {
|
|
const divElement = document.createElement("div");
|
|
divElement.id = "rssTorrentDetailsAuthor";
|
|
|
|
const header = document.createElement("b");
|
|
header.textContent = "QBT_TR(Author: )QBT_TR[CONTEXT=RSSWidget]";
|
|
divElement.append(header);
|
|
|
|
const span = document.createElement("span");
|
|
span.textContent = articleAuthor;
|
|
divElement.append(span);
|
|
|
|
detailsView.append(divElement);
|
|
}
|
|
|
|
const articleLink = article.link;
|
|
if (articleLink !== undefined) {
|
|
const divElement = document.createElement("div");
|
|
divElement.id = "rssTorrentDetailsLink";
|
|
|
|
const anchorElement = document.createElement("a");
|
|
anchorElement.href = articleLink;
|
|
anchorElement.textContent = "QBT_TR(Open link)QBT_TR[CONTEXT=RSSWidget]";
|
|
anchorElement.target = "_blank";
|
|
anchorElement.style = "font-weight: bold;";
|
|
divElement.append(anchorElement);
|
|
|
|
detailsView.append(divElement);
|
|
}
|
|
|
|
const articleDescription = article.description;
|
|
if (articleDescription !== undefined) {
|
|
const rootColor = document.documentElement.classList.contains("dark") ? "class='dark'" : "";
|
|
|
|
// Place in iframe with sandbox attribute to prevent js execution
|
|
const iframeElement = document.createElement("iframe");
|
|
iframeElement.id = "rssDescription";
|
|
iframeElement.sandbox = "allow-same-origin"; // allowed to get parent css
|
|
iframeElement.srcdoc = `<html ${rootColor}><head><meta charset="utf-8"><link rel="stylesheet" type="text/css" href="css/style.css?v=${CACHEID}"></head><body>${articleDescription}</body></html>`;
|
|
|
|
detailsView.append(iframeElement);
|
|
}
|
|
};
|
|
|
|
const updateRssFeedList = () => {
|
|
const url = new URL("api/v2/rss/items", window.location);
|
|
url.search = new URLSearchParams({
|
|
withData: true
|
|
});
|
|
fetch(url, {
|
|
method: "GET",
|
|
cache: "no-store"
|
|
})
|
|
.then(async (response) => {
|
|
if (!response.ok)
|
|
return;
|
|
|
|
const responseJSON = await response.json();
|
|
|
|
// 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(responseJSON);
|
|
|
|
// check if rows matches flattened response
|
|
const rssFeedRows = [...rssFeedTable.getRowValues()];
|
|
let match = false;
|
|
// subtract 'unread' row
|
|
if ((rssFeedRows.length - 1) === flattenedResp.length) {
|
|
match = true;
|
|
for (const [i, newData] of flattenedResp.entries()) {
|
|
const existingData = rssFeedRows[i + 1].full_data;
|
|
|
|
if (newData.fullName !== existingData.dataPath) {
|
|
match = false;
|
|
break;
|
|
}
|
|
// only non-folders have these properties
|
|
if (!newData.isFolder && ((newData.uid !== existingData.dataUid) || (newData.url !== existingData.dataUrl))) {
|
|
match = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (match) {
|
|
// partial refresh
|
|
// update status
|
|
let statusDiffers = false;
|
|
for (const [i, item] of flattenedResp.entries()) {
|
|
const oldStatus = rssFeedRows[i + 1].full_data.status;
|
|
let status = "default";
|
|
if (item.hasError)
|
|
status = "hasError";
|
|
if (item.isLoading)
|
|
status = "isLoading";
|
|
if (item.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.at(-1);
|
|
openedFeedPath = rssFeedTable.getRow(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(Boolean).length;
|
|
const newUnread = r.articles.map((art) => !art.isRead).filter(Boolean).length;
|
|
const unreadDifference = newUnread - oldUnread;
|
|
|
|
// find all parents (and self) and add unread difference
|
|
for (const row of rssFeedTable.getRowValues()) {
|
|
if (r.fullName.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
|
|
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
|
|
for (const row of rssFeedTable.getRowValues()) {
|
|
if (r.fullName.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
|
|
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) {
|
|
for (const row of rssArticleTable.getRowValues()) {
|
|
if ((row.full_data.feedUid === r.uid) && (row.full_data.dataId === r.articles[i].id)) {
|
|
row.full_data.isRead = r.articles[i].isRead;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
if (needsUpdate) {
|
|
rssFeedTable.updateTable(true);
|
|
rssArticleTable.updateTable(true);
|
|
}
|
|
|
|
}
|
|
else {
|
|
// full refresh
|
|
rssFeedTable.clear();
|
|
rssArticleTable.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(Boolean).length;
|
|
// find all items that contain this rss feed and add unread count
|
|
for (const row of rssFeedTable.getRowValues()) {
|
|
if (dataEntry.fullName.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
|
|
row.full_data.unread += numberOfUnread;
|
|
}
|
|
|
|
pathByFeedId.set(dataEntry.uid, dataEntry.fullName);
|
|
feedData[dataEntry.uid] = dataEntry.articles;
|
|
}
|
|
++rowCount;
|
|
}
|
|
rssFeedTable.updateTable(false);
|
|
rssFeedTable.updateIcons();
|
|
}
|
|
});
|
|
};
|
|
|
|
const refreshFeed = (feedUid) => {
|
|
// set icon to loading
|
|
for (const row of rssFeedTable.getRowValues()) {
|
|
if (row.full_data.dataUid === feedUid)
|
|
row.full_data.status = "isLoading";
|
|
}
|
|
rssFeedTable.updateIcons();
|
|
|
|
fetch("api/v2/rss/refreshItem", {
|
|
method: "POST",
|
|
body: new URLSearchParams({
|
|
itemPath: pathByFeedId.get(feedUid)
|
|
})
|
|
})
|
|
.then(async (response) => {
|
|
if (!response.ok) {
|
|
if (response.status === 409)
|
|
alert(await response.text());
|
|
return;
|
|
}
|
|
});
|
|
};
|
|
|
|
const refreshAllFeeds = () => {
|
|
for (const feedEntry in feedData) {
|
|
if (!Object.hasOwn(feedData, feedEntry))
|
|
continue;
|
|
refreshFeed(feedEntry);
|
|
}
|
|
};
|
|
|
|
const moveItem = (oldPath) => {
|
|
new MochaUI.Window({
|
|
id: "renamePage",
|
|
icon: "images/qbittorrent-tray.svg",
|
|
title: "QBT_TR(Please choose a new name for this RSS feed)QBT_TR[CONTEXT=RSSWidget]",
|
|
loadMethod: "iframe",
|
|
contentURL: `rename_feed.html?v=${CACHEID}&oldPath=${encodeURIComponent(oldPath)}`,
|
|
scrollbars: false,
|
|
resizable: false,
|
|
maximizable: false,
|
|
width: window.qBittorrent.Dialog.limitWidthToViewport(350),
|
|
height: 100
|
|
});
|
|
};
|
|
|
|
const editUrl = (path, url) => {
|
|
const contentURL = new URL("editfeedurl.html", window.location);
|
|
contentURL.search = new URLSearchParams({
|
|
v: "${CACHEID}",
|
|
path: path,
|
|
url: url
|
|
});
|
|
new MochaUI.Window({
|
|
id: "editFeedURL",
|
|
icon: "images/qbittorrent-tray.svg",
|
|
title: "QBT_TR(Please type a RSS feed URL)QBT_TR[CONTEXT=RSSWidget]",
|
|
loadMethod: "iframe",
|
|
contentURL: contentURL.toString(),
|
|
scrollbars: false,
|
|
resizable: false,
|
|
maximizable: false,
|
|
width: window.qBittorrent.Dialog.limitWidthToViewport(350),
|
|
height: 100
|
|
});
|
|
};
|
|
|
|
const removeItem = (paths) => {
|
|
const encodedPaths = paths.map((path) => encodeURIComponent(path));
|
|
new MochaUI.Window({
|
|
id: "confirmFeedDeletionPage",
|
|
icon: "images/qbittorrent-tray.svg",
|
|
title: "QBT_TR(Deletion confirmation)QBT_TR[CONTEXT=RSSWidget]",
|
|
loadMethod: "iframe",
|
|
contentURL: `confirmfeeddeletion.html?v=${CACHEID}&paths=${encodeURIComponent(encodedPaths.join("|"))}`,
|
|
scrollbars: false,
|
|
resizable: false,
|
|
maximizable: false,
|
|
width: window.qBittorrent.Dialog.limitWidthToViewport(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
|
|
for (const row of rssArticleTable.getRowValues())
|
|
row.full_data.isRead = true;
|
|
|
|
// find all children and set unread count to 0
|
|
for (const row of rssFeedTable.getRowValues()) {
|
|
if ((row.full_data.dataPath.slice(0, path.length) === path) && (path !== row.full_data.dataPath))
|
|
row.full_data.unread = 0;
|
|
}
|
|
|
|
// find selected row
|
|
let prevUnreadCount;
|
|
for (const row of rssFeedTable.getRowValues()) {
|
|
if (row.full_data.dataPath === path) {
|
|
prevUnreadCount = row.full_data.unread;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// find all parents (and self) and subtract previous unread count
|
|
for (const row of rssFeedTable.getRowValues()) {
|
|
if (path.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
|
|
row.full_data.unread -= prevUnreadCount;
|
|
}
|
|
|
|
rssArticleTable.updateTable(false);
|
|
rssFeedTable.updateTable(true);
|
|
|
|
// send request
|
|
fetch("api/v2/rss/markAsRead", {
|
|
method: "POST",
|
|
body: new URLSearchParams({
|
|
itemPath: path
|
|
})
|
|
})
|
|
.then(async (response) => {
|
|
if (!response.ok) {
|
|
if (response.status === 409)
|
|
alert(await response.text());
|
|
return;
|
|
}
|
|
});
|
|
};
|
|
|
|
const markArticleAsRead = (path, id) => {
|
|
// find row
|
|
let uid;
|
|
for (const row of rssFeedTable.getRowValues()) {
|
|
if (row.full_data.dataPath === path) {
|
|
uid = row.full_data.dataUid;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 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
|
|
for (const row of rssFeedTable.getRowValues()) {
|
|
if (path.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
|
|
row.full_data.unread -= 1;
|
|
}
|
|
|
|
rssFeedTable.updateTable(true);
|
|
|
|
fetch("api/v2/rss/markAsRead", {
|
|
method: "POST",
|
|
body: new URLSearchParams({
|
|
itemPath: path,
|
|
articleId: id
|
|
})
|
|
})
|
|
.then(async (response) => {
|
|
if (!response.ok) {
|
|
if (response.status === 409)
|
|
alert(await response.text());
|
|
return;
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const markSelectedAsRead = () => {
|
|
const selectedDatapaths = rssFeedTable.selectedRows
|
|
.map((sRow) => rssFeedTable.getRow(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,
|
|
icon: "images/qbittorrent-tray.svg",
|
|
title: "QBT_TR(Rss Downloader)QBT_TR[CONTEXT=AutomatedRssDownloader]",
|
|
loadMethod: "xhr",
|
|
contentURL: "views/rssDownloader.html?v=${CACHEID}",
|
|
maximizable: false,
|
|
width: loadWindowWidth(id, 800, false),
|
|
height: loadWindowHeight(id, 650, false),
|
|
onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
|
|
saveWindowSize(id);
|
|
}),
|
|
resizeLimit: {
|
|
x: [800, 2500],
|
|
y: [500, 2000]
|
|
}
|
|
});
|
|
};
|
|
|
|
return exports();
|
|
})();
|
|
Object.freeze(window.qBittorrent.Rss);
|
|
</script>
|