mirror of
https://github.com/qbittorrent/qBittorrent
synced 2025-08-19 21:03:30 -07:00
WebUI: Add new Add Torrent experience
Some checks failed
cpp.yaml / WebUI: Add new Add Torrent experience (push) Failing after 0s
js.yaml / WebUI: Add new Add Torrent experience (push) Failing after 0s
CI - File health / Check (push) Has been cancelled
CI - macOS / Build (push) Has been cancelled
CI - Python / Check (push) Has been cancelled
CI - Ubuntu / Build (push) Has been cancelled
CI - WebUI / Check (push) Has been cancelled
CI - Windows / Build (push) Has been cancelled
Some checks failed
cpp.yaml / WebUI: Add new Add Torrent experience (push) Failing after 0s
js.yaml / WebUI: Add new Add Torrent experience (push) Failing after 0s
CI - File health / Check (push) Has been cancelled
CI - macOS / Build (push) Has been cancelled
CI - Python / Check (push) Has been cancelled
CI - Ubuntu / Build (push) Has been cancelled
CI - WebUI / Check (push) Has been cancelled
CI - Windows / Build (push) Has been cancelled
This PR uses the new APIs from #21015 to provide a WebUI Add Torrent experience more closely matching the GUI's. New functionality: - View torrent size, date, infohash, files, etc. - Reprioritize and ignore files before adding - Specify tags when adding torrent - Specify save path for incomplete torrent Closes #20557, closes #10997, closes #12499, closes #14201, closes #15071, closes #15718, closes #16207. PR #21645.
This commit is contained in:
parent
02d72179fe
commit
02892d1250
18 changed files with 1582 additions and 1074 deletions
|
@ -187,8 +187,10 @@ private:
|
|||
{{u"torrents"_s, u"editCategory"_s}, Http::METHOD_POST},
|
||||
{{u"torrents"_s, u"editTracker"_s}, Http::METHOD_POST},
|
||||
{{u"torrents"_s, u"editWebSeed"_s}, Http::METHOD_POST},
|
||||
{{u"torrents"_s, u"fetchMetadata"_s}, Http::METHOD_POST},
|
||||
{{u"torrents"_s, u"filePrio"_s}, Http::METHOD_POST},
|
||||
{{u"torrents"_s, u"increasePrio"_s}, Http::METHOD_POST},
|
||||
{{u"torrents"_s, u"parseMetadata"_s}, Http::METHOD_POST},
|
||||
{{u"torrents"_s, u"reannounce"_s}, Http::METHOD_POST},
|
||||
{{u"torrents"_s, u"recheck"_s}, Http::METHOD_POST},
|
||||
{{u"torrents"_s, u"removeCategories"_s}, Http::METHOD_POST},
|
||||
|
|
404
src/webui/www/private/addtorrent.html
Normal file
404
src/webui/www/private/addtorrent.html
Normal file
|
@ -0,0 +1,404 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="${LANG}" class="dark">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>QBT_TR(Add torrent)QBT_TR[CONTEXT=AddNewTorrentDialog]</title>
|
||||
<link rel="stylesheet" type="text/css" href="css/dynamicTable.css?v=${CACHEID}">
|
||||
<link rel="stylesheet" href="css/style.css?v=${CACHEID}" type="text/css">
|
||||
<link rel="stylesheet" href="css/Window.css?v=${CACHEID}" type="text/css">
|
||||
<link rel="stylesheet" href="css/vanillaSelectBox.css" type="text/css">
|
||||
<script defer src="scripts/lib/MooTools-Core-1.6.0-compat-compressed.js"></script>
|
||||
<script defer src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script>
|
||||
<script defer src="scripts/lib/vanillaSelectBox.js"></script>
|
||||
<script defer src="scripts/localpreferences.js?v=${CACHEID}"></script>
|
||||
<script defer src="scripts/color-scheme.js?v=${CACHEID}"></script>
|
||||
<script defer src="scripts/addtorrent.js?locale=${LANG}&v=${CACHEID}"></script>
|
||||
<script defer src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script>
|
||||
<script defer src="scripts/pathAutofill.js?v=${CACHEID}"></script>
|
||||
<script defer src="scripts/file-tree.js?v=${CACHEID}"></script>
|
||||
<script defer src="scripts/filesystem.js?v=${CACHEID}"></script>
|
||||
<script defer src="scripts/contextmenu.js?locale=${LANG}&v=${CACHEID}"></script>
|
||||
<script defer src="scripts/dynamicTable.js?locale=${LANG}&v=${CACHEID}"></script>
|
||||
<script defer src="scripts/torrent-content.js?v=${CACHEID}"></script>
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
window.addEventListener("DOMContentLoaded", (event) => {
|
||||
window.addEventListener("keydown", (event) => {
|
||||
switch (event.key) {
|
||||
case "Escape":
|
||||
event.preventDefault();
|
||||
window.parent.qBittorrent.Client.closeFrameWindow(window);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
const source = searchParams.get("source");
|
||||
if ((source === null) || (source.length === 0))
|
||||
return;
|
||||
|
||||
document.getElementById("urls").value = source;
|
||||
|
||||
// fetch unless explicitly told not to
|
||||
const fetchMetadata = searchParams.get("fetch") !== "false";
|
||||
let submitted = false;
|
||||
|
||||
const windowId = searchParams.get("windowId");
|
||||
window.qBittorrent.AddTorrent.setWindowId(windowId);
|
||||
|
||||
document.getElementById("uploadForm").addEventListener("submit", (event) => {
|
||||
submitted = true;
|
||||
window.qBittorrent.AddTorrent.submitForm();
|
||||
});
|
||||
|
||||
document.getElementById("upload_frame").addEventListener("load", (event) => {
|
||||
if (submitted)
|
||||
window.parent.qBittorrent.Client.closeFrameWindow(window);
|
||||
});
|
||||
|
||||
document.getElementById("saveTorrent").addEventListener("click", (event) => {
|
||||
const url = new URL("api/v2/torrents/saveMetadata", window.location);
|
||||
url.search = new URLSearchParams({
|
||||
source: source
|
||||
});
|
||||
|
||||
window.qBittorrent.Misc.downloadFile(url, "torrent.torrent", "QBT_TR(Unable to download torrent file)QBT_TR[CONTEXT=AddNewTorrentDialog]");
|
||||
});
|
||||
|
||||
window.addEventListener("message", (event) => {
|
||||
// ensure event is from a trusted source
|
||||
if (event.origin !== window.origin)
|
||||
return;
|
||||
|
||||
window.qBittorrent.AddTorrent.populateMetadata(event.data);
|
||||
window.qBittorrent.AddTorrent.metadataCompleted(false);
|
||||
});
|
||||
|
||||
window.qBittorrent.pathAutofill.attachPathAutofill();
|
||||
window.qBittorrent.TorrentContent.init("addTorrentFilesTableDiv", window.qBittorrent.DynamicTable.AddTorrentFilesTable);
|
||||
|
||||
if (fetchMetadata)
|
||||
window.qBittorrent.AddTorrent.loadMetadata(source);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.column {
|
||||
width: 50%;
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.column {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
td.noWrap {
|
||||
white-space: normal;
|
||||
width: 125px;
|
||||
word-wrap: break-word;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
td.fullWidth {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
#btn-group-tagsSelect button {
|
||||
background-color: initial;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ul id="torrentFilesMenu" class="contextMenu">
|
||||
<li class="separator">
|
||||
<a href="#FilePrio" class="arrow-right"><span style="display: inline-block; width: 16px;"></span> QBT_TR(Priority)QBT_TR[CONTEXT=AddNewTorrentDialog]</a>
|
||||
<ul>
|
||||
<li><a href="#FilePrioIgnore"><span style="display: inline-block; width: 16px;"></span> QBT_TR(Do not download)QBT_TR[CONTEXT=AddNewTorrentDialog]</a></li>
|
||||
<li><a href="#FilePrioNormal"><span style="display: inline-block; width: 16px;"></span> QBT_TR(Normal)QBT_TR[CONTEXT=AddNewTorrentDialog]</a></li>
|
||||
<li><a href="#FilePrioHigh"><span style="display: inline-block; width: 16px;"></span> QBT_TR(High)QBT_TR[CONTEXT=AddNewTorrentDialog]</a></li>
|
||||
<li><a href="#FilePrioMaximum"><span style="display: inline-block; width: 16px;"></span> QBT_TR(Maximum)QBT_TR[CONTEXT=AddNewTorrentDialog]</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<iframe id="upload_frame" name="upload_frame" class="invisible" title="" src="about:blank"></iframe>
|
||||
<form action="api/v2/torrents/add" enctype="multipart/form-data" method="post" id="uploadForm" style="text-align: center; padding: 10px 12px;" target="upload_frame" autocorrect="off" autocapitalize="none">
|
||||
<input type="hidden" id="urls" name="urls">
|
||||
<input type="hidden" id="filePriorities" name="filePriorities">
|
||||
<div class="container">
|
||||
<div class="column">
|
||||
<fieldset class="settings" style="text-align: left;">
|
||||
<legend>QBT_TR(Save at)QBT_TR[CONTEXT=AddNewTorrentDialog]</legend>
|
||||
<table style="width: 100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="noWrap">
|
||||
<label for="autoTMM">QBT_TR(Torrent Management Mode:)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td class="fullWidth">
|
||||
<select id="autoTMM" name="autoTMM" onchange="qBittorrent.AddTorrent.changeTMM()">
|
||||
<option selected value="false">QBT_TR(Manual)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
<option value="true">QBT_TR(Automatic)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="noWrap">
|
||||
<label for="savepath">QBT_TR(Save files to location:)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td class="fullWidth">
|
||||
<input type="text" id="savepath" name="savepath" class="pathDirectory" style="width: 100%;">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<fieldset class="settings">
|
||||
<legend>
|
||||
<input type="checkbox" id="useDownloadPath">
|
||||
<input type="hidden" id="useDownloadPathHidden" name="useDownloadPath">
|
||||
<label for="useDownloadPath">QBT_TR(Use another path for incomplete torrent)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</legend>
|
||||
<div class="formRow" style="display: flex; align-items: center;">
|
||||
<label for="downloadPath">QBT_TR(Save path:)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
<input type="text" id="downloadPath" name="downloadPath" class="pathDirectory" disabled style="flex-grow: 1; margin-left: 5px; margin-right: 5px;">
|
||||
</div>
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
<fieldset class="settings" style="text-align: left;">
|
||||
<legend>QBT_TR(Torrent settings)QBT_TR[CONTEXT=AddNewTorrentDialog]</legend>
|
||||
<table style="width: 100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="noWrap">
|
||||
<label for="rename">QBT_TR(Rename torrent)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td class="fullWidth">
|
||||
<input type="text" id="rename" name="rename" style="width: 100%; max-width: 16em;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="noWrap">
|
||||
<label id="categoryLabel" for="categorySelect">QBT_TR(Category:)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td class="fullWidth">
|
||||
<div class="select-watched-folder-editable">
|
||||
<select id="categorySelect" onchange="qBittorrent.AddTorrent.changeCategorySelect(this)">
|
||||
<option selected value="\other"></option>
|
||||
</select>
|
||||
<input type="text" name="category" id="category" aria-labelledby="categoryLabel">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="noWrap"></td>
|
||||
<td class="fullWidth">
|
||||
<input type="checkbox" id="setDefaultCategory">
|
||||
<label for="setDefaultCategory">QBT_TR(Set as default category)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="noWrap">
|
||||
<label id="tagsLabel" for="tagsSelect">QBT_TR(Tags:)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td class="fullWidth">
|
||||
<div>
|
||||
<input type="hidden" id="tags" name="tags">
|
||||
<select id="tagsSelect" name="tagsSelect" multiple></select>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="noWrap">
|
||||
<label for="startTorrent">QBT_TR(Start torrent)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td class="fullWidth">
|
||||
<input type="hidden" id="startTorrentHidden" name="stopped">
|
||||
<input type="checkbox" id="startTorrent">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="noWrap">
|
||||
<label for="stopCondition">QBT_TR(Stop condition:)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td class="fullWidth">
|
||||
<select id="stopCondition" name="stopCondition">
|
||||
<option selected value="None">QBT_TR(None)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
<option value="MetadataReceived">QBT_TR(Metadata received)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
<option value="FilesChecked">QBT_TR(Files checked)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="noWrap">
|
||||
<label for="addToTopOfQueue">QBT_TR(Add to top of queue)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td class="fullWidth">
|
||||
<input type="checkbox" id="addToTopOfQueue" name="addToTopOfQueue" value="true">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="noWrap">
|
||||
<label for="skip_checking">QBT_TR(Skip hash check)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td class="fullWidth">
|
||||
<input type="checkbox" id="skip_checking" name="skip_checking" value="true">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="noWrap">
|
||||
<label for="sequentialDownload">QBT_TR(Download in sequential order)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td class="fullWidth">
|
||||
<input type="checkbox" id="sequentialDownload" name="sequentialDownload" value="true">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="noWrap">
|
||||
<label for="firstLastPiecePrio">QBT_TR(Download first and last pieces first)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td class="fullWidth">
|
||||
<input type="checkbox" id="firstLastPiecePrio" name="firstLastPiecePrio" value="true">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="noWrap">
|
||||
<label for="contentLayout">QBT_TR(Content layout:)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td class="fullWidth">
|
||||
<select id="contentLayout" name="contentLayout">
|
||||
<option selected value="Original">QBT_TR(Original)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
<option value="Subfolder">QBT_TR(Create subfolder)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
<option value="NoSubfolder">QBT_TR(Don't create subfolder)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="noWrap">
|
||||
<label for="dlLimitText">QBT_TR(Limit download rate)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td class="fullWidth">
|
||||
<input type="hidden" id="dlLimitHidden" name="dlLimit">
|
||||
<input type="text" id="dlLimitText" placeholder="KiB/s">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="noWrap">
|
||||
<label for="upLimitText">QBT_TR(Limit upload rate)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td class="fullWidth">
|
||||
<input type="hidden" id="upLimitHidden" name="upLimit">
|
||||
<input type="text" id="upLimitText" placeholder="KiB/s">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="settings" style="text-align: left;">
|
||||
<legend>QBT_TR(Torrent information)QBT_TR[CONTEXT=AddNewTorrentDialog]</legend>
|
||||
<table style="width: 100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="noWrap">
|
||||
<label for="size">QBT_TR(Size:)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td class="fullWidth">
|
||||
<span id="size">QBT_TR(Not available)QBT_TR[CONTEXT=AddNewTorrentDialog]</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="noWrap">
|
||||
<label for="createdDate">QBT_TR(Date:)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td class="fullWidth">
|
||||
<span id="createdDate">QBT_TR(Not available)QBT_TR[CONTEXT=AddNewTorrentDialog]</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="noWrap">
|
||||
<label for="infoHashV1">QBT_TR(Info hash v1:)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td class="fullWidth">
|
||||
<span id="infoHashV1">QBT_TR(Not available)QBT_TR[CONTEXT=AddNewTorrentDialog]</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="noWrap">
|
||||
<label for="infoHashV2">QBT_TR(Info hash v2:)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td class="fullWidth">
|
||||
<span id="infoHashV2">QBT_TR(Not available)QBT_TR[CONTEXT=AddNewTorrentDialog]</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="noWrap">
|
||||
<label for="comment">QBT_TR(Comment:)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td class="fullWidth">
|
||||
<textarea id="comment" rows="6" readonly style="width: calc(100% - 10px); resize: none;"></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="column" style="max-height: 750px; overflow-y: auto;">
|
||||
<fieldset class="settings" style="text-align: left; display: flex; flex-direction: column; height: 100%; box-sizing: border-box;">
|
||||
<legend style="width: fit-content;">QBT_TR(Files)QBT_TR[CONTEXT=AddNewTorrentDialog]</legend>
|
||||
<div style="text-align: right; padding-bottom: 10px;">
|
||||
<input type="text" id="torrentFilesFilterInput" placeholder="QBT_TR(Filter files...)QBT_TR[CONTEXT=AddNewTorrentDialog]" aria-label="QBT_TR(Filter files...)QBT_TR[CONTEXT=AddNewTorrentDialog]" autocorrect="off" autocapitalize="none">
|
||||
</div>
|
||||
<div id="torrentFiles" style="overflow: auto;">
|
||||
<div id="torrentFilesTableFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
|
||||
<table class="dynamicTable" style="position:relative;">
|
||||
<thead>
|
||||
<tr class="dynamicTableHeader"></tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
<div id="addTorrentFilesTableDiv" class="dynamicTableDiv">
|
||||
<table class="dynamicTable">
|
||||
<thead>
|
||||
<tr class="dynamicTableHeader"></tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div id="submitbutton" style="display: flex; margin-top: 10px; align-items: center; position: relative;">
|
||||
<div id="loadingSpinner" class="mochaSpinner" style="display: block; bottom: 2px;"></div>
|
||||
<div id="errorIcon" class="mochaErrorIcon invisible" style="bottom: 2px;"></div>
|
||||
<span id="metadataStatus" style="padding-left: 28px;">Retrieving metadata</span>
|
||||
<button id="saveTorrent" type="button" class="invisible" style="font-size: 1em;">QBT_TR(Save as .torrent file)QBT_TR[CONTEXT=AddNewTorrentDialog]</button>
|
||||
<!-- maintains button's relative position after metadataStatus element is deleted -->
|
||||
<span> </span>
|
||||
<div style="position: absolute; left: 50%; transform: translateX(-50%);">
|
||||
<button type="submit" style="font-size: 1em;">QBT_TR(Add Torrent)QBT_TR[CONTEXT=AddNewTorrentDialog]</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -155,7 +155,8 @@ Required by:
|
|||
width: 5px;
|
||||
}
|
||||
|
||||
#desktopNavbar li ul li a {
|
||||
#desktopNavbar li ul li a,
|
||||
#desktopNavbar li ul li div.anchor {
|
||||
color: var(--color-text-default);
|
||||
font-weight: normal;
|
||||
min-width: 155px;
|
||||
|
@ -163,7 +164,8 @@ Required by:
|
|||
position: relative;
|
||||
}
|
||||
|
||||
#desktopNavbar li ul li a:hover {
|
||||
#desktopNavbar li ul li a:hover,
|
||||
#desktopNavbar li ul li div.anchor:hover {
|
||||
background-color: var(--color-background-hover);
|
||||
color: var(--color-text-white);
|
||||
}
|
||||
|
|
|
@ -184,6 +184,16 @@ div.mochaToolbarWrapper.bottom {
|
|||
width: 16px;
|
||||
}
|
||||
|
||||
.mochaErrorIcon {
|
||||
background: url("../images/error.svg") no-repeat;
|
||||
background-size: 16px;
|
||||
bottom: 7px;
|
||||
height: 16px;
|
||||
left: 6px;
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.mochaIframe {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -676,6 +676,17 @@ td.generalLabel {
|
|||
width: 1px;
|
||||
}
|
||||
|
||||
td.fullWidth {
|
||||
box-sizing: border-box;
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
td.noWrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#tristate_cb {
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
|
@ -839,7 +850,8 @@ td.statusBarSeparator {
|
|||
color: var(--color-text-green);
|
||||
}
|
||||
|
||||
#torrentFilesTableDiv .dynamicTable tr.nonAlt:hover {
|
||||
#torrentFilesTableDiv .dynamicTable tr.nonAlt:hover,
|
||||
#addTorrentFilesTableDiv .dynamicTable tr.nonAlt:hover {
|
||||
background-color: var(--color-background-hover);
|
||||
color: var(--color-text-white);
|
||||
}
|
||||
|
|
|
@ -3,13 +3,11 @@
|
|||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>QBT_TR(Add Torrent Links)QBT_TR[CONTEXT=downloadFromURL]</title>
|
||||
<title>QBT_TR(Add Torrent Links)QBT_TR[CONTEXT=DownloadFromURLDialog]</title>
|
||||
<link rel="stylesheet" href="css/style.css?v=${CACHEID}" type="text/css">
|
||||
<link rel="stylesheet" href="css/Window.css?v=${CACHEID}" type="text/css">
|
||||
<script defer src="scripts/localpreferences.js?v=${CACHEID}"></script>
|
||||
<script defer src="scripts/color-scheme.js?v=${CACHEID}"></script>
|
||||
<script defer src="scripts/download.js?v=${CACHEID}"></script>
|
||||
<script defer src="scripts/pathAutofill.js?v=${CACHEID}"></script>
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
|
@ -23,6 +21,8 @@
|
|||
}
|
||||
});
|
||||
|
||||
document.getElementById("urls").focus();
|
||||
|
||||
const encodedUrls = new URLSearchParams(window.location.search).get("urls");
|
||||
if (encodedUrls !== null) {
|
||||
const urls = encodedUrls.split("|").map(decodeURIComponent);
|
||||
|
@ -30,172 +30,42 @@
|
|||
document.getElementById("urls").value = urls.join("\n");
|
||||
}
|
||||
|
||||
let submitted = false;
|
||||
document.getElementById("submitButton").addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
document.getElementById("downloadForm").addEventListener("submit", (event) => {
|
||||
document.getElementById("startTorrentHidden").value = document.getElementById("startTorrent").checked ? "false" : "true";
|
||||
const urls = document.getElementById("urls").value.split("\n").map(s => s.trim()).filter(s => (s.length > 0));
|
||||
if (urls.length === 0)
|
||||
return;
|
||||
|
||||
document.getElementById("dlLimitHidden").value = Number(document.getElementById("dlLimitText").value) * 1024;
|
||||
document.getElementById("upLimitHidden").value = Number(document.getElementById("upLimitText").value) * 1024;
|
||||
for (const url of urls)
|
||||
window.parent.qBittorrent.Client.createAddTorrentWindow("QBT_TR(Magnet link)QBT_TR[CONTEXT=DownloadFromURLDialog]", url);
|
||||
|
||||
document.getElementById("download_spinner").style.display = "block";
|
||||
submitted = true;
|
||||
window.parent.qBittorrent.Client.closeFrameWindow(window);
|
||||
});
|
||||
|
||||
document.getElementById("download_frame").addEventListener("load", (event) => {
|
||||
if (submitted)
|
||||
window.parent.qBittorrent.Client.closeFrameWindow(window);
|
||||
window.addEventListener("keydown", (event) => {
|
||||
switch (event.key) {
|
||||
case "Escape":
|
||||
event.preventDefault();
|
||||
window.parent.qBittorrent.Client.closeFrameWindow(window);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
window.qBittorrent.pathAutofill.attachPathAutofill();
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<iframe id="download_frame" name="download_frame" class="invisible" title="" src="about:blank"></iframe>
|
||||
<form action="api/v2/torrents/add" enctype="multipart/form-data" method="post" id="downloadForm" style="text-align: center;" target="download_frame" autocorrect="off" autocapitalize="none">
|
||||
<div style="text-align: center;">
|
||||
<br>
|
||||
<h2><label for="urls">QBT_TR(Add torrent links)QBT_TR[CONTEXT=AddNewTorrentDialog]</label></h2>
|
||||
<textarea id="urls" rows="10" name="urls"></textarea>
|
||||
<p><i>QBT_TR(One link per line (HTTP links, Magnet links and info-hashes are supported))QBT_TR[CONTEXT=AddNewTorrentDialog]</i></p>
|
||||
<fieldset class="settings" style="border: 0; text-align: left; margin-top: 6px;">
|
||||
<legend>QBT_TR(Torrent options)QBT_TR[CONTEXT=AddNewTorrentDialog]</legend>
|
||||
<table style="margin: auto;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="autoTMM">QBT_TR(Torrent Management Mode:)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td>
|
||||
<select id="autoTMM" name="autoTMM" onchange="qBittorrent.Download.changeTMM(this)">
|
||||
<option selected value="false">QBT_TR(Manual)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
<option value="true">QBT_TR(Automatic)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="savepath">QBT_TR(Save files to location:)QBT_TR[CONTEXT=HttpServer]</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="savepath" name="savepath" class="pathDirectory" style="width: 16em;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="rename">QBT_TR(Rename torrent)QBT_TR[CONTEXT=HttpServer]</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="rename" name="rename" style="width: 16em;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label id="categoryLabel" for="categorySelect">QBT_TR(Category:)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td>
|
||||
<div class="select-watched-folder-editable">
|
||||
<select id="categorySelect" onchange="qBittorrent.Download.changeCategorySelect(this)">
|
||||
<option selected value="\other"></option>
|
||||
</select>
|
||||
<input type="text" name="category" aria-labelledby="categoryLabel">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="startTorrent">QBT_TR(Start torrent)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="hidden" id="startTorrentHidden" name="stopped">
|
||||
<input type="checkbox" id="startTorrent">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="addToTopOfQueue">QBT_TR(Add to top of queue)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" id="addToTopOfQueue" name="addToTopOfQueue" value="true">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="stopCondition">QBT_TR(Stop condition:)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td>
|
||||
<select id="stopCondition" name="stopCondition">
|
||||
<option selected value="None">QBT_TR(None)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
<option value="MetadataReceived">QBT_TR(Metadata received)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
<option value="FilesChecked">QBT_TR(Files checked)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="skip_checking">QBT_TR(Skip hash check)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" id="skip_checking" name="skip_checking" value="true">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="contentLayout">QBT_TR(Content layout:)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td>
|
||||
<select id="contentLayout" name="contentLayout">
|
||||
<option selected value="Original">QBT_TR(Original)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
<option value="Subfolder">QBT_TR(Create subfolder)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
<option value="NoSubfolder">QBT_TR(Don't create subfolder)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="sequentialDownload">QBT_TR(Download in sequential order)QBT_TR[CONTEXT=TransferListWidget]</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" id="sequentialDownload" name="sequentialDownload" value="true">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="firstLastPiecePrio">QBT_TR(Download first and last pieces first)QBT_TR[CONTEXT=TransferListWidget]</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" id="firstLastPiecePrio" name="firstLastPiecePrio" value="true">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="dlLimitText">QBT_TR(Limit download rate)QBT_TR[CONTEXT=HttpServer]</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="hidden" id="dlLimitHidden" name="dlLimit">
|
||||
<input type="text" id="dlLimitText" style="width: 16em;" placeholder="KiB/s">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="upLimitText">QBT_TR(Limit upload rate)QBT_TR[CONTEXT=HttpServer]</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="hidden" id="upLimitHidden" name="upLimit">
|
||||
<input type="text" id="upLimitText" style="width: 16em;" placeholder="KiB/s">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="submitbutton" style="margin-top: 12px; text-align: center;">
|
||||
<button type="submit" id="submitButton">QBT_TR(Download)QBT_TR[CONTEXT=downloadFromURL]</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div style="text-align: center;">
|
||||
<br>
|
||||
<h2 class="vcenter">QBT_TR(Add torrent links)QBT_TR[CONTEXT=DownloadFromURLDialog]</h2>
|
||||
<textarea id="urls" name="urls" rows="10" required style="width: 80%;" aria-label="QBT_TR(URLs)QBT_TR[CONTEXT=DownloadFromURLDialog]"></textarea>
|
||||
<p><i>QBT_TR(One link per line (HTTP links, Magnet links and info-hashes are supported))QBT_TR[CONTEXT=DownloadFromURLDialog]</i></p>
|
||||
<div id="submitbutton" style="margin-top: 12px; text-align: center;">
|
||||
<button type="submit" id="submitButton">QBT_TR(Download)QBT_TR[CONTEXT=DownloadFromURLDialog]</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="download_spinner" class="mochaSpinner"></div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
@ -39,6 +39,7 @@
|
|||
<script defer src="scripts/file-tree.js?v=${CACHEID}"></script>
|
||||
<script defer src="scripts/dynamicTable.js?locale=${LANG}&v=${CACHEID}"></script>
|
||||
<script defer src="scripts/rename-files.js?v=${CACHEID}"></script>
|
||||
<script defer src="scripts/torrent-content.js?v=${CACHEID}"></script>
|
||||
<script defer src="scripts/client.js?locale=${LANG}&v=${CACHEID}"></script>
|
||||
<script defer src="scripts/contextmenu.js?locale=${LANG}&v=${CACHEID}"></script>
|
||||
<script defer src="scripts/pathAutofill.js?v=${CACHEID}"></script>
|
||||
|
@ -59,7 +60,15 @@
|
|||
<li>
|
||||
<a class="returnFalse">QBT_TR(File)QBT_TR[CONTEXT=MainWindow]</a>
|
||||
<ul>
|
||||
<li><a id="uploadLink"><img class="MyMenuIcon" alt="QBT_TR(Add Torrent File...)QBT_TR[CONTEXT=MainWindow]" src="images/list-add.svg" width="16" height="16">QBT_TR(Add Torrent File...)QBT_TR[CONTEXT=MainWindow]</a></li>
|
||||
<li>
|
||||
<div id="uploadLink" class="anchor">
|
||||
<label id="uploadLinkLabel" for="fileselectLink" style="cursor: pointer;">
|
||||
<img class="MyMenuIcon" alt="QBT_TR(Add Torrent File...)QBT_TR[CONTEXT=MainWindow]" src="images/list-add.svg" width="16" height="16">
|
||||
<span>QBT_TR(Add Torrent File...)QBT_TR[CONTEXT=MainWindow]</span>
|
||||
</label>
|
||||
<input type="file" id="fileselectLink" class="fileselect" accept=".torrent, application/x-bittorrent" name="fileselect[]" multiple style="display: none;">
|
||||
</div>
|
||||
</li>
|
||||
<li><a id="downloadLink"><img class="MyMenuIcon" alt="QBT_TR(Add Torrent Link...)QBT_TR[CONTEXT=MainWindow]" src="images/insert-link.svg" width="16" height="16">QBT_TR(Add Torrent Link...)QBT_TR[CONTEXT=MainWindow]</a></li>
|
||||
<li class="divider"><a id="logoutLink"><img class="MyMenuIcon" alt="QBT_TR(Logout)QBT_TR[CONTEXT=HttpServer]" src="images/system-log-out.svg" width="16" height="16">QBT_TR(Logout)QBT_TR[CONTEXT=HttpServer]</a></li>
|
||||
<li><a id="shutdownLink"><img class="MyMenuIcon" alt="QBT_TR(Exit qBittorrent)QBT_TR[CONTEXT=HttpServer]" src="images/application-exit.svg" width="16" height="16">QBT_TR(Exit qBittorrent)QBT_TR[CONTEXT=HttpServer]</a></li>
|
||||
|
@ -113,7 +122,12 @@
|
|||
</div>
|
||||
<div id="mochaToolbar">
|
||||
|
||||
<a id="uploadButton"><img class="mochaToolButton" title="QBT_TR(Add Torrent File...)QBT_TR[CONTEXT=MainWindow]" src="images/list-add.svg" alt="QBT_TR(Add Torrent File...)QBT_TR[CONTEXT=MainWindow]" width="24" height="24"></a>
|
||||
<div id="uploadButton" style="display: inline-block;">
|
||||
<label for="fileselectButton" style="cursor: pointer;">
|
||||
<img class="mochaToolButton" title="QBT_TR(Add Torrent File...)QBT_TR[CONTEXT=MainWindow]" src="images/list-add.svg" alt="QBT_TR(Add Torrent File...)QBT_TR[CONTEXT=MainWindow]" width="24" height="24">
|
||||
</label>
|
||||
<input type="file" id="fileselectButton" class="fileselect" accept=".torrent, application/x-bittorrent" name="fileselect[]" multiple style="display: none;">
|
||||
</div>
|
||||
<a id="downloadButton"><img class="mochaToolButton" title="QBT_TR(Add Torrent Link...)QBT_TR[CONTEXT=MainWindow]" src="images/insert-link.svg" alt="QBT_TR(Add Torrent Link...)QBT_TR[CONTEXT=MainWindow]" width="24" height="24"></a>
|
||||
<a id="deleteButton"><img class="mochaToolButton" title="QBT_TR(Remove)QBT_TR[CONTEXT=TransferListWidget]" src="images/list-remove.svg" alt="QBT_TR(Remove)QBT_TR[CONTEXT=TransferListWidget]" width="24" height="24"></a>
|
||||
<a id="startButton" class="divider"><img class="mochaToolButton" title="QBT_TR(Start)QBT_TR[CONTEXT=TransferListWidget]" src="images/torrent-start.svg" alt="QBT_TR(Start)QBT_TR[CONTEXT=TransferListWidget]" width="24" height="24"></a>
|
||||
|
|
338
src/webui/www/private/scripts/addtorrent.js
Normal file
338
src/webui/www/private/scripts/addtorrent.js
Normal file
|
@ -0,0 +1,338 @@
|
|||
/*
|
||||
* MIT License
|
||||
* Copyright (c) 2008 Ishan Arora <ishan@qbittorrent.org>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
window.qBittorrent ??= {};
|
||||
window.qBittorrent.AddTorrent ??= (() => {
|
||||
const exports = () => {
|
||||
return {
|
||||
changeCategorySelect: changeCategorySelect,
|
||||
changeTMM: changeTMM,
|
||||
loadMetadata: loadMetadata,
|
||||
metadataCompleted: metadataCompleted,
|
||||
populateMetadata: populateMetadata,
|
||||
setWindowId: setWindowId,
|
||||
submitForm: submitForm
|
||||
};
|
||||
};
|
||||
|
||||
let defaultSavePath = "";
|
||||
let defaultTempPath = "";
|
||||
let defaultTempPathEnabled = false;
|
||||
let windowId = "";
|
||||
let source = "";
|
||||
|
||||
const LocalPreferences = new window.qBittorrent.LocalPreferences.LocalPreferences();
|
||||
|
||||
const getCategories = () => {
|
||||
const defaultCategory = LocalPreferences.get("add_torrent_default_category", "");
|
||||
const categorySelect = document.getElementById("categorySelect");
|
||||
for (const name of window.parent.qBittorrent.Client.categoryMap.keys()) {
|
||||
const option = document.createElement("option");
|
||||
option.value = name;
|
||||
option.textContent = name;
|
||||
option.selected = name === defaultCategory;
|
||||
categorySelect.appendChild(option);
|
||||
}
|
||||
|
||||
if (defaultCategory !== "")
|
||||
changeCategorySelect(categorySelect);
|
||||
};
|
||||
|
||||
const getTags = () => {
|
||||
const tagsSelect = document.getElementById("tagsSelect");
|
||||
for (const tag of window.parent.qBittorrent.Client.tagMap.keys()) {
|
||||
const option = document.createElement("option");
|
||||
option.value = tag;
|
||||
option.textContent = tag;
|
||||
tagsSelect.appendChild(option);
|
||||
}
|
||||
|
||||
new vanillaSelectBox("#tagsSelect", {
|
||||
maxHeight: 200,
|
||||
search: false,
|
||||
disableSelectAll: true,
|
||||
translations: {
|
||||
all: (window.parent.qBittorrent.Client.tagMap.length === 0) ? "" : "QBT_TR(All)QBT_TR[CONTEXT=AddNewTorrentDialog]",
|
||||
},
|
||||
keepInlineStyles: false
|
||||
});
|
||||
};
|
||||
|
||||
const getPreferences = () => {
|
||||
const pref = window.parent.qBittorrent.Cache.preferences.get();
|
||||
|
||||
defaultSavePath = pref.save_path;
|
||||
defaultTempPath = pref.temp_path;
|
||||
defaultTempPathEnabled = pref.temp_path_enabled;
|
||||
document.getElementById("startTorrent").checked = !pref.add_stopped_enabled;
|
||||
document.getElementById("addToTopOfQueue").checked = pref.add_to_top_of_queue;
|
||||
|
||||
const autoTMM = document.getElementById("autoTMM");
|
||||
if (pref.auto_tmm_enabled) {
|
||||
autoTMM.selectedIndex = 1;
|
||||
document.getElementById("savepath").disabled = true;
|
||||
}
|
||||
else {
|
||||
autoTMM.selectedIndex = 0;
|
||||
}
|
||||
changeTMM();
|
||||
|
||||
if (pref.torrent_stop_condition === "MetadataReceived")
|
||||
document.getElementById("stopCondition").selectedIndex = 1;
|
||||
else if (pref.torrent_stop_condition === "FilesChecked")
|
||||
document.getElementById("stopCondition").selectedIndex = 2;
|
||||
else
|
||||
document.getElementById("stopCondition").selectedIndex = 0;
|
||||
|
||||
if (pref.torrent_content_layout === "Subfolder")
|
||||
document.getElementById("contentLayout").selectedIndex = 1;
|
||||
else if (pref.torrent_content_layout === "NoSubfolder")
|
||||
document.getElementById("contentLayout").selectedIndex = 2;
|
||||
else
|
||||
document.getElementById("contentLayout").selectedIndex = 0;
|
||||
};
|
||||
|
||||
const categorySavePath = (categoryName) => {
|
||||
const category = window.parent.qBittorrent.Client.categoryMap.get(categoryName);
|
||||
return (category === undefined) ? defaultSavePath : (category.savePath || `${defaultSavePath}/${categoryName}`);
|
||||
};
|
||||
|
||||
const categoryDownloadPath = (categoryName) => {
|
||||
const category = window.parent.qBittorrent.Client.categoryMap.get(categoryName);
|
||||
if (category === undefined)
|
||||
return defaultTempPath;
|
||||
if (category.downloadPath === false)
|
||||
return "";
|
||||
return category.downloadPath || `${defaultTempPath}/${categoryName}`;
|
||||
};
|
||||
|
||||
const categoryDownloadPathEnabled = (categoryName) => {
|
||||
const category = window.parent.qBittorrent.Client.categoryMap.get(categoryName);
|
||||
if ((category === undefined) || (category.downloadPath === null))
|
||||
return defaultTempPathEnabled;
|
||||
return category.downloadPath !== false;
|
||||
};
|
||||
|
||||
const changeCategorySelect = (item) => {
|
||||
const categoryName = item.value;
|
||||
if (categoryName === "\\other") {
|
||||
item.nextElementSibling.hidden = false;
|
||||
item.nextElementSibling.value = "";
|
||||
item.nextElementSibling.select();
|
||||
|
||||
if (isAutoTMMEnabled()) {
|
||||
document.getElementById("savepath").value = defaultSavePath;
|
||||
|
||||
const downloadPathEnabled = categoryDownloadPathEnabled(categoryName);
|
||||
document.getElementById("useDownloadPath").checked = downloadPathEnabled;
|
||||
changeUseDownloadPath(downloadPathEnabled);
|
||||
}
|
||||
}
|
||||
else {
|
||||
item.nextElementSibling.hidden = true;
|
||||
const text = item.options[item.selectedIndex].textContent;
|
||||
item.nextElementSibling.value = text;
|
||||
|
||||
if (isAutoTMMEnabled()) {
|
||||
document.getElementById("savepath").value = categorySavePath(categoryName);
|
||||
|
||||
const downloadPathEnabled = categoryDownloadPathEnabled(categoryName);
|
||||
document.getElementById("useDownloadPath").checked = downloadPathEnabled;
|
||||
changeUseDownloadPath(downloadPathEnabled);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const changeTagsSelect = (element) => {
|
||||
const tags = [...element.options].filter(opt => opt.selected).map(opt => opt.value);
|
||||
document.getElementById("tags").value = tags.join(",");
|
||||
};
|
||||
|
||||
const isAutoTMMEnabled = () => {
|
||||
return document.getElementById("autoTMM").selectedIndex === 1;
|
||||
};
|
||||
|
||||
const changeTMM = () => {
|
||||
const autoTMMEnabled = isAutoTMMEnabled();
|
||||
const savepath = document.getElementById("savepath");
|
||||
const useDownloadPath = document.getElementById("useDownloadPath");
|
||||
|
||||
if (autoTMMEnabled) {
|
||||
const categorySelect = document.getElementById("categorySelect");
|
||||
const categoryName = categorySelect.options[categorySelect.selectedIndex].value;
|
||||
savepath.value = categorySavePath(categoryName);
|
||||
useDownloadPath.checked = categoryDownloadPathEnabled(categoryName);
|
||||
}
|
||||
else {
|
||||
savepath.value = defaultSavePath;
|
||||
useDownloadPath.checked = defaultTempPathEnabled;
|
||||
}
|
||||
|
||||
savepath.disabled = autoTMMEnabled;
|
||||
useDownloadPath.disabled = autoTMMEnabled;
|
||||
|
||||
// only submit this value when using manual tmm
|
||||
document.getElementById("useDownloadPathHidden").disabled = autoTMMEnabled;
|
||||
|
||||
changeUseDownloadPath(useDownloadPath.checked);
|
||||
};
|
||||
|
||||
const changeUseDownloadPath = (enabled) => {
|
||||
const downloadPath = document.getElementById("downloadPath");
|
||||
if (isAutoTMMEnabled()) {
|
||||
const categorySelect = document.getElementById("categorySelect");
|
||||
const categoryName = categorySelect.options[categorySelect.selectedIndex].value;
|
||||
downloadPath.value = enabled ? categoryDownloadPath(categoryName) : "";
|
||||
downloadPath.disabled = true;
|
||||
}
|
||||
else {
|
||||
downloadPath.value = enabled ? defaultTempPath : "";
|
||||
downloadPath.disabled = !enabled;
|
||||
}
|
||||
};
|
||||
|
||||
let loadMetadataTimer = -1;
|
||||
const loadMetadata = (sourceUrl = undefined) => {
|
||||
if (sourceUrl !== undefined)
|
||||
source = sourceUrl;
|
||||
|
||||
fetch("api/v2/torrents/fetchMetadata", {
|
||||
method: "POST",
|
||||
body: new URLSearchParams({
|
||||
source: source
|
||||
})
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
metadataFailed();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
populateMetadata(data);
|
||||
|
||||
if (response.status === 200)
|
||||
metadataCompleted();
|
||||
else
|
||||
loadMetadataTimer = loadMetadata.delay(1000);
|
||||
});
|
||||
};
|
||||
|
||||
const metadataCompleted = (showDownloadButton = true) => {
|
||||
clearTimeout(loadMetadataTimer);
|
||||
loadMetadataTimer = -1;
|
||||
|
||||
document.getElementById("metadataStatus").destroy();
|
||||
document.getElementById("loadingSpinner").style.display = "none";
|
||||
|
||||
if (showDownloadButton)
|
||||
document.getElementById("saveTorrent").classList.remove("invisible");
|
||||
};
|
||||
|
||||
const metadataFailed = () => {
|
||||
clearTimeout(loadMetadataTimer);
|
||||
loadMetadataTimer = -1;
|
||||
|
||||
document.getElementById("metadataStatus").textContent = "Metadata retrieval failed";
|
||||
document.getElementById("metadataStatus").classList.add("red");
|
||||
document.getElementById("loadingSpinner").style.display = "none";
|
||||
document.getElementById("errorIcon").classList.remove("invisible");
|
||||
};
|
||||
|
||||
const populateMetadata = (metadata) => {
|
||||
// update window title
|
||||
if (metadata.info?.name !== undefined)
|
||||
window.parent.document.getElementById(`${windowId}_title`).textContent = metadata.info.name;
|
||||
|
||||
const notAvailable = "QBT_TR(Not available)QBT_TR[CONTEXT=AddNewTorrentDialog]";
|
||||
const notApplicable = "QBT_TR(N/A)QBT_TR[CONTEXT=AddNewTorrentDialog]";
|
||||
document.getElementById("infoHashV1").textContent = (metadata.infohash_v1 === undefined) ? notAvailable : (metadata.infohash_v1 || notApplicable);
|
||||
document.getElementById("infoHashV2").textContent = (metadata.infohash_v2 === undefined) ? notAvailable : (metadata.infohash_v2 || notApplicable);
|
||||
|
||||
if (metadata.info?.length !== undefined)
|
||||
document.getElementById("size").textContent = window.qBittorrent.Misc.friendlyUnit(metadata.info.length, false);
|
||||
if ((metadata.creation_date !== undefined) && (metadata.creation_date > 1))
|
||||
document.getElementById("createdDate").textContent = new Date(metadata.creation_date * 1000).toLocaleString();
|
||||
if (metadata.comment !== undefined)
|
||||
document.getElementById("comment").textContent = metadata.comment;
|
||||
|
||||
if (metadata.info?.files !== undefined) {
|
||||
const files = metadata.info.files.map((file, index) => ({
|
||||
index: index,
|
||||
name: file.path,
|
||||
size: file.length,
|
||||
priority: window.qBittorrent.FileTree.FilePriority.Normal,
|
||||
}));
|
||||
window.qBittorrent.TorrentContent.updateData(files);
|
||||
}
|
||||
};
|
||||
|
||||
const setWindowId = (id) => {
|
||||
windowId = id;
|
||||
};
|
||||
|
||||
const submitForm = () => {
|
||||
document.getElementById("startTorrentHidden").value = document.getElementById("startTorrent").checked ? "false" : "true";
|
||||
|
||||
document.getElementById("dlLimitHidden").value = Number(document.getElementById("dlLimitText").value) * 1024;
|
||||
document.getElementById("upLimitHidden").value = Number(document.getElementById("upLimitText").value) * 1024;
|
||||
|
||||
document.getElementById("filePriorities").value = [...document.getElementsByClassName("combo_priority")]
|
||||
.filter((el) => !window.qBittorrent.TorrentContent.isFolder(Number(el.dataset.fileId)))
|
||||
.sort((el1, el2) => Number(el1.dataset.fileId) - Number(el2.dataset.fileId))
|
||||
.map((el) => Number(el.value));
|
||||
|
||||
if (!isAutoTMMEnabled())
|
||||
document.getElementById("useDownloadPathHidden").value = document.getElementById("useDownloadPath").checked;
|
||||
|
||||
document.getElementById("loadingSpinner").style.display = "block";
|
||||
|
||||
if (document.getElementById("setDefaultCategory").checked) {
|
||||
const category = document.getElementById("category").value.trim();
|
||||
if (category.length === 0)
|
||||
LocalPreferences.remove("add_torrent_default_category");
|
||||
else
|
||||
LocalPreferences.set("add_torrent_default_category", category);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("load", async (event) => {
|
||||
// user might load this page directly (via browser magnet handler)
|
||||
// so wait for crucial initialization to complete
|
||||
await window.parent.qBittorrent.Client.initializeCaches();
|
||||
|
||||
getPreferences();
|
||||
getCategories();
|
||||
getTags();
|
||||
});
|
||||
|
||||
window.addEventListener("DOMContentLoaded", (event) => {
|
||||
document.getElementById("useDownloadPath").addEventListener("change", (e) => changeUseDownloadPath(e.target.checked));
|
||||
document.getElementById("tagsSelect").addEventListener("change", (e) => changeTagsSelect(e.target));
|
||||
});
|
||||
|
||||
return exports();
|
||||
})();
|
||||
Object.freeze(window.qBittorrent.AddTorrent);
|
|
@ -43,6 +43,8 @@ window.qBittorrent.Client ??= (() => {
|
|||
isShowSearchEngine: isShowSearchEngine,
|
||||
isShowRssReader: isShowRssReader,
|
||||
isShowLogViewer: isShowLogViewer,
|
||||
createAddTorrentWindow: createAddTorrentWindow,
|
||||
uploadTorrentFiles: uploadTorrentFiles,
|
||||
categoryMap: categoryMap,
|
||||
tagMap: tagMap
|
||||
};
|
||||
|
@ -128,6 +130,82 @@ window.qBittorrent.Client ??= (() => {
|
|||
return showingLogViewer;
|
||||
};
|
||||
|
||||
const createAddTorrentWindow = (title, source, metadata = undefined) => {
|
||||
const isFirefox = navigator.userAgent.includes("Firefox");
|
||||
const isSafari = navigator.userAgent.includes("AppleWebKit") && !navigator.userAgent.includes("Chrome");
|
||||
let height = 855;
|
||||
if (isSafari)
|
||||
height -= 40;
|
||||
else if (isFirefox)
|
||||
height -= 10;
|
||||
|
||||
const staticId = "uploadPage";
|
||||
const id = `${staticId}-${encodeURIComponent(source)}`;
|
||||
|
||||
const contentURL = new URL("addtorrent.html", window.location);
|
||||
contentURL.search = new URLSearchParams({
|
||||
v: "${CACHEID}",
|
||||
source: source,
|
||||
fetch: metadata === undefined,
|
||||
windowId: id
|
||||
});
|
||||
|
||||
new MochaUI.Window({
|
||||
id: id,
|
||||
icon: "images/qbittorrent-tray.svg",
|
||||
title: title,
|
||||
loadMethod: "iframe",
|
||||
contentURL: contentURL.toString(),
|
||||
scrollbars: true,
|
||||
maximizable: false,
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: 0,
|
||||
width: loadWindowWidth(staticId, 980),
|
||||
height: loadWindowHeight(staticId, height),
|
||||
onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
|
||||
saveWindowSize(staticId, id);
|
||||
}),
|
||||
onContentLoaded: () => {
|
||||
if (metadata !== undefined)
|
||||
document.getElementById(`${id}_iframe`).contentWindow.postMessage(metadata, window.origin);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const uploadTorrentFiles = (files) => {
|
||||
const fileNames = [];
|
||||
const formData = new FormData();
|
||||
for (const file of files) {
|
||||
fileNames.push(file.name);
|
||||
formData.append("file", file);
|
||||
}
|
||||
|
||||
fetch("api/v2/torrents/parseMetadata", {
|
||||
method: "POST",
|
||||
body: formData
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
alert(await response.text());
|
||||
return;
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
for (const fileName of fileNames) {
|
||||
let title = fileName;
|
||||
const metadata = json[fileName];
|
||||
if (metadata !== undefined)
|
||||
title = metadata.name;
|
||||
|
||||
const hash = metadata.infohash_v2 || metadata.infohash_v1;
|
||||
createAddTorrentWindow(title, hash, metadata);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
alert(`QBT_TR(Unable to parse response.)QBT_TR[CONTEXT=HttpServer] ${error.toString()}`);
|
||||
});
|
||||
};
|
||||
|
||||
return exports();
|
||||
})();
|
||||
Object.freeze(window.qBittorrent.Client);
|
||||
|
@ -1687,32 +1765,11 @@ window.addEventListener("DOMContentLoaded", (event) => {
|
|||
// can't handle folder due to cannot put the filelist (from dropped folder)
|
||||
// to <input> `files` field
|
||||
for (const item of ev.dataTransfer.items) {
|
||||
if (item.webkitGetAsEntry().isDirectory)
|
||||
if ((item.kind !== "file") || (item.webkitGetAsEntry().isDirectory))
|
||||
return;
|
||||
}
|
||||
|
||||
const id = "uploadPage";
|
||||
new MochaUI.Window({
|
||||
id: id,
|
||||
icon: "images/qbittorrent-tray.svg",
|
||||
title: "QBT_TR(Upload local torrent)QBT_TR[CONTEXT=HttpServer]",
|
||||
loadMethod: "iframe",
|
||||
contentURL: "upload.html?v=${CACHEID}",
|
||||
addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
|
||||
scrollbars: true,
|
||||
maximizable: false,
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: 0,
|
||||
width: loadWindowWidth(id, 500),
|
||||
height: loadWindowHeight(id, 460),
|
||||
onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
|
||||
saveWindowSize(id);
|
||||
}),
|
||||
onContentLoaded: () => {
|
||||
const fileInput = document.getElementById(`${id}_iframe`).contentDocument.getElementById("fileselect");
|
||||
fileInput.files = droppedFiles;
|
||||
}
|
||||
});
|
||||
window.qBittorrent.Client.uploadTorrentFiles(droppedFiles);
|
||||
}
|
||||
|
||||
const droppedText = ev.dataTransfer.getData("text");
|
||||
|
@ -1730,33 +1787,8 @@ window.addEventListener("DOMContentLoaded", (event) => {
|
|||
|| ((str.length === 32) && !(/[^2-7A-Z]/i.test(str))); // v1 Base32 encoded SHA-1 info-hash
|
||||
});
|
||||
|
||||
if (urls.length <= 0)
|
||||
return;
|
||||
|
||||
const id = "downloadPage";
|
||||
const contentURL = new URL("download.html", window.location);
|
||||
contentURL.search = new URLSearchParams({
|
||||
v: "${CACHEID}",
|
||||
urls: urls.map(encodeURIComponent).join("|")
|
||||
});
|
||||
new MochaUI.Window({
|
||||
id: id,
|
||||
icon: "images/qbittorrent-tray.svg",
|
||||
title: "QBT_TR(Download from URLs)QBT_TR[CONTEXT=downloadFromURL]",
|
||||
loadMethod: "iframe",
|
||||
contentURL: contentURL.toString(),
|
||||
addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
|
||||
scrollbars: true,
|
||||
maximizable: false,
|
||||
closable: true,
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: 0,
|
||||
width: loadWindowWidth(id, 500),
|
||||
height: loadWindowHeight(id, 600),
|
||||
onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
|
||||
saveWindowSize(id);
|
||||
})
|
||||
});
|
||||
for (const url of urls)
|
||||
qBittorrent.Client.createAddTorrentWindow(url, url);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,145 +0,0 @@
|
|||
/*
|
||||
* MIT License
|
||||
* Copyright (c) 2008 Ishan Arora <ishan@qbittorrent.org>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
window.qBittorrent ??= {};
|
||||
window.qBittorrent.Download ??= (() => {
|
||||
const exports = () => {
|
||||
return {
|
||||
changeCategorySelect: changeCategorySelect,
|
||||
changeTMM: changeTMM
|
||||
};
|
||||
};
|
||||
|
||||
let categories = {};
|
||||
let defaultSavePath = "";
|
||||
|
||||
const getCategories = () => {
|
||||
fetch("api/v2/torrents/categories", {
|
||||
method: "GET",
|
||||
cache: "no-store"
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (!response.ok)
|
||||
return;
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
categories = data;
|
||||
for (const i in data) {
|
||||
if (!Object.hasOwn(data, i))
|
||||
continue;
|
||||
|
||||
const category = data[i];
|
||||
const option = document.createElement("option");
|
||||
option.value = category.name;
|
||||
option.textContent = category.name;
|
||||
document.getElementById("categorySelect").appendChild(option);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getPreferences = () => {
|
||||
const pref = window.parent.qBittorrent.Cache.preferences.get();
|
||||
|
||||
defaultSavePath = pref.save_path;
|
||||
document.getElementById("savepath").value = defaultSavePath;
|
||||
document.getElementById("startTorrent").checked = !pref.add_stopped_enabled;
|
||||
document.getElementById("addToTopOfQueue").checked = pref.add_to_top_of_queue;
|
||||
|
||||
if (pref.auto_tmm_enabled) {
|
||||
document.getElementById("autoTMM").selectedIndex = 1;
|
||||
document.getElementById("savepath").disabled = true;
|
||||
}
|
||||
else {
|
||||
document.getElementById("autoTMM").selectedIndex = 0;
|
||||
}
|
||||
|
||||
if (pref.torrent_stop_condition === "MetadataReceived")
|
||||
document.getElementById("stopCondition").selectedIndex = 1;
|
||||
else if (pref.torrent_stop_condition === "FilesChecked")
|
||||
document.getElementById("stopCondition").selectedIndex = 2;
|
||||
else
|
||||
document.getElementById("stopCondition").selectedIndex = 0;
|
||||
|
||||
if (pref.torrent_content_layout === "Subfolder")
|
||||
document.getElementById("contentLayout").selectedIndex = 1;
|
||||
else if (pref.torrent_content_layout === "NoSubfolder")
|
||||
document.getElementById("contentLayout").selectedIndex = 2;
|
||||
else
|
||||
document.getElementById("contentLayout").selectedIndex = 0;
|
||||
};
|
||||
|
||||
const changeCategorySelect = (item) => {
|
||||
if (item.value === "\\other") {
|
||||
item.nextElementSibling.hidden = false;
|
||||
item.nextElementSibling.value = "";
|
||||
item.nextElementSibling.select();
|
||||
|
||||
if (document.getElementById("autoTMM").selectedIndex === 1)
|
||||
document.getElementById("savepath").value = defaultSavePath;
|
||||
}
|
||||
else {
|
||||
item.nextElementSibling.hidden = true;
|
||||
const text = item.options[item.selectedIndex].textContent;
|
||||
item.nextElementSibling.value = text;
|
||||
|
||||
if (document.getElementById("autoTMM").selectedIndex === 1) {
|
||||
const categoryName = item.value;
|
||||
const category = categories[categoryName];
|
||||
let savePath = defaultSavePath;
|
||||
if (category !== undefined)
|
||||
savePath = (category["savePath"] !== "") ? category["savePath"] : `${defaultSavePath}/${categoryName}`;
|
||||
document.getElementById("savepath").value = savePath;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const changeTMM = (item) => {
|
||||
if (item.selectedIndex === 1) {
|
||||
document.getElementById("savepath").disabled = true;
|
||||
|
||||
const categorySelect = document.getElementById("categorySelect");
|
||||
const categoryName = categorySelect.options[categorySelect.selectedIndex].value;
|
||||
const category = categories[categoryName];
|
||||
document.getElementById("savepath").value = (category === undefined) ? "" : category["savePath"];
|
||||
}
|
||||
else {
|
||||
document.getElementById("savepath").disabled = false;
|
||||
document.getElementById("savepath").value = defaultSavePath;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("load", async (event) => {
|
||||
// user might load this page directly (via browser magnet handler)
|
||||
// so wait for crucial initialization to complete
|
||||
await window.parent.qBittorrent.Client.initializeCaches();
|
||||
|
||||
getPreferences();
|
||||
getCategories();
|
||||
});
|
||||
|
||||
return exports();
|
||||
})();
|
||||
Object.freeze(window.qBittorrent.Download);
|
|
@ -44,6 +44,7 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
TorrentTrackersTable: TorrentTrackersTable,
|
||||
BulkRenameTorrentFilesTable: BulkRenameTorrentFilesTable,
|
||||
TorrentFilesTable: TorrentFilesTable,
|
||||
AddTorrentFilesTable: AddTorrentFilesTable,
|
||||
LogMessageTable: LogMessageTable,
|
||||
LogPeerTable: LogPeerTable,
|
||||
RssFeedTable: RssFeedTable,
|
||||
|
@ -66,6 +67,9 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
|
||||
let DynamicTableHeaderContextMenuClass = null;
|
||||
|
||||
if (typeof LocalPreferences === "undefined")
|
||||
window.LocalPreferences = new window.qBittorrent.LocalPreferences.LocalPreferences();
|
||||
|
||||
class DynamicTable {
|
||||
setup(dynamicTableDivId, dynamicTableFixedHeaderDivId, contextMenu, useVirtualList = false) {
|
||||
this.dynamicTableDivId = dynamicTableDivId;
|
||||
|
@ -2184,12 +2188,13 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
populateTable(root) {
|
||||
this.fileTree.setRoot(root);
|
||||
root.children.each((node) => {
|
||||
this.#addNodeToTable(node, 0);
|
||||
this.#addNodeToTable(node, 0, root);
|
||||
});
|
||||
}
|
||||
|
||||
#addNodeToTable(node, depth) {
|
||||
#addNodeToTable(node, depth, parent) {
|
||||
node.depth = depth;
|
||||
node.parent = parent;
|
||||
|
||||
if (node.isFolder) {
|
||||
const data = {
|
||||
|
@ -2212,7 +2217,7 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
}
|
||||
|
||||
node.children.each((child) => {
|
||||
this.#addNodeToTable(child, depth + 1);
|
||||
this.#addNodeToTable(child, depth + 1, node);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2719,12 +2724,13 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
populateTable(root) {
|
||||
this.fileTree.setRoot(root);
|
||||
root.children.each((node) => {
|
||||
this.#addNodeToTable(node, 0);
|
||||
this.#addNodeToTable(node, 0, root);
|
||||
});
|
||||
}
|
||||
|
||||
#addNodeToTable(node, depth) {
|
||||
#addNodeToTable(node, depth, parent) {
|
||||
node.depth = depth;
|
||||
node.parent = parent;
|
||||
|
||||
if (node.isFolder) {
|
||||
if (!this.collapseState.has(node.rowId))
|
||||
|
@ -2735,7 +2741,7 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
checked: node.checked,
|
||||
remaining: node.remaining,
|
||||
progress: node.progress,
|
||||
priority: window.qBittorrent.PropFiles.normalizePriority(node.priority),
|
||||
priority: window.qBittorrent.TorrentContent.normalizePriority(node.priority),
|
||||
availability: node.availability,
|
||||
fileId: -1,
|
||||
name: node.name
|
||||
|
@ -2752,7 +2758,7 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
}
|
||||
|
||||
node.children.each((child) => {
|
||||
this.#addNodeToTable(child, depth + 1);
|
||||
this.#addNodeToTable(child, depth + 1, node);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2813,9 +2819,9 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
|
||||
const downloadCheckbox = td.children[1];
|
||||
if (downloadCheckbox === undefined)
|
||||
td.append(window.qBittorrent.PropFiles.createDownloadCheckbox(id, row.full_data.fileId, value));
|
||||
td.append(window.qBittorrent.TorrentContent.createDownloadCheckbox(id, row.full_data.fileId, value));
|
||||
else
|
||||
window.qBittorrent.PropFiles.updateDownloadCheckbox(downloadCheckbox, id, row.full_data.fileId, value);
|
||||
window.qBittorrent.TorrentContent.updateDownloadCheckbox(downloadCheckbox, id, row.full_data.fileId, value);
|
||||
|
||||
};
|
||||
this.columns["checked"].staticWidth = 50;
|
||||
|
@ -2887,16 +2893,18 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
this.columns["size"].updateTd = displaySize;
|
||||
|
||||
// progress
|
||||
this.columns["progress"].updateTd = function(td, row) {
|
||||
const value = Number(this.getRowValue(row));
|
||||
if (this.columns["progress"] !== undefined) {
|
||||
this.columns["progress"].updateTd = function(td, row) {
|
||||
const value = Number(this.getRowValue(row));
|
||||
|
||||
const progressBar = td.firstElementChild;
|
||||
if (progressBar === null)
|
||||
td.append(new window.qBittorrent.ProgressBar.ProgressBar(value));
|
||||
else
|
||||
progressBar.setValue(value);
|
||||
};
|
||||
this.columns["progress"].staticWidth = 100;
|
||||
const progressBar = td.firstElementChild;
|
||||
if (progressBar === null)
|
||||
td.append(new window.qBittorrent.ProgressBar.ProgressBar(value));
|
||||
else
|
||||
progressBar.setValue(value);
|
||||
};
|
||||
this.columns["progress"].staticWidth = 100;
|
||||
}
|
||||
|
||||
// priority
|
||||
this.columns["priority"].updateTd = function(td, row) {
|
||||
|
@ -2905,15 +2913,17 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
|
||||
const priorityCombo = td.firstElementChild;
|
||||
if (priorityCombo === null)
|
||||
td.append(window.qBittorrent.PropFiles.createPriorityCombo(id, row.full_data.fileId, value));
|
||||
td.append(window.qBittorrent.TorrentContent.createPriorityCombo(id, row.full_data.fileId, value));
|
||||
else
|
||||
window.qBittorrent.PropFiles.updatePriorityCombo(priorityCombo, id, row.full_data.fileId, value);
|
||||
window.qBittorrent.TorrentContent.updatePriorityCombo(priorityCombo, id, row.full_data.fileId, value);
|
||||
};
|
||||
this.columns["priority"].staticWidth = 140;
|
||||
|
||||
// remaining, availability
|
||||
this.columns["remaining"].updateTd = displaySize;
|
||||
this.columns["availability"].updateTd = displayPercentage;
|
||||
if (this.columns["remaining"] !== undefined)
|
||||
this.columns["remaining"].updateTd = displaySize;
|
||||
if (this.columns["availability"] !== undefined)
|
||||
this.columns["availability"].updateTd = displayPercentage;
|
||||
}
|
||||
|
||||
#sortNodesByColumn(root, column) {
|
||||
|
@ -3068,6 +3078,17 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
}
|
||||
}
|
||||
|
||||
class AddTorrentFilesTable extends TorrentFilesTable {
|
||||
initColumns() {
|
||||
this.newColumn("checked", "", "", 50, true);
|
||||
this.newColumn("name", "", "QBT_TR(Name)QBT_TR[CONTEXT=TrackerListWidget]", 190, true);
|
||||
this.newColumn("size", "", "QBT_TR(Total Size)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
|
||||
this.newColumn("priority", "", "QBT_TR(Download Priority)QBT_TR[CONTEXT=TrackerListWidget]", 140, true);
|
||||
|
||||
this.initColumnsFunctions();
|
||||
}
|
||||
}
|
||||
|
||||
class RssFeedTable extends DynamicTable {
|
||||
initColumns() {
|
||||
this.newColumn("state_icon", "", "", 30, true);
|
||||
|
@ -3246,7 +3267,8 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
if (!tr)
|
||||
return;
|
||||
|
||||
showDownloadPage([this.getRow(tr.rowId).full_data.torrentURL]);
|
||||
const { name, torrentURL } = this._this.rows.get(this.rowId).full_data;
|
||||
qBittorrent.Client.createAddTorrentWindow(name, torrentURL);
|
||||
});
|
||||
}
|
||||
updateRow(tr, fullUpdate) {
|
||||
|
|
|
@ -159,10 +159,11 @@ let setQueuePositionFN = () => {};
|
|||
let exportTorrentFN = () => {};
|
||||
|
||||
const initializeWindows = () => {
|
||||
saveWindowSize = (windowId) => {
|
||||
const size = document.getElementById(windowId).getSize();
|
||||
LocalPreferences.set(`window_${windowId}_width`, size.x);
|
||||
LocalPreferences.set(`window_${windowId}_height`, size.y);
|
||||
saveWindowSize = (windowName, windowId = windowName) => {
|
||||
const windowInstance = MochaUI.Windows.instances[windowId];
|
||||
const size = windowInstance.contentWrapperEl.getSize();
|
||||
LocalPreferences.set(`window_${windowName}_width`, size.x);
|
||||
LocalPreferences.set(`window_${windowName}_height`, size.y);
|
||||
};
|
||||
|
||||
loadWindowWidth = (windowId, defaultValue, limitToViewportWidth = true) => {
|
||||
|
@ -207,14 +208,13 @@ const initializeWindows = () => {
|
|||
title: "QBT_TR(Download from URLs)QBT_TR[CONTEXT=downloadFromURL]",
|
||||
loadMethod: "iframe",
|
||||
contentURL: contentURL.toString(),
|
||||
addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
|
||||
scrollbars: true,
|
||||
maximizable: false,
|
||||
closable: true,
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: 0,
|
||||
width: loadWindowWidth(id, 500),
|
||||
height: loadWindowHeight(id, 600),
|
||||
height: loadWindowHeight(id, 300),
|
||||
onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
|
||||
saveWindowSize(id);
|
||||
})
|
||||
|
@ -297,31 +297,33 @@ const initializeWindows = () => {
|
|||
});
|
||||
});
|
||||
|
||||
addClickEvent("upload", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const id = "uploadPage";
|
||||
new MochaUI.Window({
|
||||
id: id,
|
||||
icon: "images/qbittorrent-tray.svg",
|
||||
title: "QBT_TR(Upload local torrent)QBT_TR[CONTEXT=HttpServer]",
|
||||
loadMethod: "iframe",
|
||||
contentURL: "upload.html?v=${CACHEID}",
|
||||
addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
|
||||
scrollbars: true,
|
||||
maximizable: false,
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: 0,
|
||||
width: loadWindowWidth(id, 500),
|
||||
height: loadWindowHeight(id, 460),
|
||||
onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
|
||||
saveWindowSize(id);
|
||||
})
|
||||
});
|
||||
updateMainData();
|
||||
document.querySelector("#uploadButton #fileselectButton").addEventListener("click", function(event) {
|
||||
// clear the value so that reselecting the same file(s) still triggers the 'change' event
|
||||
this.value = null;
|
||||
});
|
||||
|
||||
// make the entire anchor tag trigger the input, despite the input's label not spanning the entire anchor
|
||||
document.getElementById("uploadLink").addEventListener("click", (e) => {
|
||||
const fileSelector = document.getElementById("fileselectLink");
|
||||
// clear the value so that reselecting the same file(s) still triggers the 'change' event
|
||||
if (e.target === fileSelector) {
|
||||
e.target.value = null;
|
||||
}
|
||||
else {
|
||||
e.preventDefault();
|
||||
fileSelector.click();
|
||||
}
|
||||
});
|
||||
|
||||
for (const element of document.querySelectorAll("#uploadButton #fileselectButton, #uploadLink #fileselectLink")) {
|
||||
element.addEventListener("change", (event) => {
|
||||
if (element.files.length === 0)
|
||||
return;
|
||||
|
||||
window.qBittorrent.Client.uploadTorrentFiles(element.files);
|
||||
});
|
||||
}
|
||||
|
||||
globalUploadLimitFN = () => {
|
||||
const contentURL = new URL("speedlimit.html", window.location);
|
||||
contentURL.search = new URLSearchParams({
|
||||
|
@ -1383,4 +1385,10 @@ const initializeWindows = () => {
|
|||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
const userAgent = (navigator.userAgentData?.platform ?? navigator.platform).toLowerCase();
|
||||
if (userAgent.includes("ipad") || userAgent.includes("iphone") || (userAgent.includes("mac") && (navigator.maxTouchPoints > 1))) {
|
||||
for (const element of document.getElementsByClassName("fileselect"))
|
||||
element.accept = ".torrent";
|
||||
}
|
||||
};
|
||||
|
|
|
@ -32,248 +32,16 @@ window.qBittorrent ??= {};
|
|||
window.qBittorrent.PropFiles ??= (() => {
|
||||
const exports = () => {
|
||||
return {
|
||||
normalizePriority: normalizePriority,
|
||||
createDownloadCheckbox: createDownloadCheckbox,
|
||||
updateDownloadCheckbox: updateDownloadCheckbox,
|
||||
createPriorityCombo: createPriorityCombo,
|
||||
updatePriorityCombo: updatePriorityCombo,
|
||||
updateData: updateData,
|
||||
clear: clear
|
||||
};
|
||||
};
|
||||
|
||||
const torrentFilesTable = new window.qBittorrent.DynamicTable.TorrentFilesTable();
|
||||
const FilePriority = window.qBittorrent.FileTree.FilePriority;
|
||||
const TriState = window.qBittorrent.FileTree.TriState;
|
||||
let is_seed = true;
|
||||
let current_hash = "";
|
||||
|
||||
const normalizePriority = (priority) => {
|
||||
switch (priority) {
|
||||
case FilePriority.Ignored:
|
||||
case FilePriority.Normal:
|
||||
case FilePriority.High:
|
||||
case FilePriority.Maximum:
|
||||
case FilePriority.Mixed:
|
||||
return priority;
|
||||
default:
|
||||
return FilePriority.Normal;
|
||||
}
|
||||
};
|
||||
|
||||
const getAllChildren = (id, fileId) => {
|
||||
const node = torrentFilesTable.getNode(id);
|
||||
if (!node.isFolder) {
|
||||
return {
|
||||
rowIds: [id],
|
||||
fileIds: [fileId]
|
||||
};
|
||||
}
|
||||
|
||||
const rowIds = [];
|
||||
const fileIds = [];
|
||||
|
||||
const getChildFiles = (node) => {
|
||||
if (node.isFolder) {
|
||||
node.children.each((child) => {
|
||||
getChildFiles(child);
|
||||
});
|
||||
}
|
||||
else {
|
||||
rowIds.push(node.data.rowId);
|
||||
fileIds.push(node.data.fileId);
|
||||
}
|
||||
};
|
||||
|
||||
node.children.each((child) => {
|
||||
getChildFiles(child);
|
||||
});
|
||||
|
||||
return {
|
||||
rowIds: rowIds,
|
||||
fileIds: fileIds
|
||||
};
|
||||
};
|
||||
|
||||
const fileCheckboxClicked = (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const checkbox = e.target;
|
||||
const priority = checkbox.checked ? FilePriority.Normal : FilePriority.Ignored;
|
||||
const id = checkbox.getAttribute("data-id");
|
||||
const fileId = checkbox.getAttribute("data-file-id");
|
||||
|
||||
const rows = getAllChildren(id, fileId);
|
||||
|
||||
setFilePriority(rows.rowIds, rows.fileIds, priority);
|
||||
updateGlobalCheckbox();
|
||||
};
|
||||
|
||||
const fileComboboxChanged = (e) => {
|
||||
const combobox = e.target;
|
||||
const priority = combobox.value;
|
||||
const id = combobox.getAttribute("data-id");
|
||||
const fileId = combobox.getAttribute("data-file-id");
|
||||
|
||||
const rows = getAllChildren(id, fileId);
|
||||
|
||||
setFilePriority(rows.rowIds, rows.fileIds, priority);
|
||||
updateGlobalCheckbox();
|
||||
};
|
||||
|
||||
const createDownloadCheckbox = (id, fileId, checked) => {
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.setAttribute("data-id", id);
|
||||
checkbox.setAttribute("data-file-id", fileId);
|
||||
checkbox.addEventListener("click", fileCheckboxClicked);
|
||||
|
||||
updateCheckbox(checkbox, checked);
|
||||
return checkbox;
|
||||
};
|
||||
|
||||
const updateDownloadCheckbox = (checkbox, id, fileId, checked) => {
|
||||
checkbox.setAttribute("data-id", id);
|
||||
checkbox.setAttribute("data-file-id", fileId);
|
||||
updateCheckbox(checkbox, checked);
|
||||
};
|
||||
|
||||
const updateCheckbox = (checkbox, checked) => {
|
||||
switch (checked) {
|
||||
case TriState.Checked:
|
||||
setCheckboxChecked(checkbox);
|
||||
break;
|
||||
case TriState.Unchecked:
|
||||
setCheckboxUnchecked(checkbox);
|
||||
break;
|
||||
case TriState.Partial:
|
||||
setCheckboxPartial(checkbox);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const createPriorityCombo = (id, fileId, selectedPriority) => {
|
||||
const createOption = (priority, isSelected, text) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = priority.toString();
|
||||
option.selected = isSelected;
|
||||
option.textContent = text;
|
||||
return option;
|
||||
};
|
||||
|
||||
const select = document.createElement("select");
|
||||
select.id = `comboPrio${id}`;
|
||||
select.setAttribute("data-id", id);
|
||||
select.setAttribute("data-file-id", fileId);
|
||||
select.classList.add("combo_priority");
|
||||
select.addEventListener("change", fileComboboxChanged);
|
||||
|
||||
select.appendChild(createOption(FilePriority.Ignored, (FilePriority.Ignored === selectedPriority), "QBT_TR(Do not download)QBT_TR[CONTEXT=PropListDelegate]"));
|
||||
select.appendChild(createOption(FilePriority.Normal, (FilePriority.Normal === selectedPriority), "QBT_TR(Normal)QBT_TR[CONTEXT=PropListDelegate]"));
|
||||
select.appendChild(createOption(FilePriority.High, (FilePriority.High === selectedPriority), "QBT_TR(High)QBT_TR[CONTEXT=PropListDelegate]"));
|
||||
select.appendChild(createOption(FilePriority.Maximum, (FilePriority.Maximum === selectedPriority), "QBT_TR(Maximum)QBT_TR[CONTEXT=PropListDelegate]"));
|
||||
|
||||
// "Mixed" priority is for display only; it shouldn't be selectable
|
||||
const mixedPriorityOption = createOption(FilePriority.Mixed, (FilePriority.Mixed === selectedPriority), "QBT_TR(Mixed)QBT_TR[CONTEXT=PropListDelegate]");
|
||||
mixedPriorityOption.disabled = true;
|
||||
select.appendChild(mixedPriorityOption);
|
||||
|
||||
return select;
|
||||
};
|
||||
|
||||
const updatePriorityCombo = (combobox, id, fileId, selectedPriority) => {
|
||||
combobox.id = `comboPrio${id}`;
|
||||
combobox.setAttribute("data-id", id);
|
||||
combobox.setAttribute("data-file-id", fileId);
|
||||
if (Number(combobox.value) !== selectedPriority)
|
||||
selectComboboxPriority(combobox, selectedPriority);
|
||||
};
|
||||
|
||||
const selectComboboxPriority = (combobox, priority) => {
|
||||
const options = combobox.options;
|
||||
for (let i = 0; i < options.length; ++i) {
|
||||
const option = options[i];
|
||||
if (Number(option.value) === priority)
|
||||
option.selected = true;
|
||||
else
|
||||
option.selected = false;
|
||||
}
|
||||
|
||||
combobox.value = priority;
|
||||
};
|
||||
|
||||
const switchCheckboxState = (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const rowIds = [];
|
||||
const fileIds = [];
|
||||
let priority = FilePriority.Ignored;
|
||||
const checkbox = document.getElementById("tristate_cb");
|
||||
|
||||
if (checkbox.state === "checked") {
|
||||
setCheckboxUnchecked(checkbox);
|
||||
// set file priority for all checked to Ignored
|
||||
torrentFilesTable.getFilteredAndSortedRows().forEach((row) => {
|
||||
const rowId = row.rowId;
|
||||
const fileId = row.full_data.fileId;
|
||||
const isChecked = (row.full_data.checked === TriState.Checked);
|
||||
const isFolder = (fileId === -1);
|
||||
if (!isFolder && isChecked) {
|
||||
rowIds.push(rowId);
|
||||
fileIds.push(fileId);
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
setCheckboxChecked(checkbox);
|
||||
priority = FilePriority.Normal;
|
||||
// set file priority for all unchecked to Normal
|
||||
torrentFilesTable.getFilteredAndSortedRows().forEach((row) => {
|
||||
const rowId = row.rowId;
|
||||
const fileId = row.full_data.fileId;
|
||||
const isUnchecked = (row.full_data.checked === TriState.Unchecked);
|
||||
const isFolder = (fileId === -1);
|
||||
if (!isFolder && isUnchecked) {
|
||||
rowIds.push(rowId);
|
||||
fileIds.push(fileId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (rowIds.length > 0)
|
||||
setFilePriority(rowIds, fileIds, priority);
|
||||
};
|
||||
|
||||
const updateGlobalCheckbox = () => {
|
||||
const checkbox = document.getElementById("tristate_cb");
|
||||
if (torrentFilesTable.isAllCheckboxesChecked())
|
||||
setCheckboxChecked(checkbox);
|
||||
else if (torrentFilesTable.isAllCheckboxesUnchecked())
|
||||
setCheckboxUnchecked(checkbox);
|
||||
else
|
||||
setCheckboxPartial(checkbox);
|
||||
};
|
||||
|
||||
const setCheckboxChecked = (checkbox) => {
|
||||
checkbox.state = "checked";
|
||||
checkbox.indeterminate = false;
|
||||
checkbox.checked = true;
|
||||
};
|
||||
|
||||
const setCheckboxUnchecked = (checkbox) => {
|
||||
checkbox.state = "unchecked";
|
||||
checkbox.indeterminate = false;
|
||||
checkbox.checked = false;
|
||||
};
|
||||
|
||||
const setCheckboxPartial = (checkbox) => {
|
||||
checkbox.state = "partial";
|
||||
checkbox.indeterminate = true;
|
||||
};
|
||||
|
||||
const setFilePriority = (ids, fileIds, priority) => {
|
||||
if (current_hash === "")
|
||||
return;
|
||||
const onFilePriorityChanged = (fileIds, priority) => {
|
||||
// ignore folders
|
||||
fileIds = fileIds.map(Number).filter(id => !window.qBittorrent.TorrentContent.isFolder(id));
|
||||
|
||||
clearTimeout(loadTorrentFilesDataTimer);
|
||||
loadTorrentFilesDataTimer = -1;
|
||||
|
@ -292,15 +60,6 @@ window.qBittorrent.PropFiles ??= (() => {
|
|||
|
||||
loadTorrentFilesDataTimer = loadTorrentFilesData.delay(1000);
|
||||
});
|
||||
|
||||
const ignore = (priority === FilePriority.Ignored);
|
||||
ids.forEach((id) => {
|
||||
torrentFilesTable.setIgnored(id, ignore);
|
||||
|
||||
const combobox = document.getElementById(`comboPrio${id}`);
|
||||
if (combobox !== null)
|
||||
selectComboboxPriority(combobox, priority);
|
||||
});
|
||||
};
|
||||
|
||||
let loadTorrentFilesDataTimer = -1;
|
||||
|
@ -339,14 +98,13 @@ window.qBittorrent.PropFiles ??= (() => {
|
|||
|
||||
const files = await response.json();
|
||||
|
||||
clearTimeout(torrentFilesFilterInputTimer);
|
||||
torrentFilesFilterInputTimer = -1;
|
||||
window.qBittorrent.TorrentContent.clearFilterInputTimer();
|
||||
|
||||
if (files.length === 0) {
|
||||
torrentFilesTable.clear();
|
||||
}
|
||||
else {
|
||||
handleNewTorrentFiles(files);
|
||||
window.qBittorrent.TorrentContent.updateData(files);
|
||||
if (loadedNewTorrent)
|
||||
torrentFilesTable.collapseAllNodes();
|
||||
}
|
||||
|
@ -363,133 +121,7 @@ window.qBittorrent.PropFiles ??= (() => {
|
|||
loadTorrentFilesData();
|
||||
};
|
||||
|
||||
const handleNewTorrentFiles = (files) => {
|
||||
is_seed = (files.length > 0) ? files[0].is_seed : true;
|
||||
|
||||
const rows = files.map((file, index) => {
|
||||
const ignore = (file.priority === FilePriority.Ignored);
|
||||
const row = {
|
||||
fileId: index,
|
||||
checked: (ignore ? TriState.Unchecked : TriState.Checked),
|
||||
fileName: file.name,
|
||||
name: window.qBittorrent.Filesystem.fileName(file.name),
|
||||
size: file.size,
|
||||
progress: window.qBittorrent.Misc.toFixedPointString((file.progress * 100), 1),
|
||||
priority: normalizePriority(file.priority),
|
||||
remaining: (ignore ? 0 : (file.size * (1 - file.progress))),
|
||||
availability: file.availability
|
||||
};
|
||||
return row;
|
||||
});
|
||||
|
||||
addRowsToTable(rows);
|
||||
updateGlobalCheckbox();
|
||||
};
|
||||
|
||||
const addRowsToTable = (rows) => {
|
||||
const selectedFiles = torrentFilesTable.selectedRowsIds();
|
||||
let rowId = 0;
|
||||
|
||||
const rootNode = new window.qBittorrent.FileTree.FolderNode();
|
||||
|
||||
rows.forEach((row) => {
|
||||
const pathItems = row.fileName.split(window.qBittorrent.Filesystem.PathSeparator);
|
||||
|
||||
pathItems.pop(); // remove last item (i.e. file name)
|
||||
let parent = rootNode;
|
||||
pathItems.forEach((folderName) => {
|
||||
if (folderName === ".unwanted")
|
||||
return;
|
||||
|
||||
let folderNode = null;
|
||||
if (parent.children !== null) {
|
||||
for (let i = 0; i < parent.children.length; ++i) {
|
||||
const childFolder = parent.children[i];
|
||||
if (childFolder.name === folderName) {
|
||||
folderNode = childFolder;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (folderNode === null) {
|
||||
folderNode = new window.qBittorrent.FileTree.FolderNode();
|
||||
folderNode.path = (parent.path === "")
|
||||
? folderName
|
||||
: [parent.path, folderName].join(window.qBittorrent.Filesystem.PathSeparator);
|
||||
folderNode.name = folderName;
|
||||
folderNode.rowId = rowId;
|
||||
folderNode.root = parent;
|
||||
parent.addChild(folderNode);
|
||||
|
||||
++rowId;
|
||||
}
|
||||
|
||||
parent = folderNode;
|
||||
});
|
||||
|
||||
const isChecked = row.checked ? TriState.Checked : TriState.Unchecked;
|
||||
const remaining = (row.priority === FilePriority.Ignored) ? 0 : row.remaining;
|
||||
const childNode = new window.qBittorrent.FileTree.FileNode();
|
||||
childNode.name = row.name;
|
||||
childNode.path = row.fileName;
|
||||
childNode.rowId = rowId;
|
||||
childNode.size = row.size;
|
||||
childNode.checked = isChecked;
|
||||
childNode.remaining = remaining;
|
||||
childNode.progress = row.progress;
|
||||
childNode.priority = row.priority;
|
||||
childNode.availability = row.availability;
|
||||
childNode.root = parent;
|
||||
childNode.data = row;
|
||||
parent.addChild(childNode);
|
||||
|
||||
++rowId;
|
||||
});
|
||||
|
||||
torrentFilesTable.populateTable(rootNode);
|
||||
torrentFilesTable.updateTable(false);
|
||||
|
||||
if (selectedFiles.length > 0)
|
||||
torrentFilesTable.reselectRows(selectedFiles);
|
||||
};
|
||||
|
||||
const filesPriorityMenuClicked = (priority) => {
|
||||
const selectedRows = torrentFilesTable.selectedRowsIds();
|
||||
if (selectedRows.length === 0)
|
||||
return;
|
||||
|
||||
const rowIds = [];
|
||||
const fileIds = [];
|
||||
selectedRows.forEach((rowId) => {
|
||||
rowIds.push(rowId);
|
||||
fileIds.push(torrentFilesTable.getRowFileId(rowId));
|
||||
});
|
||||
|
||||
const uniqueRowIds = {};
|
||||
const uniqueFileIds = {};
|
||||
for (let i = 0; i < rowIds.length; ++i) {
|
||||
const rows = getAllChildren(rowIds[i], fileIds[i]);
|
||||
rows.rowIds.forEach((rowId) => {
|
||||
uniqueRowIds[rowId] = true;
|
||||
});
|
||||
rows.fileIds.forEach((fileId) => {
|
||||
uniqueFileIds[fileId] = true;
|
||||
});
|
||||
}
|
||||
|
||||
setFilePriority(Object.keys(uniqueRowIds), Object.keys(uniqueFileIds), priority);
|
||||
};
|
||||
|
||||
const singleFileRename = (hash) => {
|
||||
const rowId = torrentFilesTable.selectedRowsIds()[0];
|
||||
if (rowId === undefined)
|
||||
return;
|
||||
const row = torrentFilesTable.rows.get(rowId);
|
||||
if (!row)
|
||||
return;
|
||||
|
||||
const node = torrentFilesTable.getNode(rowId);
|
||||
const singleFileRename = (hash, node) => {
|
||||
const path = node.path;
|
||||
|
||||
new MochaUI.Window({
|
||||
|
@ -504,16 +136,19 @@ window.qBittorrent.PropFiles ??= (() => {
|
|||
paddingVertical: 0,
|
||||
paddingHorizontal: 0,
|
||||
width: window.qBittorrent.Dialog.limitWidthToViewport(400),
|
||||
height: 100
|
||||
height: 100,
|
||||
onCloseComplete: () => {
|
||||
updateData();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const multiFileRename = (hash) => {
|
||||
const multiFileRename = (hash, selectedRows) => {
|
||||
new MochaUI.Window({
|
||||
id: "multiRenamePage",
|
||||
icon: "images/qbittorrent-tray.svg",
|
||||
title: "QBT_TR(Renaming)QBT_TR[CONTEXT=TorrentContentTreeView]",
|
||||
data: { hash: hash, selectedRows: torrentFilesTable.selectedRows },
|
||||
data: { hash: hash, selectedRows: selectedRows },
|
||||
loadMethod: "xhr",
|
||||
contentURL: "rename_files.html?v=${CACHEID}",
|
||||
scrollbars: false,
|
||||
|
@ -523,89 +158,21 @@ window.qBittorrent.PropFiles ??= (() => {
|
|||
paddingHorizontal: 0,
|
||||
width: 800,
|
||||
height: 420,
|
||||
resizeLimit: { x: [800], y: [420] }
|
||||
resizeLimit: { x: [800], y: [420] },
|
||||
onCloseComplete: () => {
|
||||
updateData();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const torrentFilesContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
|
||||
targets: "#torrentFilesTableDiv tbody tr",
|
||||
menu: "torrentFilesMenu",
|
||||
actions: {
|
||||
Rename: (element, ref) => {
|
||||
const hash = torrentsTable.getCurrentTorrentID();
|
||||
if (!hash)
|
||||
return;
|
||||
const onFileRenameHandler = (selectedRows, selectedNodes) => {
|
||||
if (selectedNodes.length === 1)
|
||||
singleFileRename(current_hash, selectedNodes[0]);
|
||||
else if (selectedNodes.length > 1)
|
||||
multiFileRename(current_hash, selectedRows);
|
||||
};
|
||||
|
||||
if (torrentFilesTable.selectedRowsIds().length > 1)
|
||||
multiFileRename(hash);
|
||||
else
|
||||
singleFileRename(hash);
|
||||
},
|
||||
|
||||
FilePrioIgnore: (element, ref) => {
|
||||
filesPriorityMenuClicked(FilePriority.Ignored);
|
||||
},
|
||||
FilePrioNormal: (element, ref) => {
|
||||
filesPriorityMenuClicked(FilePriority.Normal);
|
||||
},
|
||||
FilePrioHigh: (element, ref) => {
|
||||
filesPriorityMenuClicked(FilePriority.High);
|
||||
},
|
||||
FilePrioMaximum: (element, ref) => {
|
||||
filesPriorityMenuClicked(FilePriority.Maximum);
|
||||
}
|
||||
},
|
||||
offsets: {
|
||||
x: 0,
|
||||
y: 2
|
||||
},
|
||||
onShow: function() {
|
||||
if (is_seed)
|
||||
this.hideItem("FilePrio");
|
||||
else
|
||||
this.showItem("FilePrio");
|
||||
}
|
||||
});
|
||||
|
||||
torrentFilesTable.setup("torrentFilesTableDiv", "torrentFilesTableFixedHeaderDiv", torrentFilesContextMenu, true);
|
||||
// inject checkbox into table header
|
||||
const tableHeaders = document.querySelectorAll("#torrentFilesTableFixedHeaderDiv .dynamicTableHeader th");
|
||||
if (tableHeaders.length > 0) {
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.id = "tristate_cb";
|
||||
checkbox.addEventListener("click", switchCheckboxState);
|
||||
|
||||
const checkboxTH = tableHeaders[0];
|
||||
checkboxTH.append(checkbox);
|
||||
}
|
||||
|
||||
// default sort by name column
|
||||
if (torrentFilesTable.getSortedColumn() === null)
|
||||
torrentFilesTable.setSortedColumn("name");
|
||||
|
||||
// listen for changes to torrentFilesFilterInput
|
||||
let torrentFilesFilterInputTimer = -1;
|
||||
document.getElementById("torrentFilesFilterInput").addEventListener("input", (event) => {
|
||||
clearTimeout(torrentFilesFilterInputTimer);
|
||||
|
||||
const value = document.getElementById("torrentFilesFilterInput").value;
|
||||
torrentFilesTable.setFilter(value);
|
||||
|
||||
torrentFilesFilterInputTimer = setTimeout(() => {
|
||||
torrentFilesFilterInputTimer = -1;
|
||||
|
||||
if (current_hash === "")
|
||||
return;
|
||||
|
||||
torrentFilesTable.updateTable();
|
||||
|
||||
if (value.trim() === "")
|
||||
torrentFilesTable.collapseAllNodes();
|
||||
else
|
||||
torrentFilesTable.expandAllNodes();
|
||||
}, window.qBittorrent.Misc.FILTER_INPUT_DELAY);
|
||||
});
|
||||
const torrentFilesTable = window.qBittorrent.TorrentContent.init("torrentFilesTableDiv", window.qBittorrent.DynamicTable.TorrentFilesTable, onFilePriorityChanged, onFileRenameHandler);
|
||||
|
||||
const clear = () => {
|
||||
torrentFilesTable.clear();
|
||||
|
|
|
@ -559,15 +559,10 @@ window.qBittorrent.Search ??= (() => {
|
|||
};
|
||||
|
||||
const downloadSearchTorrent = () => {
|
||||
const urls = [];
|
||||
for (const rowID of searchResultsTable.selectedRowsIds())
|
||||
urls.push(searchResultsTable.getRow(rowID).full_data.fileUrl);
|
||||
|
||||
// only proceed if at least 1 row was selected
|
||||
if (!urls.length)
|
||||
return;
|
||||
|
||||
showDownloadPage(urls);
|
||||
for (const rowID of searchResultsTable.selectedRowsIds()) {
|
||||
const { fileName, fileUrl } = searchResultsTable.getRow(rowID).full_data;
|
||||
qBittorrent.Client.createAddTorrentWindow(fileName, fileUrl);
|
||||
}
|
||||
};
|
||||
|
||||
const manageSearchPlugins = () => {
|
||||
|
|
571
src/webui/www/private/scripts/torrent-content.js
Normal file
571
src/webui/www/private/scripts/torrent-content.js
Normal file
|
@ -0,0 +1,571 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2025 Thomas Piccirello <thomas@piccirello.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
window.qBittorrent ??= {};
|
||||
window.qBittorrent.TorrentContent ??= (() => {
|
||||
const exports = () => {
|
||||
return {
|
||||
init: init,
|
||||
normalizePriority: normalizePriority,
|
||||
isFolder: isFolder,
|
||||
createDownloadCheckbox: createDownloadCheckbox,
|
||||
updateDownloadCheckbox: updateDownloadCheckbox,
|
||||
createPriorityCombo: createPriorityCombo,
|
||||
updatePriorityCombo: updatePriorityCombo,
|
||||
updateData: updateData,
|
||||
clearFilterInputTimer: clearFilterInputTimer
|
||||
};
|
||||
};
|
||||
|
||||
let torrentFilesTable;
|
||||
const FilePriority = window.qBittorrent.FileTree.FilePriority;
|
||||
const TriState = window.qBittorrent.FileTree.TriState;
|
||||
let torrentFilesFilterInputTimer = -1;
|
||||
let onFilePriorityChanged = null;
|
||||
|
||||
const normalizePriority = (priority) => {
|
||||
priority = Number(priority);
|
||||
|
||||
switch (priority) {
|
||||
case FilePriority.Ignored:
|
||||
case FilePriority.Normal:
|
||||
case FilePriority.High:
|
||||
case FilePriority.Maximum:
|
||||
case FilePriority.Mixed:
|
||||
return priority;
|
||||
default:
|
||||
return FilePriority.Normal;
|
||||
}
|
||||
};
|
||||
|
||||
const triStateFromPriority = (priority) => {
|
||||
switch (normalizePriority(priority)) {
|
||||
case FilePriority.Ignored:
|
||||
return TriState.Unchecked;
|
||||
case FilePriority.Normal:
|
||||
case FilePriority.High:
|
||||
case FilePriority.Maximum:
|
||||
return TriState.Checked;
|
||||
case FilePriority.Mixed:
|
||||
return TriState.Partial;
|
||||
}
|
||||
};
|
||||
|
||||
const isFolder = (fileId) => {
|
||||
return fileId === -1;
|
||||
};
|
||||
|
||||
const getAllChildren = (id, fileId) => {
|
||||
const node = torrentFilesTable.getNode(id);
|
||||
const rowIds = [node.data.rowId];
|
||||
const fileIds = [node.data.fileId];
|
||||
|
||||
const getChildFiles = (node) => {
|
||||
rowIds.push(node.data.rowId);
|
||||
fileIds.push(node.data.fileId);
|
||||
|
||||
if (node.isFolder) {
|
||||
node.children.forEach((child) => {
|
||||
getChildFiles(child);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
node.children.forEach((child) => {
|
||||
getChildFiles(child);
|
||||
});
|
||||
|
||||
return {
|
||||
rowIds: rowIds,
|
||||
fileIds: fileIds
|
||||
};
|
||||
};
|
||||
|
||||
const fileCheckboxClicked = (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const checkbox = e.target;
|
||||
const priority = checkbox.checked ? FilePriority.Normal : FilePriority.Ignored;
|
||||
const id = checkbox.getAttribute("data-id");
|
||||
const fileId = Number(checkbox.getAttribute("data-file-id"));
|
||||
|
||||
const rows = getAllChildren(id, fileId);
|
||||
|
||||
setFilePriority(rows.rowIds, rows.fileIds, priority);
|
||||
updateParentFolder(id);
|
||||
};
|
||||
|
||||
const fileComboboxChanged = (e) => {
|
||||
const combobox = e.target;
|
||||
const priority = combobox.value;
|
||||
const id = combobox.getAttribute("data-id");
|
||||
const fileId = Number(combobox.getAttribute("data-file-id"));
|
||||
|
||||
const rows = getAllChildren(id, fileId);
|
||||
|
||||
setFilePriority(rows.rowIds, rows.fileIds, priority);
|
||||
updateParentFolder(id);
|
||||
};
|
||||
|
||||
const createDownloadCheckbox = (id, fileId, checked) => {
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.setAttribute("data-id", id);
|
||||
checkbox.setAttribute("data-file-id", fileId);
|
||||
checkbox.addEventListener("click", fileCheckboxClicked);
|
||||
|
||||
updateCheckbox(checkbox, checked);
|
||||
return checkbox;
|
||||
};
|
||||
|
||||
const updateDownloadCheckbox = (checkbox, id, fileId, checked) => {
|
||||
checkbox.setAttribute("data-id", id);
|
||||
checkbox.setAttribute("data-file-id", fileId);
|
||||
updateCheckbox(checkbox, checked);
|
||||
};
|
||||
|
||||
const updateCheckbox = (checkbox, checked) => {
|
||||
switch (checked) {
|
||||
case TriState.Checked:
|
||||
setCheckboxChecked(checkbox);
|
||||
break;
|
||||
case TriState.Unchecked:
|
||||
setCheckboxUnchecked(checkbox);
|
||||
break;
|
||||
case TriState.Partial:
|
||||
setCheckboxPartial(checkbox);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const createPriorityCombo = (id, fileId, selectedPriority) => {
|
||||
const createOption = (priority, isSelected, text) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = priority.toString();
|
||||
option.selected = isSelected;
|
||||
option.textContent = text;
|
||||
return option;
|
||||
};
|
||||
|
||||
const select = document.createElement("select");
|
||||
select.setAttribute("data-id", id);
|
||||
select.setAttribute("data-file-id", fileId);
|
||||
select.classList.add("combo_priority");
|
||||
select.addEventListener("change", fileComboboxChanged);
|
||||
|
||||
select.appendChild(createOption(FilePriority.Ignored, (FilePriority.Ignored === selectedPriority), "QBT_TR(Do not download)QBT_TR[CONTEXT=PropListDelegate]"));
|
||||
select.appendChild(createOption(FilePriority.Normal, (FilePriority.Normal === selectedPriority), "QBT_TR(Normal)QBT_TR[CONTEXT=PropListDelegate]"));
|
||||
select.appendChild(createOption(FilePriority.High, (FilePriority.High === selectedPriority), "QBT_TR(High)QBT_TR[CONTEXT=PropListDelegate]"));
|
||||
select.appendChild(createOption(FilePriority.Maximum, (FilePriority.Maximum === selectedPriority), "QBT_TR(Maximum)QBT_TR[CONTEXT=PropListDelegate]"));
|
||||
|
||||
// "Mixed" priority is for display only; it shouldn't be selectable
|
||||
const mixedPriorityOption = createOption(FilePriority.Mixed, (FilePriority.Mixed === selectedPriority), "QBT_TR(Mixed)QBT_TR[CONTEXT=PropListDelegate]");
|
||||
mixedPriorityOption.disabled = true;
|
||||
select.appendChild(mixedPriorityOption);
|
||||
|
||||
return select;
|
||||
};
|
||||
|
||||
const updatePriorityCombo = (combobox, id, fileId, selectedPriority) => {
|
||||
combobox.setAttribute("data-id", id);
|
||||
combobox.setAttribute("data-file-id", fileId);
|
||||
if (normalizePriority(combobox.value) !== selectedPriority)
|
||||
selectComboboxPriority(combobox, normalizePriority(selectedPriority));
|
||||
};
|
||||
|
||||
const selectComboboxPriority = (combobox, priority) => {
|
||||
const options = combobox.options;
|
||||
for (let i = 0; i < options.length; ++i) {
|
||||
const option = options[i];
|
||||
if (normalizePriority(option.value) === priority)
|
||||
option.selected = true;
|
||||
else
|
||||
option.selected = false;
|
||||
}
|
||||
|
||||
combobox.value = priority;
|
||||
};
|
||||
|
||||
const getComboboxPriority = (id) => {
|
||||
const row = torrentFilesTable.rows.get(id.toString());
|
||||
return normalizePriority(row.full_data.priority, 10);
|
||||
};
|
||||
|
||||
const switchGlobalCheckboxState = (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const rowIds = [];
|
||||
const fileIds = [];
|
||||
const checkbox = document.getElementById("tristate_cb");
|
||||
const priority = (checkbox.state === TriState.Checked) ? FilePriority.Ignored : FilePriority.Normal;
|
||||
|
||||
if (checkbox.state === TriState.Checked) {
|
||||
setCheckboxUnchecked(checkbox);
|
||||
torrentFilesTable.rows.forEach((row) => {
|
||||
const rowId = row.rowId;
|
||||
const fileId = row.full_data.fileId;
|
||||
const isChecked = (getCheckboxState(rowId) === TriState.Checked);
|
||||
if (isChecked) {
|
||||
rowIds.push(rowId);
|
||||
fileIds.push(fileId);
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
setCheckboxChecked(checkbox);
|
||||
torrentFilesTable.rows.forEach((row) => {
|
||||
const rowId = row.rowId;
|
||||
const fileId = row.full_data.fileId;
|
||||
const isUnchecked = (getCheckboxState(rowId) === TriState.Unchecked);
|
||||
if (isUnchecked) {
|
||||
rowIds.push(rowId);
|
||||
fileIds.push(fileId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (rowIds.length > 0) {
|
||||
setFilePriority(rowIds, fileIds, priority);
|
||||
for (const id of rowIds)
|
||||
updateParentFolder(id);
|
||||
}
|
||||
};
|
||||
|
||||
const updateGlobalCheckbox = () => {
|
||||
const checkbox = document.getElementById("tristate_cb");
|
||||
if (torrentFilesTable.isAllCheckboxesChecked())
|
||||
setCheckboxChecked(checkbox);
|
||||
else if (torrentFilesTable.isAllCheckboxesUnchecked())
|
||||
setCheckboxUnchecked(checkbox);
|
||||
else
|
||||
setCheckboxPartial(checkbox);
|
||||
};
|
||||
|
||||
const setCheckboxChecked = (checkbox) => {
|
||||
checkbox.state = TriState.Checked;
|
||||
checkbox.indeterminate = false;
|
||||
checkbox.checked = true;
|
||||
};
|
||||
|
||||
const setCheckboxUnchecked = (checkbox) => {
|
||||
checkbox.state = TriState.Unchecked;
|
||||
checkbox.indeterminate = false;
|
||||
checkbox.checked = false;
|
||||
};
|
||||
|
||||
const setCheckboxPartial = (checkbox) => {
|
||||
checkbox.state = TriState.Partial;
|
||||
checkbox.indeterminate = true;
|
||||
};
|
||||
|
||||
const getCheckboxState = (id) => {
|
||||
const row = torrentFilesTable.rows.get(id.toString());
|
||||
return Number(row.full_data.checked);
|
||||
};
|
||||
|
||||
const setFilePriority = (ids, fileIds, priority) => {
|
||||
priority = normalizePriority(priority);
|
||||
|
||||
if (onFilePriorityChanged)
|
||||
onFilePriorityChanged(fileIds, priority);
|
||||
|
||||
const ignore = (priority === FilePriority.Ignored);
|
||||
ids.forEach((id) => {
|
||||
id = id.toString();
|
||||
torrentFilesTable.setIgnored(id, ignore);
|
||||
|
||||
const row = torrentFilesTable.rows.get(id);
|
||||
row.full_data.priority = priority;
|
||||
row.full_data.checked = triStateFromPriority(priority);
|
||||
});
|
||||
};
|
||||
|
||||
const updateData = (files) => {
|
||||
const rows = files.map((file, index) => {
|
||||
const ignore = (file.priority === FilePriority.Ignored);
|
||||
const row = {
|
||||
fileId: index,
|
||||
checked: (ignore ? TriState.Unchecked : TriState.Checked),
|
||||
fileName: file.name,
|
||||
name: window.qBittorrent.Filesystem.fileName(file.name),
|
||||
size: file.size,
|
||||
progress: window.qBittorrent.Misc.toFixedPointString((file.progress * 100), 1),
|
||||
priority: normalizePriority(file.priority),
|
||||
remaining: (ignore ? 0 : (file.size * (1 - file.progress))),
|
||||
availability: file.availability
|
||||
};
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
addRowsToTable(rows);
|
||||
updateGlobalCheckbox();
|
||||
};
|
||||
|
||||
const addRowsToTable = (rows) => {
|
||||
const selectedFiles = torrentFilesTable.selectedRowsIds();
|
||||
let rowId = 0;
|
||||
|
||||
const rootNode = new window.qBittorrent.FileTree.FolderNode();
|
||||
|
||||
rows.forEach((row) => {
|
||||
const pathItems = row.fileName.split(window.qBittorrent.Filesystem.PathSeparator);
|
||||
|
||||
pathItems.pop(); // remove last item (i.e. file name)
|
||||
let parent = rootNode;
|
||||
pathItems.forEach((folderName) => {
|
||||
if (folderName === ".unwanted")
|
||||
return;
|
||||
|
||||
let folderNode = null;
|
||||
if (parent.children !== null) {
|
||||
for (let i = 0; i < parent.children.length; ++i) {
|
||||
const childFolder = parent.children[i];
|
||||
if (childFolder.name === folderName) {
|
||||
folderNode = childFolder;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (folderNode === null) {
|
||||
folderNode = new window.qBittorrent.FileTree.FolderNode();
|
||||
folderNode.path = (parent.path === "")
|
||||
? folderName
|
||||
: [parent.path, folderName].join(window.qBittorrent.Filesystem.PathSeparator);
|
||||
folderNode.name = folderName;
|
||||
folderNode.rowId = rowId;
|
||||
folderNode.root = parent;
|
||||
parent.addChild(folderNode);
|
||||
|
||||
++rowId;
|
||||
}
|
||||
|
||||
parent = folderNode;
|
||||
});
|
||||
|
||||
const isChecked = row.checked ? TriState.Checked : TriState.Unchecked;
|
||||
const remaining = (row.priority === FilePriority.Ignored) ? 0 : row.remaining;
|
||||
const childNode = new window.qBittorrent.FileTree.FileNode();
|
||||
childNode.name = row.name;
|
||||
childNode.path = row.fileName;
|
||||
childNode.rowId = rowId;
|
||||
childNode.size = row.size;
|
||||
childNode.checked = isChecked;
|
||||
childNode.remaining = remaining;
|
||||
childNode.progress = row.progress;
|
||||
childNode.priority = row.priority;
|
||||
childNode.availability = row.availability;
|
||||
childNode.root = parent;
|
||||
childNode.data = row;
|
||||
parent.addChild(childNode);
|
||||
|
||||
++rowId;
|
||||
});
|
||||
|
||||
torrentFilesTable.populateTable(rootNode);
|
||||
torrentFilesTable.updateTable();
|
||||
|
||||
if (selectedFiles.length > 0)
|
||||
torrentFilesTable.reselectRows(selectedFiles);
|
||||
};
|
||||
|
||||
const filesPriorityMenuClicked = (priority) => {
|
||||
const selectedRows = torrentFilesTable.selectedRowsIds();
|
||||
if (selectedRows.length === 0)
|
||||
return;
|
||||
|
||||
const rowIds = [];
|
||||
const fileIds = [];
|
||||
selectedRows.forEach((rowId) => {
|
||||
rowIds.push(rowId);
|
||||
fileIds.push(Number(torrentFilesTable.getRowFileId(rowId)));
|
||||
});
|
||||
|
||||
const uniqueRowIds = new Set();
|
||||
const uniqueFileIds = new Set();
|
||||
for (let i = 0; i < rowIds.length; ++i) {
|
||||
const rows = getAllChildren(rowIds[i], fileIds[i]);
|
||||
rows.rowIds.forEach((rowId) => {
|
||||
uniqueRowIds.add(rowId);
|
||||
});
|
||||
rows.fileIds.forEach((fileId) => {
|
||||
uniqueFileIds.add(fileId);
|
||||
});
|
||||
}
|
||||
|
||||
setFilePriority([...uniqueRowIds.keys()], [...uniqueFileIds.keys()], priority);
|
||||
for (const id of rowIds)
|
||||
updateParentFolder(id);
|
||||
};
|
||||
|
||||
const updateParentFolder = (id) => {
|
||||
const updateComplete = () => {
|
||||
// we've finished recursing
|
||||
updateGlobalCheckbox();
|
||||
torrentFilesTable.updateTable(true);
|
||||
};
|
||||
|
||||
const node = torrentFilesTable.getNode(id);
|
||||
const parent = node.parent;
|
||||
if (parent === torrentFilesTable.getRoot()) {
|
||||
updateComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
const siblings = parent.children;
|
||||
|
||||
let checkedCount = 0;
|
||||
let uncheckedCount = 0;
|
||||
let indeterminateCount = 0;
|
||||
let desiredComboboxPriority = null;
|
||||
for (const sibling of siblings) {
|
||||
switch (getCheckboxState(sibling.rowId)) {
|
||||
case TriState.Checked:
|
||||
++checkedCount;
|
||||
break;
|
||||
case TriState.Unchecked:
|
||||
++uncheckedCount;
|
||||
break;
|
||||
case TriState.Partial:
|
||||
++indeterminateCount;
|
||||
break;
|
||||
}
|
||||
|
||||
if (desiredComboboxPriority === null)
|
||||
desiredComboboxPriority = getComboboxPriority(sibling.rowId);
|
||||
else if (desiredComboboxPriority !== getComboboxPriority(sibling.rowId))
|
||||
desiredComboboxPriority = FilePriority.Mixed;
|
||||
}
|
||||
|
||||
const currentCheckboxState = getCheckboxState(parent.rowId);
|
||||
let desiredCheckboxState = TriState.Unchecked;
|
||||
if ((indeterminateCount > 0) || ((checkedCount > 0) && (uncheckedCount > 0)))
|
||||
desiredCheckboxState = TriState.Partial;
|
||||
else if (checkedCount > 0)
|
||||
desiredCheckboxState = TriState.Checked;
|
||||
|
||||
const currentComboboxPriority = getComboboxPriority(parent.rowId);
|
||||
if ((currentCheckboxState !== desiredCheckboxState) || (currentComboboxPriority !== desiredComboboxPriority)) {
|
||||
const row = torrentFilesTable.rows.get(parent.rowId.toString());
|
||||
row.full_data.priority = desiredComboboxPriority;
|
||||
row.full_data.checked = desiredCheckboxState;
|
||||
|
||||
updateParentFolder(parent.rowId);
|
||||
}
|
||||
else {
|
||||
updateComplete();
|
||||
}
|
||||
};
|
||||
|
||||
const init = (tableId, tableClass, onFilePriorityChangedHandler = undefined, onFileRenameHandler = undefined) => {
|
||||
if (onFilePriorityChangedHandler !== undefined)
|
||||
onFilePriorityChanged = onFilePriorityChangedHandler;
|
||||
|
||||
torrentFilesTable = new tableClass();
|
||||
|
||||
const torrentFilesContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
|
||||
targets: `#${tableId} tbody tr`,
|
||||
menu: "torrentFilesMenu",
|
||||
actions: {
|
||||
Rename: (element, ref) => {
|
||||
if (onFileRenameHandler !== undefined) {
|
||||
const nodes = torrentFilesTable.selectedRowsIds().map(row => torrentFilesTable.getNode(row));
|
||||
onFileRenameHandler(torrentFilesTable.selectedRows, nodes);
|
||||
}
|
||||
},
|
||||
|
||||
FilePrioIgnore: (element, ref) => {
|
||||
filesPriorityMenuClicked(FilePriority.Ignored);
|
||||
},
|
||||
FilePrioNormal: (element, ref) => {
|
||||
filesPriorityMenuClicked(FilePriority.Normal);
|
||||
},
|
||||
FilePrioHigh: (element, ref) => {
|
||||
filesPriorityMenuClicked(FilePriority.High);
|
||||
},
|
||||
FilePrioMaximum: (element, ref) => {
|
||||
filesPriorityMenuClicked(FilePriority.Maximum);
|
||||
}
|
||||
},
|
||||
offsets: {
|
||||
x: 0,
|
||||
y: 2
|
||||
},
|
||||
});
|
||||
|
||||
torrentFilesTable.setup(tableId, "torrentFilesTableFixedHeaderDiv", torrentFilesContextMenu, true);
|
||||
// inject checkbox into table header
|
||||
const tableHeaders = document.querySelectorAll("#torrentFilesTableFixedHeaderDiv .dynamicTableHeader th");
|
||||
if (tableHeaders.length > 0) {
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.id = "tristate_cb";
|
||||
checkbox.addEventListener("click", switchGlobalCheckboxState);
|
||||
|
||||
const checkboxTH = tableHeaders[0];
|
||||
checkboxTH.appendChild(checkbox);
|
||||
}
|
||||
|
||||
// default sort by name column
|
||||
if (torrentFilesTable.getSortedColumn() === null)
|
||||
torrentFilesTable.setSortedColumn("name");
|
||||
|
||||
// listen for changes to torrentFilesFilterInput
|
||||
document.getElementById("torrentFilesFilterInput").addEventListener("input", (event) => {
|
||||
clearTimeout(torrentFilesFilterInputTimer);
|
||||
torrentFilesFilterInputTimer = -1;
|
||||
|
||||
const value = document.getElementById("torrentFilesFilterInput").value;
|
||||
torrentFilesTable.setFilter(value);
|
||||
|
||||
torrentFilesFilterInputTimer = setTimeout(() => {
|
||||
torrentFilesFilterInputTimer = -1;
|
||||
|
||||
torrentFilesTable.updateTable();
|
||||
|
||||
if (value.trim() === "")
|
||||
torrentFilesTable.collapseAllNodes();
|
||||
else
|
||||
torrentFilesTable.expandAllNodes();
|
||||
}, window.qBittorrent.Misc.FILTER_INPUT_DELAY);
|
||||
});
|
||||
|
||||
return torrentFilesTable;
|
||||
};
|
||||
|
||||
const clearFilterInputTimer = () => {
|
||||
clearTimeout(torrentFilesFilterInputTimer);
|
||||
torrentFilesFilterInputTimer = -1;
|
||||
};
|
||||
|
||||
return exports();
|
||||
})();
|
||||
Object.freeze(window.qBittorrent.TorrentContent);
|
|
@ -1,195 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="${LANG}" class="dark">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>QBT_TR(Upload local torrent)QBT_TR[CONTEXT=HttpServer]</title>
|
||||
<link rel="stylesheet" href="css/style.css?v=${CACHEID}" type="text/css">
|
||||
<link rel="stylesheet" href="css/Window.css?v=${CACHEID}" type="text/css">
|
||||
<script defer src="scripts/localpreferences.js?v=${CACHEID}"></script>
|
||||
<script defer src="scripts/color-scheme.js?v=${CACHEID}"></script>
|
||||
<script defer src="scripts/download.js?v=${CACHEID}"></script>
|
||||
<script defer src="scripts/pathAutofill.js?v=${CACHEID}"></script>
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
window.addEventListener("DOMContentLoaded", (event) => {
|
||||
window.addEventListener("keydown", (event) => {
|
||||
switch (event.key) {
|
||||
case "Escape":
|
||||
event.preventDefault();
|
||||
window.parent.qBittorrent.Client.closeFrameWindow(window);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
let submitted = false;
|
||||
|
||||
document.getElementById("uploadForm").addEventListener("submit", (event) => {
|
||||
document.getElementById("startTorrentHidden").value = document.getElementById("startTorrent").checked ? "false" : "true";
|
||||
|
||||
document.getElementById("dlLimitHidden").value = Number(document.getElementById("dlLimitText").value) * 1024;
|
||||
document.getElementById("upLimitHidden").value = Number(document.getElementById("upLimitText").value) * 1024;
|
||||
|
||||
document.getElementById("upload_spinner").style.display = "block";
|
||||
submitted = true;
|
||||
});
|
||||
|
||||
document.getElementById("upload_frame").addEventListener("load", (event) => {
|
||||
if (submitted)
|
||||
window.parent.qBittorrent.Client.closeFrameWindow(window);
|
||||
});
|
||||
|
||||
const userAgent = (navigator.userAgentData?.platform ?? navigator.platform).toLowerCase();
|
||||
if (userAgent.includes("ipad") || userAgent.includes("iphone") || (userAgent.includes("mac") && (navigator.maxTouchPoints > 1)))
|
||||
document.getElementById("fileselect").accept = ".torrent";
|
||||
|
||||
window.qBittorrent.pathAutofill.attachPathAutofill();
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<iframe id="upload_frame" name="upload_frame" class="invisible" title="" src="about:blank"></iframe>
|
||||
<form action="api/v2/torrents/add" enctype="multipart/form-data" method="post" id="uploadForm" style="text-align: center;" target="upload_frame" autocorrect="off" autocapitalize="none">
|
||||
<div style="margin-top: 25px; display: inline-block; border: 1px solid lightgrey; border-radius: 4px;">
|
||||
<input type="file" id="fileselect" accept=".torrent, application/x-bittorrent" name="fileselect[]" multiple aria-label="QBT_TR(Select .torrent files)QBT_TR[CONTEXT=AddNewTorrentDialog]">
|
||||
</div>
|
||||
<fieldset class="settings" style="border: 0; text-align: left; margin-top: 12px;">
|
||||
<legend>QBT_TR(Torrent options)QBT_TR[CONTEXT=AddNewTorrentDialog]</legend>
|
||||
<table style="margin: auto;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="autoTMM">QBT_TR(Torrent Management Mode:)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td>
|
||||
<select id="autoTMM" name="autoTMM" onchange="qBittorrent.Download.changeTMM(this)">
|
||||
<option selected value="false">QBT_TR(Manual)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
<option value="true">QBT_TR(Automatic)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="savepath">QBT_TR(Save files to location:)QBT_TR[CONTEXT=HttpServer]</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="savepath" name="savepath" class="pathDirectory" style="width: 16em;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="rename">QBT_TR(Rename torrent)QBT_TR[CONTEXT=HttpServer]</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="rename" name="rename" style="width: 16em;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label id="categoryLabel" for="categorySelect">QBT_TR(Category:)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td>
|
||||
<div class="select-watched-folder-editable">
|
||||
<select id="categorySelect" onchange="qBittorrent.Download.changeCategorySelect(this)">
|
||||
<option selected value="\other"></option>
|
||||
</select>
|
||||
<input type="text" name="category" aria-labelledby="categoryLabel">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="startTorrent">QBT_TR(Start torrent)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="hidden" id="startTorrentHidden" name="stopped">
|
||||
<input type="checkbox" id="startTorrent">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="addToTopOfQueue">QBT_TR(Add to top of queue)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" id="addToTopOfQueue" name="addToTopOfQueue" value="true">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="stopCondition">QBT_TR(Stop condition:)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td>
|
||||
<select id="stopCondition" name="stopCondition">
|
||||
<option selected value="None">QBT_TR(None)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
<option value="MetadataReceived">QBT_TR(Metadata received)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
<option value="FilesChecked">QBT_TR(Files checked)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="skip_checking">QBT_TR(Skip hash check)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" id="skip_checking" name="skip_checking" value="true">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="contentLayout">QBT_TR(Content layout:)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
|
||||
</td>
|
||||
<td>
|
||||
<select id="contentLayout" name="contentLayout">
|
||||
<option selected value="Original">QBT_TR(Original)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
<option value="Subfolder">QBT_TR(Create subfolder)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
<option value="NoSubfolder">QBT_TR(Don't create subfolder)QBT_TR[CONTEXT=AddNewTorrentDialog]</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="sequentialDownload">QBT_TR(Download in sequential order)QBT_TR[CONTEXT=TransferListWidget]</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" id="sequentialDownload" name="sequentialDownload" value="true">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="firstLastPiecePrio">QBT_TR(Download first and last pieces first)QBT_TR[CONTEXT=TransferListWidget]</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" id="firstLastPiecePrio" name="firstLastPiecePrio" value="true">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="dlLimitText">QBT_TR(Limit download rate)QBT_TR[CONTEXT=HttpServer]</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="hidden" id="dlLimitHidden" name="dlLimit">
|
||||
<input type="text" id="dlLimitText" style="width: 16em;" placeholder="KiB/s">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="upLimitText">QBT_TR(Limit upload rate)QBT_TR[CONTEXT=HttpServer]</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="hidden" id="upLimitHidden" name="upLimit">
|
||||
<input type="text" id="upLimitText" style="width: 16em;" placeholder="KiB/s">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="submitbutton" style="margin-top: 30px; text-align: center;">
|
||||
<button type="submit" style="font-size: 1em;">QBT_TR(Upload Torrents)QBT_TR[CONTEXT=HttpServer]</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
<div id="upload_spinner" class="mochaSpinner"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -294,10 +294,10 @@
|
|||
menu: "rssArticleMenu",
|
||||
actions: {
|
||||
Download: (el) => {
|
||||
let dlString = "";
|
||||
for (const rowID of rssArticleTable.selectedRows)
|
||||
dlString += `${rssArticleTable.getRow(rowID).full_data.torrentURL}\n`;
|
||||
showDownloadPage([dlString]);
|
||||
for (const rowID of rssArticleTable.selectedRows) {
|
||||
const { name, torrentURL } = rssArticleTable.getRow(rowID).full_data;
|
||||
window.qBittorrent.Client.createAddTorrentWindow(name, torrentURL);
|
||||
}
|
||||
},
|
||||
OpenNews: (el) => {
|
||||
for (const rowID of rssArticleTable.selectedRows)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<RCC>
|
||||
<qresource prefix="/www">
|
||||
<file>private/addpeers.html</file>
|
||||
<file>private/addtorrent.html</file>
|
||||
<file>private/addtrackers.html</file>
|
||||
<file>private/addwebseeds.html</file>
|
||||
<file>private/confirmfeeddeletion.html</file>
|
||||
|
@ -392,11 +393,11 @@
|
|||
<file>private/rename_file.html</file>
|
||||
<file>private/rename_files.html</file>
|
||||
<file>private/rename_rule.html</file>
|
||||
<file>private/scripts/addtorrent.js</file>
|
||||
<file>private/scripts/cache.js</file>
|
||||
<file>private/scripts/client.js</file>
|
||||
<file>private/scripts/color-scheme.js</file>
|
||||
<file>private/scripts/contextmenu.js</file>
|
||||
<file>private/scripts/download.js</file>
|
||||
<file>private/scripts/dynamicTable.js</file>
|
||||
<file>private/scripts/file-tree.js</file>
|
||||
<file>private/scripts/filesystem.js</file>
|
||||
|
@ -420,10 +421,10 @@
|
|||
<file>private/scripts/rename-files.js</file>
|
||||
<file>private/scripts/search.js</file>
|
||||
<file>private/scripts/statistics.js</file>
|
||||
<file>private/scripts/torrent-content.js</file>
|
||||
<file>private/setlocation.html</file>
|
||||
<file>private/shareratio.html</file>
|
||||
<file>private/speedlimit.html</file>
|
||||
<file>private/upload.html</file>
|
||||
<file>private/views/about.html</file>
|
||||
<file>private/views/aboutToolbar.html</file>
|
||||
<file>private/views/confirmAutoTMM.html</file>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue