Compare commits
4 Commits
2631692cff
...
d6672abb94
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6672abb94 | ||
|
|
03fb036ae3 | ||
|
|
f743ae2d08 | ||
|
|
a265ba7fd2 |
@@ -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
|
||||
|
||||
@@ -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}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
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>
|
||||
<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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||