Compare commits

...

4 Commits

Author SHA1 Message Date
Chocobo1
d6672abb94 WebUI: use static method for getting time
Unify API usage across the code base.

PR #23077.
2025-08-11 16:47:37 +08:00
tehcneko
03fb036ae3 WebUI: Replace GIFs with SVG
Unused GIFs have been removed along with their CSS; some GIFs have been replaced with CSS, and all SVGs were drawn myself.

PR #23074.
2025-08-11 16:38:11 +08:00
tehcneko
f743ae2d08 WebUI: Use native css transition for context menu
Reduce MooTools usage.

PR #23069.
2025-08-11 16:28:47 +08:00
tehcneko
a265ba7fd2 WebUI: Implement missing tracker list features
Implemented: Tracker endpoints in the list, missing "Tracker Error" and "Unreachable" status, "Next Announce" and "Min Announce" column and double click to edit tracker url.

PR #23045.
2025-08-11 16:20:58 +08:00
28 changed files with 351 additions and 176 deletions

View File

@@ -1,5 +1,12 @@
# 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
* [#23031](https://github.com/qbittorrent/qBittorrent/pull/23031)
* 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 <algorithm>
#include <chrono>
#include <concepts>
#include <functional>
@@ -67,14 +69,19 @@
// Tracker keys
const QString KEY_TRACKER_URL = u"url"_s;
const QString KEY_TRACKER_NAME = u"name"_s;
const QString KEY_TRACKER_UPDATING = u"updating"_s;
const QString KEY_TRACKER_STATUS = u"status"_s;
const QString KEY_TRACKER_TIER = u"tier"_s;
const QString KEY_TRACKER_MSG = u"msg"_s;
const QString KEY_TRACKER_BT_VERSION = u"bt_version"_s;
const QString KEY_TRACKER_PEERS_COUNT = u"num_peers"_s;
const QString KEY_TRACKER_SEEDS_COUNT = u"num_seeds"_s;
const QString KEY_TRACKER_LEECHES_COUNT = u"num_leeches"_s;
const QString KEY_TRACKER_DOWNLOADED_COUNT = u"num_downloaded"_s;
const QString KEY_TRACKER_NEXT_ANNOUNCE = u"next_announce"_s;
const QString KEY_TRACKER_MIN_ANNOUNCE = u"min_announce"_s;
const QString KEY_TRACKER_ENDPOINTS = u"endpoints"_s;
// Web seed keys
const QString KEY_WEBSEED_URL = u"url"_s;
@@ -269,24 +276,52 @@ namespace
QJsonArray getTrackers(const BitTorrent::Torrent *const torrent)
{
const auto now = std::chrono::system_clock::now();
const auto timepointNow = BitTorrent::AnnounceTimePoint::clock::now();
const auto toSecondsSinceEpoch = [&now, &timepointNow](const BitTorrent::AnnounceTimePoint &time) -> qint64
{
const auto timeEpoch = (now + (time - timepointNow)).time_since_epoch();
return std::chrono::duration_cast<std::chrono::seconds>(timeEpoch).count();
};
QJsonArray trackerList;
for (const BitTorrent::TrackerEntryStatus &tracker : asConst(torrent->trackers()))
{
const bool isNotWorking = (tracker.state == BitTorrent::TrackerEndpointState::NotWorking)
|| (tracker.state == BitTorrent::TrackerEndpointState::TrackerError)
|| (tracker.state == BitTorrent::TrackerEndpointState::Unreachable);
QJsonArray endpointsList;
for (const BitTorrent::TrackerEndpointStatus &endpoint : tracker.endpoints)
{
endpointsList << QJsonObject
{
{KEY_TRACKER_NAME, endpoint.name},
{KEY_TRACKER_UPDATING, endpoint.isUpdating},
{KEY_TRACKER_STATUS, static_cast<int>(endpoint.state)},
{KEY_TRACKER_MSG, endpoint.message},
{KEY_TRACKER_BT_VERSION, static_cast<int>(endpoint.btVersion)},
{KEY_TRACKER_PEERS_COUNT, endpoint.numPeers},
{KEY_TRACKER_SEEDS_COUNT, endpoint.numSeeds},
{KEY_TRACKER_LEECHES_COUNT, endpoint.numLeeches},
{KEY_TRACKER_DOWNLOADED_COUNT, endpoint.numDownloaded},
{KEY_TRACKER_NEXT_ANNOUNCE, toSecondsSinceEpoch(endpoint.nextAnnounceTime)},
{KEY_TRACKER_MIN_ANNOUNCE, toSecondsSinceEpoch(endpoint.minAnnounceTime)}
};
}
trackerList << QJsonObject
{
{KEY_TRACKER_URL, tracker.url},
{KEY_TRACKER_TIER, tracker.tier},
{KEY_TRACKER_UPDATING, tracker.isUpdating},
{KEY_TRACKER_STATUS, static_cast<int>((isNotWorking ? BitTorrent::TrackerEndpointState::NotWorking : tracker.state))},
{KEY_TRACKER_STATUS, static_cast<int>(tracker.state)},
{KEY_TRACKER_MSG, tracker.message},
{KEY_TRACKER_PEERS_COUNT, tracker.numPeers},
{KEY_TRACKER_SEEDS_COUNT, tracker.numSeeds},
{KEY_TRACKER_LEECHES_COUNT, tracker.numLeeches},
{KEY_TRACKER_DOWNLOADED_COUNT, tracker.numDownloaded}
{KEY_TRACKER_DOWNLOADED_COUNT, tracker.numDownloaded},
{KEY_TRACKER_NEXT_ANNOUNCE, toSecondsSinceEpoch(tracker.nextAnnounceTime)},
{KEY_TRACKER_MIN_ANNOUNCE, toSecondsSinceEpoch(tracker.minAnnounceTime)},
{KEY_TRACKER_ENDPOINTS, endpointsList}
};
}

View File

@@ -53,7 +53,7 @@
#include "base/utils/version.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 AuthController;

View File

@@ -27,41 +27,6 @@ Required by:
position: relative;
}
#desktopTitlebarWrapper {
height: 45px;
overflow: hidden;
position: relative;
}
#desktopTitlebar {
background: url("../images/logo.gif") no-repeat;
background-position: left 0;
height: 32px;
padding: 7px 8px 6px;
}
#desktopTitlebar h1.applicationTitle {
display: none;
font-size: 20px;
font-weight: bold;
line-height: 25px;
margin: 0;
padding: 0 5px 0 0;
}
#desktopTitlebar h2.tagline {
font-family: Verdana, Arial, Helvetica, sans-serif;
font-size: 10px;
font-weight: bold;
padding: 7px 0 0;
text-align: center;
text-transform: uppercase;
}
#desktopTitlebar h2.tagline .taglineEm {
font-weight: bold;
}
#topNav {
font-family: Verdana, Arial, Helvetica, sans-serif;
font-size: 10px;
@@ -117,11 +82,17 @@ Required by:
filter: var(--color-icon-hover);
}
#desktopNavbar ul li a.arrow-right,
#desktopNavbar ul li a:hover.arrow-right {
background-image: url("../images/arrow-right.gif");
background-position: right 7px;
background-repeat: no-repeat;
#desktopNavbar ul li a.arrow-right::after,
#desktopNavbar ul li a:hover.arrow-right::after {
border: solid currentcolor;
border-width: 0 2px 2px 0;
content: "";
display: inline-block;
padding: 2px;
position: absolute;
right: 6px;
top: 50%;
transform: rotate(-45deg) translateY(-50%);
}
#desktopNavbar li ul {
@@ -298,17 +269,15 @@ li.divider {
}
.horizontalHandle .handleIcon {
background: url("../images/handle-icon-horizontal.gif") center center
background: url("../images/handle-icon-horizontal.svg") center center
no-repeat;
font-size: 1px;
height: 6px;
line-height: 1px;
margin: 0 auto;
overflow: hidden;
}
.columnHandle {
background: url("../images/handle-icon.gif") center center no-repeat;
background: url("../images/handle-icon.svg") center center no-repeat;
border: 1px solid var(--color-border-default);
border-bottom: 0;
border-top: 0;
@@ -331,7 +300,7 @@ li.divider {
/* Have to specify div here for IE6's sake */
div.toolbox.divider {
background: url("../images/toolbox-divider.gif") repeat-y;
background: url("../images/toolbox-divider.svg") repeat-y;
padding-left: 8px;
}
@@ -363,15 +332,8 @@ div.toolbox.divider {
border-radius: 3px;
}
#spinnerWrapper {
background: url("../images/spinner-placeholder.gif") no-repeat;
height: 16px;
margin: 4px 5px 0;
width: 16px;
}
#spinner {
background: url("../images/spinner.gif") no-repeat;
background: url("../images/spinner.svg") no-repeat;
display: none;
height: 16px;
width: 16px;

View File

@@ -175,7 +175,7 @@ div.mochaToolbarWrapper.bottom {
}
.mochaSpinner {
background: url("../images/spinner.gif") no-repeat;
background: url("../images/spinner.svg") no-repeat;
bottom: 7px;
display: none;
height: 16px;
@@ -332,32 +332,6 @@ div.mochaToolbarWrapper.bottom {
text-align: center;
}
/* Example Window Themes */
#about_contentWrapper {
background: #e5e5e5 url("../images/logo2.gif") 3px 3px no-repeat;
}
#builder_contentWrapper {
background: #f5f5f7;
}
#json01 .mochaTitlebar {
background: #6dd2db;
}
#json02 .mochaTitlebar {
background: #6db6db;
}
#json03 .mochaTitlebar {
background: #6d92db;
}
.jsonExample .mochaTitlebar h3 {
color: #dddddd;
}
/* This does not work in IE6. */
.isFocused.jsonExample .mochaTitlebar h3 {
color: #ffffff;

View File

@@ -323,7 +323,17 @@ a.propButton img {
background-color: var(--color-background-default);
border: 1px solid var(--color-border-default);
display: none;
opacity: 0;
padding: 0;
transition:
opacity 0.2s ease-in-out,
visibility 0.2s ease-in-out;
visibility: hidden;
}
.contextMenu.visible {
opacity: 1;
visibility: visible;
}
.contextMenu .separator {
@@ -355,6 +365,7 @@ a.propButton img {
display: flex;
gap: 2px;
padding: 5px 20px 5px 5px;
position: relative;
text-decoration: none;
white-space: nowrap;
}
@@ -383,10 +394,16 @@ a.propButton img {
position: relative;
}
.contextMenu li:not(.disabled) .arrow-right {
background-image: url("../images/arrow-right.gif");
background-position: right center;
background-repeat: no-repeat;
.contextMenu li:not(.disabled) .arrow-right::after {
border: solid currentcolor;
border-width: 0 2px 2px 0;
content: "";
display: inline-block;
padding: 2px;
position: absolute;
right: 6px;
top: 50%;
transform: rotate(-45deg) translateY(-50%);
}
.contextMenu li:not(.disabled):hover > ul {
@@ -442,7 +459,7 @@ a.propButton img {
}
#mochaToolbar .divider {
background-image: url("../images/toolbox-divider.gif");
background-image: url("../images/toolbox-divider.svg");
background-position: left center;
background-repeat: no-repeat;
padding-left: 14px;
@@ -532,27 +549,6 @@ a.propButton img {
width: 190px;
}
/* Tri-state checkbox */
label.tristate {
background: url("../images/3-state-checkbox.gif") 0 0 no-repeat;
display: block;
float: left;
height: 13px;
margin: 0.15em 8px 5px 0;
overflow: hidden;
text-indent: -999em;
width: 13px;
}
label.checked {
background-position: 0 -13px;
}
label.partial {
background-position: 0 -26px;
}
fieldset.settings {
border: 1px solid var(--color-border-default);
padding: 4px 4px 6px 6px;
@@ -785,7 +781,7 @@ td.noWrap {
}
td.statusBarSeparator {
background-image: url("../images/toolbox-divider.gif");
background-image: url("../images/toolbox-divider.svg");
background-position: center 1px;
background-repeat: no-repeat;
background-size: 2px 18px;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 B

View File

@@ -0,0 +1 @@
<svg width="18" height="18" xmlns="http://www.w3.org/2000/svg"><path stroke="gray" stroke-dasharray="1,1" d="M7.5 0v9M7 8.5h11" /></svg>

After

Width:  |  Height:  |  Size: 136 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 B

View File

@@ -0,0 +1 @@
<svg height="8" width="20" xmlns="http://www.w3.org/2000/svg"><g fill="#999"><circle cx="6" cy="4" r="1"/><circle cx="10" cy="4" r="1"/><circle cx="14" cy="4" r="1"/></g></svg>

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 B

View File

@@ -0,0 +1 @@
<svg height="20" width="8" xmlns="http://www.w3.org/2000/svg"><g fill="#999"><circle cx="4" cy="6" r="1"/><circle cx="4" cy="10" r="1"/><circle cx="4" cy="14" r="1"/></g></svg>

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 793 B

View File

@@ -0,0 +1,26 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<path stroke="#999" stroke-linecap="round" stroke-width="2" d="M8 4V1" opacity="0">
<animate attributeName="opacity" begin="0s" dur="0.8s" repeatCount="indefinite" values="1;0" />
</path>
<path stroke="#999" stroke-linecap="round" stroke-width="2" d="M10.828 5.172 12.95 3.05" opacity="0">
<animate attributeName="opacity" begin="0.1s" dur="0.8s" repeatCount="indefinite" values="1;0" />
</path>
<path stroke="#999" stroke-linecap="round" stroke-width="2" d="M12 8h3" opacity="0">
<animate attributeName="opacity" begin="0.2s" dur="0.8s" repeatCount="indefinite" values="1;0" />
</path>
<path stroke="#999" stroke-linecap="round" stroke-width="2" d="m10.828 10.828 2.122 2.122" opacity="0">
<animate attributeName="opacity" begin="0.3s" dur="0.8s" repeatCount="indefinite" values="1;0" />
</path>
<path stroke="#999" stroke-linecap="round" stroke-width="2" d="M8 12v3" opacity="0">
<animate attributeName="opacity" begin="0.4s" dur="0.8s" repeatCount="indefinite" values="1;0" />
</path>
<path stroke="#999" stroke-linecap="round" stroke-width="2" d="M5.172 10.828 3.05 12.95" opacity="0">
<animate attributeName="opacity" begin="0.5s" dur="0.8s" repeatCount="indefinite" values="1;0" />
</path>
<path stroke="#999" stroke-linecap="round" stroke-width="2" d="M4 8H1" opacity="0">
<animate attributeName="opacity" begin="0.6s" dur="0.8s" repeatCount="indefinite" values="1;0" />
</path>
<path stroke="#999" stroke-linecap="round" stroke-width="2" d="M5.172 5.172 3.05 3.05" opacity="0">
<animate attributeName="opacity" begin="0.7s" dur="0.8s" repeatCount="indefinite" values="1;0" />
</path>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 B

View File

@@ -0,0 +1 @@
<svg height="20" width="2" xmlns="http://www.w3.org/2000/svg"><path d="m0 0h1v20h-1z" fill="#fff"/><path d="m1 0h1v20h-1z" fill="#c4c4c4"/></svg>

After

Width:  |  Height:  |  Size: 145 B

View File

@@ -52,9 +52,6 @@
</noscript>
<div id="desktop">
<div id="desktopHeader">
<!--<div id="desktopTitlebar">
<h1 class="applicationTitle">qBittorrent Web User Interface <span class="version">version 2.0.0</span></h1>
</div>-->
<div id="desktopNavbar">
<ul>
<li>
@@ -259,11 +256,11 @@
</ul>
<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 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="#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 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><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/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 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>

View File

@@ -60,7 +60,6 @@ window.qBittorrent.ContextMenu ??= (() => {
onShow: () => {},
onHide: () => {},
onClick: () => {},
fadeSpeed: 200,
touchTimer: 600,
...options
};
@@ -68,15 +67,6 @@ window.qBittorrent.ContextMenu ??= (() => {
// option diffs menu
this.menu = document.getElementById(this.options.menu);
// fx
this.fx = new Fx.Tween(this.menu, {
property: "opacity",
duration: this.options.fadeSpeed,
onComplete: () => {
this.menu.style.visibility = (getComputedStyle(this.menu).opacity > 0) ? "visible" : "hidden";
}
});
// hide and begin the listener
this.hide().startListener();
@@ -231,7 +221,7 @@ window.qBittorrent.ContextMenu ??= (() => {
show(trigger) {
if (lastShownContextMenu && (lastShownContextMenu !== this))
lastShownContextMenu.hide();
this.fx.start(1);
this.menu.classList.add("visible");
this.options.onShow.call(this);
lastShownContextMenu = this;
return this;
@@ -240,7 +230,7 @@ window.qBittorrent.ContextMenu ??= (() => {
// hide the menu
hide(trigger) {
if (lastShownContextMenu && (lastShownContextMenu.menu.style.visibility !== "hidden")) {
this.fx.start(0);
this.menu.classList.remove("visible");
this.options.onHide.call(this);
}
return this;

View File

@@ -1505,7 +1505,7 @@ window.qBittorrent.DynamicTable ??= (() => {
td.title = "∞";
}
else {
const formattedVal = "QBT_TR(%1 ago)QBT_TR[CONTEXT=TransferListDelegate]".replace("%1", window.qBittorrent.Misc.friendlyDuration((new Date() / 1000) - val));
const formattedVal = "QBT_TR(%1 ago)QBT_TR[CONTEXT=TransferListDelegate]".replace("%1", window.qBittorrent.Misc.friendlyDuration((Date.now() / 1000) - val));
td.textContent = formattedVal;
td.title = formattedVal;
}
@@ -2078,15 +2078,68 @@ window.qBittorrent.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() {
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("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("peers", "", "QBT_TR(Peers)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("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("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();
}
@@ -2101,6 +2154,43 @@ window.qBittorrent.DynamicTable ??= (() => {
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["status"].compareRows = naturalSort;
this.columns["message"].compareRows = naturalSort;
@@ -2155,6 +2245,8 @@ window.qBittorrent.DynamicTable ??= (() => {
statusClass = "trackerUpdating";
break;
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";
break;
}
@@ -2167,6 +2259,76 @@ window.qBittorrent.DynamicTable ??= (() => {
td.textContent = status;
td.title = status;
};
const friendlyDuration = function(td, row) {
const value = this.getRowValue(row) ?? 0;
const seconds = Math.max(value - (Date.now() / 1000), 0);
const duration = window.qBittorrent.Misc.friendlyDuration(seconds, 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);
});
}
}
@@ -2327,7 +2489,7 @@ window.qBittorrent.DynamicTable ??= (() => {
if (td.firstElementChild === null) {
const treeImg = document.createElement("img");
treeImg.src = "images/L.gif";
treeImg.src = "images/L.svg";
treeImg.style.marginBottom = "-2px";
td.append(treeImg);
}
@@ -2812,7 +2974,7 @@ window.qBittorrent.DynamicTable ??= (() => {
if (td.firstElementChild === null) {
const treeImg = document.createElement("img");
treeImg.src = "images/L.gif";
treeImg.src = "images/L.svg";
treeImg.style.marginBottom = "-2px";
td.append(treeImg);
}

View File

@@ -32,6 +32,7 @@ window.qBittorrent ??= {};
window.qBittorrent.PropTrackers ??= (() => {
const exports = () => {
return {
editTracker: editTrackerFN,
updateData: updateData,
clear: clear
};
@@ -42,6 +43,25 @@ window.qBittorrent.PropTrackers ??= (() => {
const torrentTrackersTable = new window.qBittorrent.DynamicTable.TorrentTrackersTable();
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 = () => {
if (document.hidden)
return;
@@ -53,11 +73,13 @@ window.qBittorrent.PropTrackers ??= (() => {
const new_hash = torrentsTable.getCurrentTorrentID();
if (new_hash === "") {
torrentTrackersTable.clear();
torrentTrackersTable.clearCollapseState();
clearTimeout(loadTrackersDataTimer);
return;
}
if (new_hash !== current_hash) {
torrentTrackersTable.clear();
torrentTrackersTable.clearCollapseState();
current_hash = new_hash;
}
@@ -79,43 +101,50 @@ window.qBittorrent.PropTrackers ??= (() => {
if (trackers) {
torrentTrackersTable.clear();
const notApplicable = "QBT_TR(N/A)QBT_TR[CONTEXT=TrackerListWidget]";
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 = {
rowId: tracker.url,
tier: (tracker.tier >= 0) ? tracker.tier : "",
btVersion: "",
url: tracker.url,
status: status,
peers: (tracker.num_peers >= 0) ? tracker.num_peers : "QBT_TR(N/A)QBT_TR[CONTEXT=TrackerListWidget]",
seeds: (tracker.num_seeds >= 0) ? tracker.num_seeds : "QBT_TR(N/A)QBT_TR[CONTEXT=TrackerListWidget]",
leeches: (tracker.num_leeches >= 0) ? tracker.num_leeches : "QBT_TR(N/A)QBT_TR[CONTEXT=TrackerListWidget]",
downloaded: (tracker.num_downloaded >= 0) ? tracker.num_downloaded : "QBT_TR(N/A)QBT_TR[CONTEXT=TrackerListWidget]",
status: trackerStatusText(tracker),
peers: (tracker.num_peers >= 0) ? tracker.num_peers : notApplicable,
seeds: (tracker.num_seeds >= 0) ? tracker.num_seeds : notApplicable,
leeches: (tracker.num_leeches >= 0) ? tracker.num_leeches : notApplicable,
downloaded: (tracker.num_downloaded >= 0) ? tracker.num_downloaded : notApplicable,
message: tracker.msg,
nextAnnounce: tracker.next_announce,
minAnnounce: tracker.min_announce,
_isTracker: true,
_hasEndpoints: tracker.endpoints && (tracker.endpoints.length > 0),
_sortable: !tracker.url.startsWith("** [")
};
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);
@@ -163,7 +192,7 @@ window.qBittorrent.PropTrackers ??= (() => {
onShow: function() {
const selectedTrackers = torrentTrackersTable.selectedRowsIds();
const containsStaticTracker = selectedTrackers.some((tracker) => {
return tracker.startsWith("** [");
return tracker.startsWith("** [") || tracker.startsWith("endpoint|");
});
if (containsStaticTracker || (selectedTrackers.length === 0)) {
@@ -171,7 +200,6 @@ window.qBittorrent.PropTrackers ??= (() => {
this.hideItem("RemoveTracker");
this.hideItem("CopyTrackerUrl");
this.hideItem("ReannounceTrackers");
this.hideItem("ReannounceAllTrackers");
}
else {
if (selectedTrackers.length === 1)

View File

@@ -767,7 +767,7 @@ Supports the formats: S01E01, 1x1, 2017.12.31 and 31.12.2017 (Date formats also
// calculate days since last match
if (rulesList[ruleName].lastMatch !== "") {
const timeDiffInMs = new Date().getTime() - new Date(rulesList[ruleName].lastMatch).getTime();
const timeDiffInMs = Date.now() - new Date(rulesList[ruleName].lastMatch).getTime();
const daysAgo = Math.floor(timeDiffInMs / (1000 * 60 * 60 * 24)).toString();
document.getElementById("lastMatchText").textContent = " QBT_TR(Last Match: %1 days ago)QBT_TR[CONTEXT=AutomatedRssDownloader]".replace("%1", daysAgo);
}

View File

@@ -20,11 +20,9 @@
<file>private/editfeedurl.html</file>
<file>private/edittracker.html</file>
<file>private/editwebseed.html</file>
<file>private/images/3-state-checkbox.gif</file>
<file>private/images/application-exit.svg</file>
<file>private/images/application-rss.svg</file>
<file>private/images/application-url.svg</file>
<file>private/images/arrow-right.gif</file>
<file>private/images/browser-cookies.svg</file>
<file>private/images/checked-completed.svg</file>
<file>private/images/collapse.svg</file>
@@ -322,18 +320,16 @@
<file>private/images/go-down.svg</file>
<file>private/images/go-top.svg</file>
<file>private/images/go-up.svg</file>
<file>private/images/handle-icon-horizontal.gif</file>
<file>private/images/handle-icon.gif</file>
<file>private/images/handle-icon-horizontal.svg</file>
<file>private/images/handle-icon.svg</file>
<file>private/images/hash.svg</file>
<file>private/images/help-about.svg</file>
<file>private/images/help-contents.svg</file>
<file>private/images/insert-link.svg</file>
<file>private/images/ip-blocked.svg</file>
<file>private/images/L.gif</file>
<file>private/images/L.svg</file>
<file>private/images/list-add.svg</file>
<file>private/images/list-remove.svg</file>
<file>private/images/logo.gif</file>
<file>private/images/logo2.gif</file>
<file>private/images/mail-inbox.svg</file>
<file>private/images/mascot.png</file>
<file>private/images/name.svg</file>
@@ -357,9 +353,7 @@
<file>private/images/set-location.svg</file>
<file>private/images/slow.svg</file>
<file>private/images/slow_off.svg</file>
<file>private/images/spacer.gif</file>
<file>private/images/spinner-placeholder.gif</file>
<file>private/images/spinner.gif</file>
<file>private/images/spinner.svg</file>
<file>private/images/stalledDL.svg</file>
<file>private/images/stalledUP.svg</file>
<file>private/images/stopped.svg</file>
@@ -367,7 +361,7 @@
<file>private/images/tags.svg</file>
<file>private/images/task-complete.svg</file>
<file>private/images/task-reject.svg</file>
<file>private/images/toolbox-divider.gif</file>
<file>private/images/toolbox-divider.svg</file>
<file>private/images/torrent-creator.svg</file>
<file>private/images/torrent-magnet.svg</file>
<file>private/images/torrent-start-forced.svg</file>