WebUI: Implement missing tracker list features

This commit is contained in:
tehcneko 2025-07-31 14:23:32 +01:00
commit 4b0bef041a
6 changed files with 241 additions and 43 deletions

View file

@ -1,5 +1,12 @@
# WebAPI Changelog # WebAPI Changelog
## 2.13.0
* [#23045](https://github.com/qbittorrent/qBittorrent/pull/23045)
* `torrents/trackers` returns three new fields: `next_announce`, `min_announce` and `endpoints`
* `endpoints` is an array of tracker endpoints, each with `name`, `updating`, `status`, `msg`, `bt_version`, `num_peers`, `num_peers`, `num_leeches`, `num_downloaded`, `next_announce` and `min_announce` fields
* `torrents/trackers` now returns `5` and `6` in `status` field as possible values
* `5` for `Tracker error` and `6` for `Unreachable`
## 2.12.1 ## 2.12.1
* [#23031](https://github.com/qbittorrent/qBittorrent/pull/23031) * [#23031](https://github.com/qbittorrent/qBittorrent/pull/23031)
* Add `torrents/setComment` endpoint with parameters `hashes` and `comment` for setting a new torrent comment * Add `torrents/setComment` endpoint with parameters `hashes` and `comment` for setting a new torrent comment

View file

@ -28,6 +28,8 @@
#include "torrentscontroller.h" #include "torrentscontroller.h"
#include <algorithm>
#include <chrono>
#include <concepts> #include <concepts>
#include <functional> #include <functional>
@ -274,7 +276,12 @@ namespace
QJsonArray getTrackers(const BitTorrent::Torrent *const torrent) QJsonArray getTrackers(const BitTorrent::Torrent *const torrent)
{ {
auto now = BitTorrent::AnnounceTimePoint::clock::now(); const auto toSeconds = [](const std::chrono::nanoseconds &duration) -> qint64
{
return std::max<qint64>(0, std::chrono::duration_cast<std::chrono::seconds>(duration).count());
};
const auto now = BitTorrent::AnnounceTimePoint::clock::now();
QJsonArray trackerList; QJsonArray trackerList;
for (const BitTorrent::TrackerEntryStatus &tracker : asConst(torrent->trackers())) for (const BitTorrent::TrackerEntryStatus &tracker : asConst(torrent->trackers()))
@ -294,10 +301,8 @@ namespace
{KEY_TRACKER_SEEDS_COUNT, endpoint.numSeeds}, {KEY_TRACKER_SEEDS_COUNT, endpoint.numSeeds},
{KEY_TRACKER_LEECHES_COUNT, endpoint.numLeeches}, {KEY_TRACKER_LEECHES_COUNT, endpoint.numLeeches},
{KEY_TRACKER_DOWNLOADED_COUNT, endpoint.numDownloaded}, {KEY_TRACKER_DOWNLOADED_COUNT, endpoint.numDownloaded},
{KEY_TRACKER_NEXT_ANNOUNCE, std::max<qint64>(0, {KEY_TRACKER_NEXT_ANNOUNCE, toSeconds(endpoint.nextAnnounceTime - now)},
std::chrono::duration_cast<std::chrono::seconds>(endpoint.nextAnnounceTime - now).count())}, {KEY_TRACKER_MIN_ANNOUNCE, toSeconds(endpoint.minAnnounceTime - now)}
{KEY_TRACKER_MIN_ANNOUNCE, std::max<qint64>(0,
std::chrono::duration_cast<std::chrono::seconds>(endpoint.minAnnounceTime - now).count())}
}; };
} }
@ -312,10 +317,8 @@ namespace
{KEY_TRACKER_SEEDS_COUNT, tracker.numSeeds}, {KEY_TRACKER_SEEDS_COUNT, tracker.numSeeds},
{KEY_TRACKER_LEECHES_COUNT, tracker.numLeeches}, {KEY_TRACKER_LEECHES_COUNT, tracker.numLeeches},
{KEY_TRACKER_DOWNLOADED_COUNT, tracker.numDownloaded}, {KEY_TRACKER_DOWNLOADED_COUNT, tracker.numDownloaded},
{KEY_TRACKER_NEXT_ANNOUNCE, std::max<qint64>(0, {KEY_TRACKER_NEXT_ANNOUNCE, toSeconds(tracker.nextAnnounceTime - now)},
std::chrono::duration_cast<std::chrono::seconds>(tracker.nextAnnounceTime - now).count())}, {KEY_TRACKER_MIN_ANNOUNCE, toSeconds(tracker.minAnnounceTime - now)},
{KEY_TRACKER_MIN_ANNOUNCE, std::max<qint64>(0,
std::chrono::duration_cast<std::chrono::seconds>(tracker.minAnnounceTime - now).count())},
{KEY_TRACKER_ENDPOINTS, endpointsList} {KEY_TRACKER_ENDPOINTS, endpointsList}
}; };
} }

