WebUI: Improve filter lists

This PR adds following improvements: 
* Remove unused tracker entries while processing sync data
* Take into account filter selection & terms when performing 'Start/stop/delete' context actions in filter lists
  Now, only filtered torrents will be affected by them, just like in the GUI.
* Provide better feedback when performing 'Start/stop/delete' context actions in filter lists
  Small improvement over GUI - now these actions will be disabled if it's not possible to use them.
* Add context menu to status filter list
* Fix error when toggling filter title
  Fixup for small bug introduced in https://github.com/qbittorrent/qBittorrent/pull/21269

PR #21438.
This commit is contained in:
skomerko
2024-10-12 07:40:18 +02:00
committed by GitHub
parent b1fd61af3a
commit 81509dfb65
9 changed files with 259 additions and 345 deletions

View File

@@ -641,12 +641,6 @@ window.addEventListener("DOMContentLoaded", () => {
trackerFilterList.appendChild(createLink(TRACKERS_ALL, "QBT_TR(All (%1))QBT_TR[CONTEXT=TrackerFiltersList]", torrentsTable.getRowSize()));
trackerFilterList.appendChild(createLink(TRACKERS_TRACKERLESS, "QBT_TR(Trackerless (%1))QBT_TR[CONTEXT=TrackerFiltersList]", trackerlessTorrentsCount));
// Remove unused trackers
for (const [key, { trackerTorrentMap }] of trackerList) {
if (trackerTorrentMap.size === 0)
trackerList.delete(key);
}
// Sort trackers by hostname
const sortedList = [];
trackerList.forEach(({ host, trackerTorrentMap }, hash) => {
@@ -815,8 +809,17 @@ window.addEventListener("DOMContentLoaded", () => {
const host = window.qBittorrent.Misc.getHost(tracker);
const hash = window.qBittorrent.Misc.genHash(host);
const trackerListEntry = trackerList.get(hash);
if (trackerListEntry)
if (trackerListEntry) {
trackerListEntry.trackerTorrentMap.delete(tracker);
// Remove unused trackers
if (trackerListEntry.trackerTorrentMap.size === 0) {
trackerList.delete(hash);
if (selectedTracker === hash) {
selectedTracker = TRACKERS_ALL;
LocalPreferences.set("selected_tracker", selectedTracker.toString());
}
}
}
}
updateTrackers = true;
}
@@ -1647,7 +1650,7 @@ window.addEventListener("DOMContentLoaded", () => {
return;
if (event.target.isContentEditable)
return;
deleteFN();
deleteSelectedTorrentsFN();
event.preventDefault();
},
"shift+delete": (event) => {
@@ -1655,7 +1658,7 @@ window.addEventListener("DOMContentLoaded", () => {
return;
if (event.target.isContentEditable)
return;
deleteFN(true);
deleteSelectedTorrentsFN(true);
event.preventDefault();
}
}

View File

@@ -34,6 +34,7 @@ window.qBittorrent.ContextMenu ??= (() => {
return {
ContextMenu: ContextMenu,
TorrentsTableContextMenu: TorrentsTableContextMenu,
StatusesFilterContextMenu: StatusesFilterContextMenu,
CategoriesFilterContextMenu: CategoriesFilterContextMenu,
TagsFilterContextMenu: TagsFilterContextMenu,
TrackersFilterContextMenu: TrackersFilterContextMenu,
@@ -300,6 +301,31 @@ window.qBittorrent.ContextMenu ??= (() => {
}
});
const FilterListContextMenu = new Class({
Extends: ContextMenu,
initialize: function(options) {
this.parent(options);
this.torrentObserver = new MutationObserver((records, observer) => {
this.updateTorrentActions();
});
},
startTorrentObserver: function() {
this.torrentObserver.observe(torrentsTable.tableBody, { childList: true });
},
stopTorrentObserver: function() {
this.torrentObserver.disconnect();
},
updateTorrentActions: function() {
const torrentsVisible = torrentsTable.tableBody.children.length > 0;
this.setEnabled("startTorrents", torrentsVisible)
.setEnabled("stopTorrents", torrentsVisible)
.setEnabled("deleteTorrents", torrentsVisible);
}
});
const TorrentsTableContextMenu = new Class({
Extends: ContextMenu,
@@ -574,8 +600,15 @@ window.qBittorrent.ContextMenu ??= (() => {
}
});
const StatusesFilterContextMenu = new Class({
Extends: FilterListContextMenu,
updateMenuItems: function() {
this.updateTorrentActions();
}
});
const CategoriesFilterContextMenu = new Class({
Extends: ContextMenu,
Extends: FilterListContextMenu,
updateMenuItems: function() {
const id = Number(this.options.element.id);
if ((id !== CATEGORIES_ALL) && (id !== CATEGORIES_UNCATEGORIZED)) {
@@ -591,28 +624,34 @@ window.qBittorrent.ContextMenu ??= (() => {
this.hideItem("deleteCategory");
this.hideItem("createSubcategory");
}
this.updateTorrentActions();
}
});
const TagsFilterContextMenu = new Class({
Extends: ContextMenu,
Extends: FilterListContextMenu,
updateMenuItems: function() {
const id = Number(this.options.element.id);
if ((id !== TAGS_ALL) && (id !== TAGS_UNTAGGED))
this.showItem("deleteTag");
else
this.hideItem("deleteTag");
this.updateTorrentActions();
}
});
const TrackersFilterContextMenu = new Class({
Extends: ContextMenu,
Extends: FilterListContextMenu,
updateMenuItems: function() {
const id = Number(this.options.element.id);
if ((id !== TRACKERS_ALL) && (id !== TRACKERS_TRACKERLESS))
this.showItem("deleteTracker");
else
this.hideItem("deleteTracker");
this.updateTorrentActions();
}
});

View File

@@ -1525,9 +1525,20 @@ window.qBittorrent.DynamicTable ??= (() => {
getFilteredTorrentsHashes: function(filterName, categoryHash, tagHash, trackerHash) {
const rowsHashes = [];
const useRegex = document.getElementById("torrentsFilterRegexBox").checked;
const filterText = document.getElementById("torrentsFilterInput").value.trim().toLowerCase();
let filterTerms;
try {
filterTerms = (filterText.length > 0)
? (useRegex ? new RegExp(filterText) : filterText.split(" "))
: null;
}
catch (e) { // SyntaxError: Invalid regex pattern
return filteredRows;
}
for (const row of this.rows.values()) {
if (this.applyFilter(row, filterName, categoryHash, tagHash, trackerHash, null))
if (this.applyFilter(row, filterName, categoryHash, tagHash, trackerHash, filterTerms))
rowsHashes.push(row["rowId"]);
}

View File

@@ -115,7 +115,7 @@ let setForceStartFN = function() {};
let globalDownloadLimitFN = function() {};
let StatisticsLinkFN = function() {};
let downloadLimitFN = function() {};
let deleteFN = function() {};
let deleteSelectedTorrentsFN = function() {};
let stopFN = function() {};
let startFN = function() {};
let autoTorrentManagementFN = function() {};
@@ -124,6 +124,9 @@ let reannounceFN = function() {};
let setLocationFN = function() {};
let renameFN = function() {};
let renameFilesFN = function() {};
let startVisibleTorrentsFN = function() {};
let stopVisibleTorrentsFN = function() {};
let deleteVisibleTorrentsFN = function() {};
let torrentNewCategoryFN = function() {};
let torrentSetCategoryFN = function() {};
let createCategoryFN = function() {};
@@ -131,21 +134,12 @@ let createSubcategoryFN = function() {};
let editCategoryFN = function() {};
let removeCategoryFN = function() {};
let deleteUnusedCategoriesFN = function() {};
let startTorrentsByCategoryFN = function() {};
let stopTorrentsByCategoryFN = function() {};
let deleteTorrentsByCategoryFN = function() {};
let torrentAddTagsFN = function() {};
let torrentSetTagsFN = function() {};
let torrentRemoveAllTagsFN = function() {};
let createTagFN = function() {};
let removeTagFN = function() {};
let deleteUnusedTagsFN = function() {};
let startTorrentsByTagFN = function() {};
let stopTorrentsByTagFN = function() {};
let deleteTorrentsByTagFN = function() {};
let startTorrentsByTrackerFN = function() {};
let stopTorrentsByTrackerFN = function() {};
let deleteTorrentsByTrackerFN = function() {};
let deleteTrackerFN = function() {};
let copyNameFN = function() {};
let copyInfohashFN = function(policy) {};
@@ -476,7 +470,7 @@ const initializeWindows = function() {
}
};
deleteFN = function(forceDeleteFiles = false) {
deleteSelectedTorrentsFN = function(forceDeleteFiles = false) {
const hashes = torrentsTable.selectedRowsIds();
if (hashes.length > 0) {
if (window.qBittorrent.Cache.preferences.get().confirm_torrent_deletion) {
@@ -510,6 +504,7 @@ const initializeWindows = function() {
onSuccess: function() {
torrentsTable.deselectAll();
updateMainData();
updatePropertiesPanel();
},
onFailure: function() {
alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]");
@@ -522,7 +517,7 @@ const initializeWindows = function() {
addClickEvent("delete", (e) => {
e.preventDefault();
e.stopPropagation();
deleteFN();
deleteSelectedTorrentsFN();
});
stopFN = function() {
@@ -705,6 +700,86 @@ const initializeWindows = function() {
}
};
startVisibleTorrentsFN = function() {
const hashes = torrentsTable.getFilteredTorrentsHashes(selectedStatus, selectedCategory, selectedTag, selectedTracker);
if (hashes.length > 0) {
new Request({
url: "api/v2/torrents/start",
method: "post",
data: {
hashes: hashes.join("|")
},
onSuccess: () => {
updateMainData();
updatePropertiesPanel();
},
onFailure: () => {
alert("QBT_TR(Unable to start torrents.)QBT_TR[CONTEXT=HttpServer]");
}
}).send();
}
};
stopVisibleTorrentsFN = function() {
const hashes = torrentsTable.getFilteredTorrentsHashes(selectedStatus, selectedCategory, selectedTag, selectedTracker);
if (hashes.length > 0) {
new Request({
url: "api/v2/torrents/stop",
method: "post",
data: {
hashes: hashes.join("|")
},
onSuccess: () => {
updateMainData();
updatePropertiesPanel();
},
onFailure: () => {
alert("QBT_TR(Unable to stop torrents.)QBT_TR[CONTEXT=HttpServer]");
}
}).send();
}
};
deleteVisibleTorrentsFN = function() {
const hashes = torrentsTable.getFilteredTorrentsHashes(selectedStatus, selectedCategory, selectedTag, selectedTracker);
if (hashes.length > 0) {
if (window.qBittorrent.Cache.preferences.get().confirm_torrent_deletion) {
new MochaUI.Modal({
...window.qBittorrent.Dialog.baseModalOptions,
id: "confirmDeletionPage",
title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]",
data: {
hashes: hashes,
isDeletingVisibleTorrents: true
},
contentURL: "views/confirmdeletion.html",
onContentLoaded: function(w) {
MochaUI.resizeWindow(w, { centered: true });
MochaUI.centerWindow(w);
}
});
}
else {
new Request({
url: "api/v2/torrents/delete",
method: "post",
data: {
hashes: hashes.join("|"),
deleteFiles: false,
},
onSuccess: () => {
torrentsTable.deselectAll();
updateMainData();
updatePropertiesPanel();
},
onFailure: () => {
alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]");
}
}).send();
}
}
};
torrentNewCategoryFN = function() {
const action = "set";
const hashes = torrentsTable.selectedRowsIds();
@@ -838,74 +913,6 @@ const initializeWindows = function() {
}).send();
};
startTorrentsByCategoryFN = function(categoryHash) {
const hashes = torrentsTable.getFilteredTorrentsHashes("all", categoryHash, TAGS_ALL, TRACKERS_ALL);
if (hashes.length) {
new Request({
url: "api/v2/torrents/start",
method: "post",
data: {
hashes: hashes.join("|")
}
}).send();
updateMainData();
}
};
stopTorrentsByCategoryFN = function(categoryHash) {
const hashes = torrentsTable.getFilteredTorrentsHashes("all", categoryHash, TAGS_ALL, TRACKERS_ALL);
if (hashes.length) {
new Request({
url: "api/v2/torrents/stop",
method: "post",
data: {
hashes: hashes.join("|")
}
}).send();
updateMainData();
}
};
deleteTorrentsByCategoryFN = function(categoryHash) {
const hashes = torrentsTable.getFilteredTorrentsHashes("all", categoryHash, TAGS_ALL, TRACKERS_ALL);
if (hashes.length > 0) {
if (window.qBittorrent.Cache.preferences.get().confirm_torrent_deletion) {
new MochaUI.Modal({
...window.qBittorrent.Dialog.baseModalOptions,
id: "confirmDeletionPage",
title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]",
data: { hashes: hashes },
contentURL: "views/confirmdeletion.html",
onContentLoaded: function(w) {
MochaUI.resizeWindow(w, { centered: true });
MochaUI.centerWindow(w);
},
onCloseComplete: function() {
// make sure overlay is properly hidden upon modal closing
document.getElementById("modalOverlay").style.display = "none";
}
});
}
else {
new Request({
url: "api/v2/torrents/delete",
method: "post",
data: {
hashes: hashes.join("|"),
deleteFiles: false,
},
onSuccess: function() {
torrentsTable.deselectAll();
updateMainData();
},
onFailure: function() {
alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]");
}
}).send();
}
}
};
torrentAddTagsFN = function() {
const action = "set";
const hashes = torrentsTable.selectedRowsIds();
@@ -1003,203 +1010,6 @@ const initializeWindows = function() {
setTagFilter(TAGS_ALL);
};
startTorrentsByTagFN = function(tagHash) {
const hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, tagHash, TRACKERS_ALL);
if (hashes.length) {
new Request({
url: "api/v2/torrents/start",
method: "post",
data: {
hashes: hashes.join("|")
}
}).send();
updateMainData();
}
};
stopTorrentsByTagFN = function(tagHash) {
const hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, tagHash, TRACKERS_ALL);
if (hashes.length) {
new Request({
url: "api/v2/torrents/stop",
method: "post",
data: {
hashes: hashes.join("|")
}
}).send();
updateMainData();
}
};
deleteTorrentsByTagFN = function(tagHash) {
const hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, tagHash, TRACKERS_ALL);
if (hashes.length > 0) {
if (window.qBittorrent.Cache.preferences.get().confirm_torrent_deletion) {
new MochaUI.Modal({
...window.qBittorrent.Dialog.baseModalOptions,
id: "confirmDeletionPage",
title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]",
data: { hashes: hashes },
contentURL: "views/confirmdeletion.html",
onContentLoaded: function(w) {
MochaUI.resizeWindow(w, { centered: true });
MochaUI.centerWindow(w);
},
onCloseComplete: function() {
// make sure overlay is properly hidden upon modal closing
document.getElementById("modalOverlay").style.display = "none";
}
});
}
else {
new Request({
url: "api/v2/torrents/delete",
method: "post",
data: {
hashes: hashes.join("|"),
deleteFiles: false,
},
onSuccess: function() {
torrentsTable.deselectAll();
updateMainData();
},
onFailure: function() {
alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]");
}
}).send();
}
}
};
startTorrentsByTrackerFN = function(trackerHash) {
const trackerHashInt = Number.parseInt(trackerHash, 10);
let hashes = [];
switch (trackerHashInt) {
case TRACKERS_ALL:
hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL);
break;
case TRACKERS_TRACKERLESS:
hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_TRACKERLESS);
break;
default: {
const uniqueTorrents = new Set();
for (const torrents of trackerList.get(trackerHashInt).trackerTorrentMap.values()) {
for (const torrent of torrents)
uniqueTorrents.add(torrent);
}
hashes = [...uniqueTorrents];
break;
}
}
if (hashes.length > 0) {
new Request({
url: "api/v2/torrents/start",
method: "post",
data: {
hashes: hashes.join("|")
}
}).send();
updateMainData();
}
};
stopTorrentsByTrackerFN = function(trackerHash) {
const trackerHashInt = Number.parseInt(trackerHash, 10);
let hashes = [];
switch (trackerHashInt) {
case TRACKERS_ALL:
hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL);
break;
case TRACKERS_TRACKERLESS:
hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_TRACKERLESS);
break;
default: {
const uniqueTorrents = new Set();
for (const torrents of trackerList.get(trackerHashInt).trackerTorrentMap.values()) {
for (const torrent of torrents)
uniqueTorrents.add(torrent);
}
hashes = [...uniqueTorrents];
break;
}
}
if (hashes.length) {
new Request({
url: "api/v2/torrents/stop",
method: "post",
data: {
hashes: hashes.join("|")
}
}).send();
updateMainData();
}
};
deleteTorrentsByTrackerFN = function(trackerHash) {
const trackerHashInt = Number.parseInt(trackerHash, 10);
let hashes = [];
switch (trackerHashInt) {
case TRACKERS_ALL:
hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL);
break;
case TRACKERS_TRACKERLESS:
hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_TRACKERLESS);
break;
default: {
const uniqueTorrents = new Set();
for (const torrents of trackerList.get(trackerHashInt).trackerTorrentMap.values()) {
for (const torrent of torrents)
uniqueTorrents.add(torrent);
}
hashes = [...uniqueTorrents];
break;
}
}
if (hashes.length > 0) {
if (window.qBittorrent.Cache.preferences.get().confirm_torrent_deletion) {
new MochaUI.Modal({
...window.qBittorrent.Dialog.baseModalOptions,
id: "confirmDeletionPage",
title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]",
data: {
hashes: hashes,
filterList: "tracker"
},
contentURL: "views/confirmdeletion.html",
onContentLoaded: function(w) {
MochaUI.resizeWindow(w, { centered: true });
MochaUI.centerWindow(w);
},
onCloseComplete: function() {
// make sure overlay is properly hidden upon modal closing
document.getElementById("modalOverlay").style.display = "none";
}
});
}
else {
new Request({
url: "api/v2/torrents/delete",
method: "post",
data: {
hashes: hashes.join("|"),
deleteFiles: false,
},
onSuccess: function() {
torrentsTable.deselectAll();
setTrackerFilter(TRACKERS_ALL);
updateMainData();
},
onFailure: function() {
alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]");
},
}).send();
}
}
};
deleteTrackerFN = function(trackerHash) {
const trackerHashInt = Number.parseInt(trackerHash, 10);
if ((trackerHashInt === TRACKERS_ALL) || (trackerHashInt === TRACKERS_TRACKERLESS))