Compare commits
4 Commits
2631692cff
...
d6672abb94
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6672abb94 | ||
|
|
03fb036ae3 | ||
|
|
f743ae2d08 | ||
|
|
a265ba7fd2 |
@@ -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
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
|
|
||||||
#include "torrentscontroller.h"
|
#include "torrentscontroller.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
#include <concepts>
|
#include <concepts>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
@@ -67,14 +69,19 @@
|
|||||||
|
|
||||||
// Tracker keys
|
// Tracker keys
|
||||||
const QString KEY_TRACKER_URL = u"url"_s;
|
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_UPDATING = u"updating"_s;
|
||||||
const QString KEY_TRACKER_STATUS = u"status"_s;
|
const QString KEY_TRACKER_STATUS = u"status"_s;
|
||||||
const QString KEY_TRACKER_TIER = u"tier"_s;
|
const QString KEY_TRACKER_TIER = u"tier"_s;
|
||||||
const QString KEY_TRACKER_MSG = u"msg"_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_PEERS_COUNT = u"num_peers"_s;
|
||||||
const QString KEY_TRACKER_SEEDS_COUNT = u"num_seeds"_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_LEECHES_COUNT = u"num_leeches"_s;
|
||||||
const QString KEY_TRACKER_DOWNLOADED_COUNT = u"num_downloaded"_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
|
// Web seed keys
|
||||||
const QString KEY_WEBSEED_URL = u"url"_s;
|
const QString KEY_WEBSEED_URL = u"url"_s;
|
||||||
@@ -269,24 +276,52 @@ namespace
|
|||||||
|
|
||||||
QJsonArray getTrackers(const BitTorrent::Torrent *const torrent)
|
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;
|
QJsonArray trackerList;
|
||||||
|
|
||||||
for (const BitTorrent::TrackerEntryStatus &tracker : asConst(torrent->trackers()))
|
for (const BitTorrent::TrackerEntryStatus &tracker : asConst(torrent->trackers()))
|
||||||
{
|
{
|
||||||
const bool isNotWorking = (tracker.state == BitTorrent::TrackerEndpointState::NotWorking)
|
QJsonArray endpointsList;
|
||||||
|| (tracker.state == BitTorrent::TrackerEndpointState::TrackerError)
|
|
||||||
|| (tracker.state == BitTorrent::TrackerEndpointState::Unreachable);
|
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
|
trackerList << QJsonObject
|
||||||
{
|
{
|
||||||
{KEY_TRACKER_URL, tracker.url},
|
{KEY_TRACKER_URL, tracker.url},
|
||||||
{KEY_TRACKER_TIER, tracker.tier},
|
{KEY_TRACKER_TIER, tracker.tier},
|
||||||
{KEY_TRACKER_UPDATING, tracker.isUpdating},
|
{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_MSG, tracker.message},
|
||||||
{KEY_TRACKER_PEERS_COUNT, tracker.numPeers},
|
{KEY_TRACKER_PEERS_COUNT, tracker.numPeers},
|
||||||
{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, toSecondsSinceEpoch(tracker.nextAnnounceTime)},
|
||||||
|
{KEY_TRACKER_MIN_ANNOUNCE, toSecondsSinceEpoch(tracker.minAnnounceTime)},
|
||||||
|
{KEY_TRACKER_ENDPOINTS, endpointsList}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -27,41 +27,6 @@ Required by:
|
|||||||
position: relative;
|
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 {
|
#topNav {
|
||||||
font-family: Verdana, Arial, Helvetica, sans-serif;
|
font-family: Verdana, Arial, Helvetica, sans-serif;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
@@ -117,11 +82,17 @@ Required by:
|
|||||||
filter: var(--color-icon-hover);
|
filter: var(--color-icon-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
#desktopNavbar ul li a.arrow-right,
|
#desktopNavbar ul li a.arrow-right::after,
|
||||||
#desktopNavbar ul li a:hover.arrow-right {
|
#desktopNavbar ul li a:hover.arrow-right::after {
|
||||||
background-image: url("../images/arrow-right.gif");
|
border: solid currentcolor;
|
||||||
background-position: right 7px;
|
border-width: 0 2px 2px 0;
|
||||||
background-repeat: no-repeat;
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px;
|
||||||
|
position: absolute;
|
||||||
|
right: 6px;
|
||||||
|
top: 50%;
|
||||||
|
transform: rotate(-45deg) translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
#desktopNavbar li ul {
|
#desktopNavbar li ul {
|
||||||
@@ -298,17 +269,15 @@ li.divider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.horizontalHandle .handleIcon {
|
.horizontalHandle .handleIcon {
|
||||||
background: url("../images/handle-icon-horizontal.gif") center center
|
background: url("../images/handle-icon-horizontal.svg") center center
|
||||||
no-repeat;
|
no-repeat;
|
||||||
font-size: 1px;
|
|
||||||
height: 6px;
|
height: 6px;
|
||||||
line-height: 1px;
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.columnHandle {
|
.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: 1px solid var(--color-border-default);
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
border-top: 0;
|
border-top: 0;
|
||||||
@@ -331,7 +300,7 @@ li.divider {
|
|||||||
|
|
||||||
/* Have to specify div here for IE6's sake */
|
/* Have to specify div here for IE6's sake */
|
||||||
div.toolbox.divider {
|
div.toolbox.divider {
|
||||||
background: url("../images/toolbox-divider.gif") repeat-y;
|
background: url("../images/toolbox-divider.svg") repeat-y;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,15 +332,8 @@ div.toolbox.divider {
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#spinnerWrapper {
|
|
||||||
background: url("../images/spinner-placeholder.gif") no-repeat;
|
|
||||||
height: 16px;
|
|
||||||
margin: 4px 5px 0;
|
|
||||||
width: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#spinner {
|
#spinner {
|
||||||
background: url("../images/spinner.gif") no-repeat;
|
background: url("../images/spinner.svg") no-repeat;
|
||||||
display: none;
|
display: none;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ div.mochaToolbarWrapper.bottom {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mochaSpinner {
|
.mochaSpinner {
|
||||||
background: url("../images/spinner.gif") no-repeat;
|
background: url("../images/spinner.svg") no-repeat;
|
||||||
bottom: 7px;
|
bottom: 7px;
|
||||||
display: none;
|
display: none;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
@@ -332,32 +332,6 @@ div.mochaToolbarWrapper.bottom {
|
|||||||
text-align: center;
|
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. */
|
/* This does not work in IE6. */
|
||||||
.isFocused.jsonExample .mochaTitlebar h3 {
|
.isFocused.jsonExample .mochaTitlebar h3 {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
|||||||
@@ -323,7 +323,17 @@ a.propButton img {
|
|||||||
background-color: var(--color-background-default);
|
background-color: var(--color-background-default);
|
||||||
border: 1px solid var(--color-border-default);
|
border: 1px solid var(--color-border-default);
|
||||||
display: none;
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
padding: 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 {
|
.contextMenu .separator {
|
||||||
@@ -355,6 +365,7 @@ a.propButton img {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
padding: 5px 20px 5px 5px;
|
padding: 5px 20px 5px 5px;
|
||||||
|
position: relative;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -383,10 +394,16 @@ a.propButton img {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contextMenu li:not(.disabled) .arrow-right {
|
.contextMenu li:not(.disabled) .arrow-right::after {
|
||||||
background-image: url("../images/arrow-right.gif");
|
border: solid currentcolor;
|
||||||
background-position: right center;
|
border-width: 0 2px 2px 0;
|
||||||
background-repeat: no-repeat;
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px;
|
||||||
|
position: absolute;
|
||||||
|
right: 6px;
|
||||||
|
top: 50%;
|
||||||
|
transform: rotate(-45deg) translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.contextMenu li:not(.disabled):hover > ul {
|
.contextMenu li:not(.disabled):hover > ul {
|
||||||
@@ -442,7 +459,7 @@ a.propButton img {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#mochaToolbar .divider {
|
#mochaToolbar .divider {
|
||||||
background-image: url("../images/toolbox-divider.gif");
|
background-image: url("../images/toolbox-divider.svg");
|
||||||
background-position: left center;
|
background-position: left center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
padding-left: 14px;
|
padding-left: 14px;
|
||||||
@@ -532,27 +549,6 @@ a.propButton img {
|
|||||||
width: 190px;
|
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 {
|
fieldset.settings {
|
||||||
border: 1px solid var(--color-border-default);
|
border: 1px solid var(--color-border-default);
|
||||||
padding: 4px 4px 6px 6px;
|
padding: 4px 4px 6px 6px;
|
||||||
@@ -785,7 +781,7 @@ td.noWrap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
td.statusBarSeparator {
|
td.statusBarSeparator {
|
||||||
background-image: url("../images/toolbox-divider.gif");
|
background-image: url("../images/toolbox-divider.svg");
|
||||||
background-position: center 1px;
|
background-position: center 1px;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: 2px 18px;
|
background-size: 2px 18px;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 322 B |
|
Before Width: | Height: | Size: 66 B |
1
src/webui/www/private/images/L.svg
Normal 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 |
|
Before Width: | Height: | Size: 54 B |
|
Before Width: | Height: | Size: 46 B |
1
src/webui/www/private/images/handle-icon-horizontal.svg
Normal 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 |
|
Before Width: | Height: | Size: 46 B |
1
src/webui/www/private/images/handle-icon.svg
Normal 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 |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 49 B |
|
Before Width: | Height: | Size: 80 B |
|
Before Width: | Height: | Size: 793 B |
26
src/webui/www/private/images/spinner.svg
Normal 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 |
|
Before Width: | Height: | Size: 48 B |
1
src/webui/www/private/images/toolbox-divider.svg
Normal 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 |
@@ -52,9 +52,6 @@
|
|||||||
</noscript>
|
</noscript>
|
||||||
<div id="desktop">
|
<div id="desktop">
|
||||||
<div id="desktopHeader">
|
<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">
|
<div id="desktopNavbar">
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
@@ -259,11 +256,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>
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ window.qBittorrent.ContextMenu ??= (() => {
|
|||||||
onShow: () => {},
|
onShow: () => {},
|
||||||
onHide: () => {},
|
onHide: () => {},
|
||||||
onClick: () => {},
|
onClick: () => {},
|
||||||
fadeSpeed: 200,
|
|
||||||
touchTimer: 600,
|
touchTimer: 600,
|
||||||
...options
|
...options
|
||||||
};
|
};
|
||||||
@@ -68,15 +67,6 @@ window.qBittorrent.ContextMenu ??= (() => {
|
|||||||
// option diffs menu
|
// option diffs menu
|
||||||
this.menu = document.getElementById(this.options.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
|
// hide and begin the listener
|
||||||
this.hide().startListener();
|
this.hide().startListener();
|
||||||
|
|
||||||
@@ -231,7 +221,7 @@ window.qBittorrent.ContextMenu ??= (() => {
|
|||||||
show(trigger) {
|
show(trigger) {
|
||||||
if (lastShownContextMenu && (lastShownContextMenu !== this))
|
if (lastShownContextMenu && (lastShownContextMenu !== this))
|
||||||
lastShownContextMenu.hide();
|
lastShownContextMenu.hide();
|
||||||
this.fx.start(1);
|
this.menu.classList.add("visible");
|
||||||
this.options.onShow.call(this);
|
this.options.onShow.call(this);
|
||||||
lastShownContextMenu = this;
|
lastShownContextMenu = this;
|
||||||
return this;
|
return this;
|
||||||
@@ -240,7 +230,7 @@ window.qBittorrent.ContextMenu ??= (() => {
|
|||||||
// hide the menu
|
// hide the menu
|
||||||
hide(trigger) {
|
hide(trigger) {
|
||||||
if (lastShownContextMenu && (lastShownContextMenu.menu.style.visibility !== "hidden")) {
|
if (lastShownContextMenu && (lastShownContextMenu.menu.style.visibility !== "hidden")) {
|
||||||
this.fx.start(0);
|
this.menu.classList.remove("visible");
|
||||||
this.options.onHide.call(this);
|
this.options.onHide.call(this);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
@@ -1505,7 +1505,7 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||||||
td.title = "∞";
|
td.title = "∞";
|
||||||
}
|
}
|
||||||
else {
|
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.textContent = formattedVal;
|
||||||
td.title = formattedVal;
|
td.title = formattedVal;
|
||||||
}
|
}
|
||||||
@@ -2078,15 +2078,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();
|
||||||
}
|
}
|
||||||
@@ -2101,6 +2154,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;
|
||||||
@@ -2155,6 +2245,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;
|
||||||
}
|
}
|
||||||
@@ -2167,6 +2259,76 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||||||
td.textContent = status;
|
td.textContent = status;
|
||||||
td.title = 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) {
|
if (td.firstElementChild === null) {
|
||||||
const treeImg = document.createElement("img");
|
const treeImg = document.createElement("img");
|
||||||
treeImg.src = "images/L.gif";
|
treeImg.src = "images/L.svg";
|
||||||
treeImg.style.marginBottom = "-2px";
|
treeImg.style.marginBottom = "-2px";
|
||||||
td.append(treeImg);
|
td.append(treeImg);
|
||||||
}
|
}
|
||||||
@@ -2812,7 +2974,7 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||||||
|
|
||||||
if (td.firstElementChild === null) {
|
if (td.firstElementChild === null) {
|
||||||
const treeImg = document.createElement("img");
|
const treeImg = document.createElement("img");
|
||||||
treeImg.src = "images/L.gif";
|
treeImg.src = "images/L.svg";
|
||||||
treeImg.style.marginBottom = "-2px";
|
treeImg.style.marginBottom = "-2px";
|
||||||
td.append(treeImg);
|
td.append(treeImg);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -767,7 +767,7 @@ Supports the formats: S01E01, 1x1, 2017.12.31 and 31.12.2017 (Date formats also
|
|||||||
|
|
||||||
// calculate days since last match
|
// calculate days since last match
|
||||||
if (rulesList[ruleName].lastMatch !== "") {
|
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();
|
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);
|
document.getElementById("lastMatchText").textContent = " QBT_TR(Last Match: %1 days ago)QBT_TR[CONTEXT=AutomatedRssDownloader]".replace("%1", daysAgo);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,9 @@
|
|||||||
<file>private/editfeedurl.html</file>
|
<file>private/editfeedurl.html</file>
|
||||||
<file>private/edittracker.html</file>
|
<file>private/edittracker.html</file>
|
||||||
<file>private/editwebseed.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-exit.svg</file>
|
||||||
<file>private/images/application-rss.svg</file>
|
<file>private/images/application-rss.svg</file>
|
||||||
<file>private/images/application-url.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/browser-cookies.svg</file>
|
||||||
<file>private/images/checked-completed.svg</file>
|
<file>private/images/checked-completed.svg</file>
|
||||||
<file>private/images/collapse.svg</file>
|
<file>private/images/collapse.svg</file>
|
||||||
@@ -322,18 +320,16 @@
|
|||||||
<file>private/images/go-down.svg</file>
|
<file>private/images/go-down.svg</file>
|
||||||
<file>private/images/go-top.svg</file>
|
<file>private/images/go-top.svg</file>
|
||||||
<file>private/images/go-up.svg</file>
|
<file>private/images/go-up.svg</file>
|
||||||
<file>private/images/handle-icon-horizontal.gif</file>
|
<file>private/images/handle-icon-horizontal.svg</file>
|
||||||
<file>private/images/handle-icon.gif</file>
|
<file>private/images/handle-icon.svg</file>
|
||||||
<file>private/images/hash.svg</file>
|
<file>private/images/hash.svg</file>
|
||||||
<file>private/images/help-about.svg</file>
|
<file>private/images/help-about.svg</file>
|
||||||
<file>private/images/help-contents.svg</file>
|
<file>private/images/help-contents.svg</file>
|
||||||
<file>private/images/insert-link.svg</file>
|
<file>private/images/insert-link.svg</file>
|
||||||
<file>private/images/ip-blocked.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-add.svg</file>
|
||||||
<file>private/images/list-remove.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/mail-inbox.svg</file>
|
||||||
<file>private/images/mascot.png</file>
|
<file>private/images/mascot.png</file>
|
||||||
<file>private/images/name.svg</file>
|
<file>private/images/name.svg</file>
|
||||||
@@ -357,9 +353,7 @@
|
|||||||
<file>private/images/set-location.svg</file>
|
<file>private/images/set-location.svg</file>
|
||||||
<file>private/images/slow.svg</file>
|
<file>private/images/slow.svg</file>
|
||||||
<file>private/images/slow_off.svg</file>
|
<file>private/images/slow_off.svg</file>
|
||||||
<file>private/images/spacer.gif</file>
|
<file>private/images/spinner.svg</file>
|
||||||
<file>private/images/spinner-placeholder.gif</file>
|
|
||||||
<file>private/images/spinner.gif</file>
|
|
||||||
<file>private/images/stalledDL.svg</file>
|
<file>private/images/stalledDL.svg</file>
|
||||||
<file>private/images/stalledUP.svg</file>
|
<file>private/images/stalledUP.svg</file>
|
||||||
<file>private/images/stopped.svg</file>
|
<file>private/images/stopped.svg</file>
|
||||||
@@ -367,7 +361,7 @@
|
|||||||
<file>private/images/tags.svg</file>
|
<file>private/images/tags.svg</file>
|
||||||
<file>private/images/task-complete.svg</file>
|
<file>private/images/task-complete.svg</file>
|
||||||
<file>private/images/task-reject.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-creator.svg</file>
|
||||||
<file>private/images/torrent-magnet.svg</file>
|
<file>private/images/torrent-magnet.svg</file>
|
||||||
<file>private/images/torrent-start-forced.svg</file>
|
<file>private/images/torrent-start-forced.svg</file>
|
||||||
|
|||||||