View file

@ -53,7 +53,7 @@
#include "base/utils/version.h" #include "base/utils/version.h"
#include "api/isessionmanager.h" #include "api/isessionmanager.h"
inline const Utils::Version<3, 2> API_VERSION {2, 12, 1}; inline const Utils::Version<3, 2> API_VERSION {2, 13, 0};
class APIController; class APIController;
class AuthController; class AuthController;

View file

@ -245,11 +245,11 @@
</ul> </ul>
<ul id="torrentTrackersMenu" class="contextMenu"> <ul id="torrentTrackersMenu" class="contextMenu">
<li><a href="#AddTracker"><img src="images/list-add.svg" alt="QBT_TR(Add trackers...)QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Add trackers...)QBT_TR[CONTEXT=TrackerListWidget]</a></li> <li><a href="#AddTracker"><img src="images/list-add.svg" alt="QBT_TR(Add trackers...)QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Add trackers...)QBT_TR[CONTEXT=TrackerListWidget]</a></li>
<li class="separator"><a href="#EditTracker"><img src="images/edit-rename.svg" alt="QBT_TR(Edit tracker URL...)QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Edit tracker URL...)QBT_TR[CONTEXT=TrackerListWidget]</a></li> <li><a href="#EditTracker"><img src="images/edit-rename.svg" alt="QBT_TR(Edit tracker URL...)QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Edit tracker URL...)QBT_TR[CONTEXT=TrackerListWidget]</a></li>
<li><a href="#RemoveTracker"><img src="images/list-remove.svg" alt="QBT_TR(Remove tracker)QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Remove tracker)QBT_TR[CONTEXT=TrackerListWidget]</a></li> <li><a href="#RemoveTracker"><img src="images/list-remove.svg" alt="QBT_TR(Remove tracker)QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Remove tracker)QBT_TR[CONTEXT=TrackerListWidget]</a></li>
<li><a href="#CopyTrackerUrl" id="CopyTrackerUrl"><img src="images/edit-copy.svg" alt="QBT_TR(Copy tracker URL)QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Copy tracker URL)QBT_TR[CONTEXT=TrackerListWidget]</a></li> <li><a href="#CopyTrackerUrl" id="CopyTrackerUrl"><img src="images/edit-copy.svg" alt="QBT_TR(Copy tracker URL)QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Copy tracker URL)QBT_TR[CONTEXT=TrackerListWidget]</a></li>
<li><a href="#ReannounceTrackers" id="ReannounceTrackers"><img src="images/view-refresh.svg" alt="QBT_TR(Copy tracker URL)QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Force reannounce to selected tracker(s))QBT_TR[CONTEXT=TrackerListWidget]</a></li> <li><a href="#ReannounceTrackers" id="ReannounceTrackers"><img src="images/reannounce.svg" alt="QBT_TR(Force reannounce to selected tracker(s))QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Force reannounce to selected tracker(s))QBT_TR[CONTEXT=TrackerListWidget]</a></li>
<li class="separator"><a href="#ReannounceAllTrackers" id="ReannounceAllTrackers"><img src="images/view-refresh.svg" alt="QBT_TR(Copy tracker URL)QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Force reannounce to all trackers)QBT_TR[CONTEXT=TrackerListWidget]</a></li> <li class="separator"><a href="#ReannounceAllTrackers" id="ReannounceAllTrackers"><img src="images/reannounce.svg" alt="QBT_TR(Force reannounce to all trackers)QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Force reannounce to all trackers)QBT_TR[CONTEXT=TrackerListWidget]</a></li>
</ul> </ul>
<ul id="torrentPeersMenu" class="contextMenu"> <ul id="torrentPeersMenu" class="contextMenu">
<li><a href="#addPeer"><img src="images/peers-add.svg" alt="QBT_TR(Add peers...)QBT_TR[CONTEXT=PeerListWidget]"> QBT_TR(Add peers...)QBT_TR[CONTEXT=PeerListWidget]</a></li> <li><a href="#addPeer"><img src="images/peers-add.svg" alt="QBT_TR(Add peers...)QBT_TR[CONTEXT=PeerListWidget]"> QBT_TR(Add peers...)QBT_TR[CONTEXT=PeerListWidget]</a></li>

View file

@ -2074,15 +2074,68 @@ window.qBittorrent.DynamicTable ??= (() => {
} }
class TorrentTrackersTable extends DynamicTable { class TorrentTrackersTable extends DynamicTable {
collapseState = new Map(); // { rowId: String, isCollapsed: bool }
isTrackerCollapsed(id) {
return this.collapseState.get(id) ?? true;
}
toggleTrackerCollapsed(id) {
this.collapseState.set(id, !this.isTrackerCollapsed(id));
this.#updateTrackerRowState(id, this.isTrackerCollapsed(id));
}
#updateEndpointVisibility(endpoint, shouldHide) {
const span = document.getElementById(`trackersTableTrackerUrl${endpoint}`);
// span won't exist if row has been filtered out
if (span === null)
return;
const tr = span.parentElement.parentElement;
tr.classList.toggle("invisible", shouldHide);
}
#updateTrackerCollapseIcon(tracker, isCollapsed) {
const span = document.getElementById(`trackersTableTrackerUrl${tracker}`);
// span won't exist if row has been filtered out
if (span === null)
return;
const td = span.parentElement;
// rotate the collapse icon
const collapseIcon = td.firstElementChild;
collapseIcon.classList.toggle("rotate", isCollapsed);
}
#updateTrackerRowState(id, shouldCollapse) {
// collapsed rows will be filtered out when using virtual list
if (this.useVirtualList)
return;
this.#updateTrackerCollapseIcon(id, shouldCollapse);
for (const row of this.getRowValues()) {
const parentId = row.full_data._tracker;
if (parentId === id)
this.#updateEndpointVisibility(row.rowId, shouldCollapse);
}
}
clearCollapseState() {
this.collapseState.clear();
}
initColumns() { initColumns() {
this.newColumn("url", "", "QBT_TR(URL/Announce Endpoint)QBT_TR[CONTEXT=TrackerListWidget]", 250, true);
this.newColumn("tier", "", "QBT_TR(Tier)QBT_TR[CONTEXT=TrackerListWidget]", 35, true); this.newColumn("tier", "", "QBT_TR(Tier)QBT_TR[CONTEXT=TrackerListWidget]", 35, true);
this.newColumn("url", "", "QBT_TR(URL)QBT_TR[CONTEXT=TrackerListWidget]", 250, true); this.newColumn("btVersion", "", "QBT_TR(BT Protocol)QBT_TR[CONTEXT=TrackerListWidget]", 35, true);
this.newColumn("status", "", "QBT_TR(Status)QBT_TR[CONTEXT=TrackerListWidget]", 125, true); this.newColumn("status", "", "QBT_TR(Status)QBT_TR[CONTEXT=TrackerListWidget]", 125, true);
this.newColumn("peers", "", "QBT_TR(Peers)QBT_TR[CONTEXT=TrackerListWidget]", 75, true); this.newColumn("peers", "", "QBT_TR(Peers)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
this.newColumn("seeds", "", "QBT_TR(Seeds)QBT_TR[CONTEXT=TrackerListWidget]", 75, true); this.newColumn("seeds", "", "QBT_TR(Seeds)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
this.newColumn("leeches", "", "QBT_TR(Leeches)QBT_TR[CONTEXT=TrackerListWidget]", 75, true); this.newColumn("leeches", "", "QBT_TR(Leeches)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
this.newColumn("downloaded", "", "QBT_TR(Times Downloaded)QBT_TR[CONTEXT=TrackerListWidget]", 100, true); this.newColumn("downloaded", "", "QBT_TR(Times Downloaded)QBT_TR[CONTEXT=TrackerListWidget]", 100, true);
this.newColumn("message", "", "QBT_TR(Message)QBT_TR[CONTEXT=TrackerListWidget]", 250, true); this.newColumn("message", "", "QBT_TR(Message)QBT_TR[CONTEXT=TrackerListWidget]", 250, true);
this.newColumn("nextAnnounce", "", "QBT_TR(Next Announce)QBT_TR[CONTEXT=TrackerListWidget]", 150, true);
this.newColumn("minAnnounce", "", "QBT_TR(Min Announce)QBT_TR[CONTEXT=TrackerListWidget]", 150, true);
this.initColumnsFunctions(); this.initColumnsFunctions();
} }
@ -2097,6 +2150,43 @@ window.qBittorrent.DynamicTable ??= (() => {
return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2); return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
}; };
this.columns["url"].updateTd = (td, row) => {
const id = row.rowId;
const data = row.full_data;
let collapseIcon = td.firstElementChild;
if (collapseIcon === null) {
collapseIcon = document.createElement("img");
collapseIcon.src = "images/go-down.svg";
collapseIcon.className = "filesTableCollapseIcon";
collapseIcon.addEventListener("click", (e) => {
const id = collapseIcon.dataset.id;
this.toggleTrackerCollapsed(id);
if (this.useVirtualList)
this.rerender();
});
td.append(collapseIcon);
}
if (data._isTracker) {
collapseIcon.style.display = "inline";
collapseIcon.style.visibility = data._hasEndpoints ? "visible" : "hidden";
collapseIcon.dataset.id = id;
collapseIcon.classList.toggle("rotate", this.isTrackerCollapsed(id));
}
else {
collapseIcon.style.display = "none";
}
let span = td.children[1];
if (span === undefined) {
span = document.createElement("span");
td.append(span);
}
span.id = `trackersTableTrackerUrl${id}`;
span.textContent = data.url;
span.style.marginLeft = data._isTracker ? "0" : "20px";
};
this.columns["url"].compareRows = naturalSort; this.columns["url"].compareRows = naturalSort;
this.columns["status"].compareRows = naturalSort; this.columns["status"].compareRows = naturalSort;
this.columns["message"].compareRows = naturalSort; this.columns["message"].compareRows = naturalSort;
@ -2151,6 +2241,8 @@ window.qBittorrent.DynamicTable ??= (() => {
statusClass = "trackerUpdating"; statusClass = "trackerUpdating";
break; break;
case "QBT_TR(Not working)QBT_TR[CONTEXT=TrackerListWidget]": case "QBT_TR(Not working)QBT_TR[CONTEXT=TrackerListWidget]":
case "QBT_TR(Tracker error)QBT_TR[CONTEXT=TrackerListWidget]":
case "QBT_TR(Unreachable)QBT_TR[CONTEXT=TrackerListWidget]":
statusClass = "trackerNotWorking"; statusClass = "trackerNotWorking";
break; break;
} }
@ -2163,6 +2255,74 @@ window.qBittorrent.DynamicTable ??= (() => {
td.textContent = status; td.textContent = status;
td.title = status; td.title = status;
}; };
const friendlyDuration = function(td, row) {
const duration = window.qBittorrent.Misc.friendlyDuration(this.getRowValue(row) ?? 0, window.qBittorrent.Misc.MAX_ETA);
td.textContent = duration;
td.title = duration;
};
this.columns["nextAnnounce"].updateTd = friendlyDuration;
this.columns["minAnnounce"].updateTd = friendlyDuration;
}
getFilteredAndSortedRows() {
const trackers = [];
const trakcerEndpoints = new Map();
for (const row of this.getRowValues()) {
const tracker = row.full_data._tracker;
if (tracker) {
if (this.useVirtualList && this.isTrackerCollapsed(tracker))
continue;
const endpoints = trakcerEndpoints.get(tracker);
if (endpoints === undefined)
trakcerEndpoints.set(tracker, [row]);
else
endpoints.push(row);
}
else {
trackers.push(row);
}
}
const column = this.columns[this.sortedColumn];
const isReverseSort = this.reverseSort === "0";
const sortRows = (row1, row2) => {
const result = column.compareRows(row1, row2);
return isReverseSort ? result : -result;
};
const result = [];
for (const tracker of trackers.sort(sortRows)) {
result.push(tracker);
const endpoints = trakcerEndpoints.get(tracker.rowId) || [];
result.push(...endpoints.sort(sortRows));
}
return result;
}
updateTable(fullUpdate = false) {
super.updateTable(fullUpdate);
if (!this.useVirtualList) {
for (const row of this.getRowValues()) {
if (row.full_data._isTracker)
continue;
this.#updateEndpointVisibility(row.rowId, this.isTrackerCollapsed(row.full_data._tracker));
}
}
}
setupCommonEvents() {
super.setupCommonEvents();
this.dynamicTableDiv.addEventListener("dblclick", (e) => {
const tr = e.target.closest("tr");
if (!tr || (tr.rowId.startsWith("** [") || tr.rowId.startsWith("endpoint|")))
return;
window.qBittorrent.PropTrackers.editTracker(tr);
});
} }
} }

View file

@ -32,6 +32,7 @@ window.qBittorrent ??= {};
window.qBittorrent.PropTrackers ??= (() => { window.qBittorrent.PropTrackers ??= (() => {
const exports = () => { const exports = () => {
return { return {
editTracker: editTrackerFN,
updateData: updateData, updateData: updateData,
clear: clear clear: clear
}; };
@ -42,6 +43,25 @@ window.qBittorrent.PropTrackers ??= (() => {
const torrentTrackersTable = new window.qBittorrent.DynamicTable.TorrentTrackersTable(); const torrentTrackersTable = new window.qBittorrent.DynamicTable.TorrentTrackersTable();
let loadTrackersDataTimer = -1; let loadTrackersDataTimer = -1;
const trackerStatusText = (tracker) => {
if (tracker.updating)
return "QBT_TR(Updating...)QBT_TR[CONTEXT=TrackerListWidget]";
switch (tracker.status) {
case 0:
return "QBT_TR(Disabled)QBT_TR[CONTEXT=TrackerListWidget]";
case 1:
return "QBT_TR(Not contacted yet)QBT_TR[CONTEXT=TrackerListWidget]";
case 2:
return "QBT_TR(Working)QBT_TR[CONTEXT=TrackerListWidget]";
case 4:
return "QBT_TR(Not working)QBT_TR[CONTEXT=TrackerListWidget]";
case 5:
return "QBT_TR(Tracker error)QBT_TR[CONTEXT=TrackerListWidget]";
case 6:
return "QBT_TR(Unreachable)QBT_TR[CONTEXT=TrackerListWidget]";
}
};
const loadTrackersData = () => { const loadTrackersData = () => {
if (document.hidden) if (document.hidden)
return; return;
@ -53,11 +73,13 @@ window.qBittorrent.PropTrackers ??= (() => {
const new_hash = torrentsTable.getCurrentTorrentID(); const new_hash = torrentsTable.getCurrentTorrentID();
if (new_hash === "") { if (new_hash === "") {
torrentTrackersTable.clear(); torrentTrackersTable.clear();
torrentTrackersTable.clearCollapseState();
clearTimeout(loadTrackersDataTimer); clearTimeout(loadTrackersDataTimer);
return; return;
} }
if (new_hash !== current_hash) { if (new_hash !== current_hash) {
torrentTrackersTable.clear(); torrentTrackersTable.clear();
torrentTrackersTable.clearCollapseState();
current_hash = new_hash; current_hash = new_hash;
} }
@ -79,43 +101,50 @@ window.qBittorrent.PropTrackers ??= (() => {
if (trackers) { if (trackers) {
torrentTrackersTable.clear(); torrentTrackersTable.clear();
const notApplicable = "QBT_TR(N/A)QBT_TR[CONTEXT=TrackerListWidget]";
trackers.each((tracker) => { trackers.each((tracker) => {
let status;
if (tracker.updating) {
status = "QBT_TR(Updating...)QBT_TR[CONTEXT=TrackerListWidget]";
}
else {
switch (tracker.status) {
case 0:
status = "QBT_TR(Disabled)QBT_TR[CONTEXT=TrackerListWidget]";
break;
case 1:
status = "QBT_TR(Not contacted yet)QBT_TR[CONTEXT=TrackerListWidget]";
break;
case 2:
status = "QBT_TR(Working)QBT_TR[CONTEXT=TrackerListWidget]";
break;
case 4:
status = "QBT_TR(Not working)QBT_TR[CONTEXT=TrackerListWidget]";
break;
}
}
const row = { const row = {
rowId: tracker.url, rowId: tracker.url,
tier: (tracker.tier >= 0) ? tracker.tier : "", tier: (tracker.tier >= 0) ? tracker.tier : "",
btVersion: "",
url: tracker.url, url: tracker.url,
status: status, status: trackerStatusText(tracker),
peers: (tracker.num_peers >= 0) ? tracker.num_peers : "QBT_TR(N/A)QBT_TR[CONTEXT=TrackerListWidget]", peers: (tracker.num_peers >= 0) ? tracker.num_peers : notApplicable,
seeds: (tracker.num_seeds >= 0) ? tracker.num_seeds : "QBT_TR(N/A)QBT_TR[CONTEXT=TrackerListWidget]", seeds: (tracker.num_seeds >= 0) ? tracker.num_seeds : notApplicable,
leeches: (tracker.num_leeches >= 0) ? tracker.num_leeches : "QBT_TR(N/A)QBT_TR[CONTEXT=TrackerListWidget]", leeches: (tracker.num_leeches >= 0) ? tracker.num_leeches : notApplicable,
downloaded: (tracker.num_downloaded >= 0) ? tracker.num_downloaded : "QBT_TR(N/A)QBT_TR[CONTEXT=TrackerListWidget]", downloaded: (tracker.num_downloaded >= 0) ? tracker.num_downloaded : notApplicable,
message: tracker.msg, message: tracker.msg,
nextAnnounce: tracker.next_announce,
minAnnounce: tracker.min_announce,
_isTracker: true,
_hasEndpoints: tracker.endpoints && (tracker.endpoints.length > 0),
_sortable: !tracker.url.startsWith("** [") _sortable: !tracker.url.startsWith("** [")
}; };
torrentTrackersTable.updateRowData(row); torrentTrackersTable.updateRowData(row);
if (tracker.endpoints !== undefined) {
for (const endpoint of tracker.endpoints) {
const row = {
rowId: `endpoint|${tracker.url}|${endpoint.name}|${endpoint.bt_version}`,
tier: "",
btVersion: `v${endpoint.bt_version}`,
url: endpoint.name,
status: trackerStatusText(endpoint),
peers: (endpoint.num_peers >= 0) ? endpoint.num_peers : notApplicable,
seeds: (endpoint.num_seeds >= 0) ? endpoint.num_seeds : notApplicable,
leeches: (endpoint.num_leeches >= 0) ? endpoint.num_leeches : notApplicable,
downloaded: (endpoint.num_downloaded >= 0) ? endpoint.num_downloaded : notApplicable,
message: endpoint.msg,
nextAnnounce: endpoint.next_announce,
minAnnounce: endpoint.min_announce,
_isTracker: false,
_tracker: tracker.url,
_sortable: true,
};
torrentTrackersTable.updateRowData(row);
}
}
}); });
torrentTrackersTable.updateTable(false); torrentTrackersTable.updateTable(false);
@ -163,7 +192,7 @@ window.qBittorrent.PropTrackers ??= (() => {
onShow: function() { onShow: function() {
const selectedTrackers = torrentTrackersTable.selectedRowsIds(); const selectedTrackers = torrentTrackersTable.selectedRowsIds();
const containsStaticTracker = selectedTrackers.some((tracker) => { const containsStaticTracker = selectedTrackers.some((tracker) => {
return tracker.startsWith("** ["); return tracker.startsWith("** [") || tracker.startsWith("endpoint|");
}); });
if (containsStaticTracker || (selectedTrackers.length === 0)) { if (containsStaticTracker || (selectedTrackers.length === 0)) {
@ -171,7 +200,6 @@ window.qBittorrent.PropTrackers ??= (() => {
this.hideItem("RemoveTracker"); this.hideItem("RemoveTracker");
this.hideItem("CopyTrackerUrl"); this.hideItem("CopyTrackerUrl");
this.hideItem("ReannounceTrackers"); this.hideItem("ReannounceTrackers");
this.hideItem("ReannounceAllTrackers");
} }
else { else {
if (selectedTrackers.length === 1) if (selectedTrackers.length === 1)