WebUI: Support managing category download path

PR #22938.
This commit is contained in:
Thomas Piccirello 2025-07-14 19:29:03 +02:00 committed by GitHub
commit 7aebd07f9f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 144 additions and 51 deletions

View file

@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>QBT_TR(New Category)QBT_TR[CONTEXT=TransferListWidget]</title> <title>QBT_TR(New Category)QBT_TR[CONTEXT=Category]</title>
<link rel="stylesheet" href="css/style.css?v=${CACHEID}" type="text/css"> <link rel="stylesheet" href="css/style.css?v=${CACHEID}" type="text/css">
<script defer src="scripts/localpreferences.js?v=${CACHEID}"></script> <script defer src="scripts/localpreferences.js?v=${CACHEID}"></script>
<script defer src="scripts/color-scheme.js?v=${CACHEID}"></script> <script defer src="scripts/color-scheme.js?v=${CACHEID}"></script>
@ -26,27 +26,78 @@
} }
}); });
const defaultDownloadPath = () => {
const category = document.getElementById("categoryName").value.trim();
return `${pref.temp_path}/${category}`;
};
const setDownloadPath = (option) => {
const downloadPath = document.getElementById("downloadPath");
const defaultPath = defaultDownloadPath();
switch (option) {
case "default":
downloadPath.disabled = true;
downloadPath.value = pref.temp_path_enabled ? defaultPath : "";
downloadPath.placeholder = "";
break;
case "yes":
downloadPath.disabled = false;
if ((categoryData !== undefined) && (categoryData.downloadPath !== false) && (categoryData.downloadPath !== null))
downloadPath.value = categoryData.downloadPath;
else
downloadPath.value = "";
downloadPath.placeholder = defaultPath;
break;
case "no":
downloadPath.disabled = true;
downloadPath.value = "";
downloadPath.placeholder = "";
break;
}
};
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
const uriAction = window.qBittorrent.Misc.safeTrim(searchParams.get("action")); const uriAction = window.qBittorrent.Misc.safeTrim(searchParams.get("action"));
const uriHashes = window.qBittorrent.Misc.safeTrim(searchParams.get("hashes")); const uriHashes = window.qBittorrent.Misc.safeTrim(searchParams.get("hashes"));
const uriCategoryName = window.qBittorrent.Misc.safeTrim(searchParams.get("categoryName")); const uriCategoryName = window.qBittorrent.Misc.safeTrim(searchParams.get("categoryName"));
const uriSavePath = window.qBittorrent.Misc.safeTrim(searchParams.get("savePath"));
const pref = window.parent.qBittorrent.Cache.preferences.get();
const categoryData = window.parent.qBittorrent.Client.categoryMap.get(uriCategoryName);
const useDownloadPathElem = document.getElementById("useDownloadPath");
const categoryNameElem = document.getElementById("categoryName");
const savePathElem = document.getElementById("savePath");
if (uriAction === "edit") { if (uriAction === "edit") {
if (!uriCategoryName) if (!uriCategoryName)
return; return;
document.getElementById("categoryName").disabled = true; categoryNameElem.disabled = true;
document.getElementById("categoryName").value = window.qBittorrent.Misc.escapeHtml(uriCategoryName); categoryNameElem.value = uriCategoryName;
document.getElementById("savePath").value = window.qBittorrent.Misc.escapeHtml(uriSavePath); savePathElem.value = categoryData.savePath;
document.getElementById("savePath").focus(); savePathElem.placeholder = `${pref.save_path}/${uriCategoryName}`;
savePathElem.focus();
switch (categoryData.downloadPath) {
case false:
useDownloadPathElem.selectedIndex = 2;
setDownloadPath("no");
break;
case null:
useDownloadPathElem.selectedIndex = 0;
setDownloadPath("default");
break;
default:
useDownloadPathElem.selectedIndex = 1;
setDownloadPath("yes");
break;
}
} }
else if (uriAction === "createSubcategory") { else if (uriAction === "createSubcategory") {
document.getElementById("categoryName").value = window.qBittorrent.Misc.escapeHtml(uriCategoryName); categoryNameElem.value = uriCategoryName;
document.getElementById("categoryName").focus(); categoryNameElem.focus();
} }
else { else {
document.getElementById("categoryName").focus(); categoryNameElem.focus();
} }
document.getElementById("categoryNameButton").addEventListener("click", (e) => { document.getElementById("categoryNameButton").addEventListener("click", (e) => {
@ -66,6 +117,16 @@
return true; return true;
}; };
const dlPathParams = {};
const useDownloadPath = document.getElementById("useDownloadPath");
if (useDownloadPath.value === "no") {
dlPathParams.downloadPathEnabled = false;
}
else if (useDownloadPath.value === "yes") {
dlPathParams.downloadPathEnabled = true;
dlPathParams.downloadPath = document.getElementById("downloadPath").value.trim();
}
switch (uriAction) { switch (uriAction) {
case "set": case "set":
if ((uriHashes === "") || !verifyCategoryName(categoryName)) if ((uriHashes === "") || !verifyCategoryName(categoryName))
@ -74,6 +135,7 @@
fetch("api/v2/torrents/createCategory", { fetch("api/v2/torrents/createCategory", {
method: "POST", method: "POST",
body: new URLSearchParams({ body: new URLSearchParams({
...dlPathParams,
category: categoryName, category: categoryName,
savePath: savePath savePath: savePath
}) })
@ -110,6 +172,7 @@
fetch("api/v2/torrents/createCategory", { fetch("api/v2/torrents/createCategory", {
method: "POST", method: "POST",
body: new URLSearchParams({ body: new URLSearchParams({
...body,
category: categoryName, category: categoryName,
savePath: savePath savePath: savePath
}) })
@ -129,6 +192,7 @@
fetch("api/v2/torrents/editCategory", { fetch("api/v2/torrents/editCategory", {
method: "POST", method: "POST",
body: new URLSearchParams({ body: new URLSearchParams({
...body,
category: uriCategoryName, // category name can't be changed category: uriCategoryName, // category name can't be changed
savePath: savePath savePath: savePath
}) })
@ -146,6 +210,10 @@
} }
}); });
useDownloadPathElem.addEventListener("change", (e) => {
setDownloadPath(e.target.value);
});
window.qBittorrent.pathAutofill.attachPathAutofill(); window.qBittorrent.pathAutofill.attachPathAutofill();
}); });
</script> </script>
@ -153,10 +221,32 @@
<body> <body>
<div style="padding: 10px 10px 0px 10px;"> <div style="padding: 10px 10px 0px 10px;">
<label for="categoryName" style="font-weight: bold;">QBT_TR(Category:)QBT_TR[CONTEXT=TransferListWidget]</label> <div style="display: flex; align-items: center;">
<input type="text" id="categoryName" style="width: 99%;"> <label for="categoryName">QBT_TR(Category:)QBT_TR[CONTEXT=Category]</label>
<label for="savePath" style="font-weight: bold;">QBT_TR(Save path:)QBT_TR[CONTEXT=TransferListWidget]</label> <input type="text" id="categoryName" style="flex-grow: 1; margin-left: 10px;">
<input type="text" id="savePath" class="pathDirectory" style="width: 99%;"> </div>
<div style="display: flex; align-items: center;">
<label for="savePath">QBT_TR(Save path:)QBT_TR[CONTEXT=Category]</label>
<input type="text" id="savePath" class="pathDirectory" style="flex-grow: 1; margin-left: 10px;">
</div>
<fieldset class="settings" style="text-align: left;">
<legend>QBT_TR(Save path for incomplete torrents:)QBT_TR[CONTEXT=Category]</legend>
<div style="display: flex; align-items: center;">
<label for="useDownloadPath">QBT_TR(Use another path for incomplete torrents:)QBT_TR[CONTEXT=Category]</label>
<select id="useDownloadPath">
<option selected value="default">QBT_TR(Default)QBT_TR[CONTEXT=Category]</option>
<option value="yes">QBT_TR(Yes)QBT_TR[CONTEXT=Category]</option>
<option value="no">QBT_TR(No)QBT_TR[CONTEXT=Category]</option>
</select>
</div>
<div style="display: flex; align-items: center;">
<label for="downloadPath">QBT_TR(Path:)QBT_TR[CONTEXT=Category]</label>
<input type="text" id="downloadPath" class="pathDirectory" style="flex-grow: 1; margin-left: 10px;">
</div>
</fieldset>
<div style="text-align: center; padding-top: 10px;"> <div style="text-align: center; padding-top: 10px;">
<input type="button" value="QBT_TR(OK)QBT_TR[CONTEXT=Category]" id="categoryNameButton"> <input type="button" value="QBT_TR(OK)QBT_TR[CONTEXT=Category]" id="categoryNameButton">
</div> </div>

View file

@ -42,10 +42,17 @@ window.qBittorrent.Client ??= (() => {
showLogViewer: showLogViewer, showLogViewer: showLogViewer,
isShowSearchEngine: isShowSearchEngine, isShowSearchEngine: isShowSearchEngine,
isShowRssReader: isShowRssReader, isShowRssReader: isShowRssReader,
isShowLogViewer: isShowLogViewer isShowLogViewer: isShowLogViewer,
categoryMap: categoryMap,
tagMap: tagMap
}; };
}; };
// Map<category: String, {savePath: String, torrents: Set}>
const categoryMap = new Map();
// Map<tag: String, torrents: Set>
const tagMap = new Map();
let cacheAllSettled; let cacheAllSettled;
const setup = () => { const setup = () => {
// fetch various data and store it in memory // fetch various data and store it in memory
@ -146,9 +153,6 @@ const displayFullURLTrackerColumn = LocalPreferences.get("full_url_tracker_colum
const CATEGORIES_ALL = "b4af0e4c-e76d-4bac-a392-46cbc18d9655"; const CATEGORIES_ALL = "b4af0e4c-e76d-4bac-a392-46cbc18d9655";
const CATEGORIES_UNCATEGORIZED = "e24bd469-ea22-404c-8e2e-a17c82f37ea0"; const CATEGORIES_UNCATEGORIZED = "e24bd469-ea22-404c-8e2e-a17c82f37ea0";
// Map<category: String, {savePath: String, torrents: Set}>
const categoryMap = new Map();
let selectedCategory = LocalPreferences.get("selected_category", CATEGORIES_ALL); let selectedCategory = LocalPreferences.get("selected_category", CATEGORIES_ALL);
let setCategoryFilter = () => {}; let setCategoryFilter = () => {};
@ -156,9 +160,6 @@ let setCategoryFilter = () => {};
const TAGS_ALL = "b4af0e4c-e76d-4bac-a392-46cbc18d9655"; const TAGS_ALL = "b4af0e4c-e76d-4bac-a392-46cbc18d9655";
const TAGS_UNTAGGED = "e24bd469-ea22-404c-8e2e-a17c82f37ea0"; const TAGS_UNTAGGED = "e24bd469-ea22-404c-8e2e-a17c82f37ea0";
// Map<tag: String, torrents: Set>
const tagMap = new Map();
let selectedTag = LocalPreferences.get("selected_tag", TAGS_ALL); let selectedTag = LocalPreferences.get("selected_tag", TAGS_ALL);
let setTagFilter = () => {}; let setTagFilter = () => {};
@ -389,7 +390,7 @@ window.addEventListener("DOMContentLoaded", (event) => {
return false; return false;
let removed = false; let removed = false;
for (const data of categoryMap.values()) { for (const data of window.qBittorrent.Client.categoryMap.values()) {
const deleteResult = data.torrents.delete(hash); const deleteResult = data.torrents.delete(hash);
removed ||= deleteResult; removed ||= deleteResult;
} }
@ -407,12 +408,12 @@ window.addEventListener("DOMContentLoaded", (event) => {
return true; return true;
} }
let categoryData = categoryMap.get(category); let categoryData = window.qBittorrent.Client.categoryMap.get(category);
if (categoryData === undefined) { // This should not happen if (categoryData === undefined) { // This should not happen
categoryData = { categoryData = {
torrents: new Set() torrents: new Set()
}; };
categoryMap.set(category, categoryData); window.qBittorrent.Client.categoryMap.set(category, categoryData);
} }
if (categoryData.torrents.has(hash)) if (categoryData.torrents.has(hash))
@ -428,7 +429,7 @@ window.addEventListener("DOMContentLoaded", (event) => {
return false; return false;
let removed = false; let removed = false;
for (const torrents of tagMap.values()) { for (const torrents of window.qBittorrent.Client.tagMap.values()) {
const deleteResult = torrents.delete(hash); const deleteResult = torrents.delete(hash);
removed ||= deleteResult; removed ||= deleteResult;
} }
@ -448,10 +449,10 @@ window.addEventListener("DOMContentLoaded", (event) => {
const tags = torrent["tags"].split(", "); const tags = torrent["tags"].split(", ");
let added = false; let added = false;
for (const tag of tags) { for (const tag of tags) {
let torrents = tagMap.get(tag); let torrents = window.qBittorrent.Client.tagMap.get(tag);
if (torrents === undefined) { // This should not happen if (torrents === undefined) { // This should not happen
torrents = new Set(); torrents = new Set();
tagMap.set(tag, torrents); window.qBittorrent.Client.tagMap.set(tag, torrents);
} }
if (!torrents.has(hash)) { if (!torrents.has(hash)) {
@ -550,7 +551,7 @@ window.addEventListener("DOMContentLoaded", (event) => {
} }
const sortedCategories = []; const sortedCategories = [];
for (const [category, categoryData] of categoryMap) { for (const [category, categoryData] of window.qBittorrent.Client.categoryMap) {
sortedCategories.push({ sortedCategories.push({
categoryName: category, categoryName: category,
categoryCount: categoryData.torrents.size, categoryCount: categoryData.torrents.size,
@ -651,7 +652,7 @@ window.addEventListener("DOMContentLoaded", (event) => {
tagFilterList.appendChild(createLink(TAGS_UNTAGGED, "QBT_TR(Untagged)QBT_TR[CONTEXT=TagFilterModel]", untagged)); tagFilterList.appendChild(createLink(TAGS_UNTAGGED, "QBT_TR(Untagged)QBT_TR[CONTEXT=TagFilterModel]", untagged));
const sortedTags = []; const sortedTags = [];
for (const [tag, torrents] of tagMap) { for (const [tag, torrents] of window.qBittorrent.Client.tagMap) {
sortedTags.push({ sortedTags.push({
tagName: tag, tagName: tag,
tagSize: torrents.size tagSize: torrents.size
@ -815,8 +816,8 @@ window.addEventListener("DOMContentLoaded", (event) => {
updateTrackers = true; updateTrackers = true;
updateTorrents = true; updateTorrents = true;
torrentsTable.clear(); torrentsTable.clear();
categoryMap.clear(); window.qBittorrent.Client.categoryMap.clear();
tagMap.clear(); window.qBittorrent.Client.tagMap.clear();
trackerMap.clear(); trackerMap.clear();
} }
if (responseJSON["rid"]) if (responseJSON["rid"])
@ -827,35 +828,38 @@ window.addEventListener("DOMContentLoaded", (event) => {
continue; continue;
const responseData = responseJSON["categories"][responseName]; const responseData = responseJSON["categories"][responseName];
const categoryData = categoryMap.get(responseName); const categoryData = window.qBittorrent.Client.categoryMap.get(responseName);
if (categoryData === undefined) { if (categoryData === undefined) {
categoryMap.set(responseName, { window.qBittorrent.Client.categoryMap.set(responseName, {
savePath: responseData.savePath, savePath: responseData.savePath,
downloadPath: responseData.download_path ?? null,
torrents: new Set() torrents: new Set()
}); });
} }
else { else {
// only the save path can change for existing categories if (responseData.savePath !== undefined)
categoryData.savePath = responseData.savePath; categoryData.savePath = responseData.savePath;
if (responseData.download_path !== undefined)
categoryData.downloadPath = responseData.download_path;
} }
} }
updateCategories = true; updateCategories = true;
} }
if (responseJSON["categories_removed"]) { if (responseJSON["categories_removed"]) {
for (const category of responseJSON["categories_removed"]) for (const category of responseJSON["categories_removed"])
categoryMap.delete(category); window.qBittorrent.Client.categoryMap.delete(category);
updateCategories = true; updateCategories = true;
} }
if (responseJSON["tags"]) { if (responseJSON["tags"]) {
for (const tag of responseJSON["tags"]) { for (const tag of responseJSON["tags"]) {
if (!tagMap.has(tag)) if (!window.qBittorrent.Client.tagMap.has(tag))
tagMap.set(tag, new Set()); window.qBittorrent.Client.tagMap.set(tag, new Set());
} }
updateTags = true; updateTags = true;
} }
if (responseJSON["tags_removed"]) { if (responseJSON["tags_removed"]) {
for (const tag of responseJSON["tags_removed"]) for (const tag of responseJSON["tags_removed"])
tagMap.delete(tag); window.qBittorrent.Client.tagMap.delete(tag);
updateTags = true; updateTags = true;
} }
if (responseJSON["trackers"]) { if (responseJSON["trackers"]) {
@ -945,11 +949,11 @@ window.addEventListener("DOMContentLoaded", (event) => {
if (updateCategories) { if (updateCategories) {
updateCategoryList(); updateCategoryList();
window.qBittorrent.TransferList.contextMenu.updateCategoriesSubMenu(categoryMap); window.qBittorrent.TransferList.contextMenu.updateCategoriesSubMenu(window.qBittorrent.Client.categoryMap);
} }
if (updateTags) { if (updateTags) {
updateTagList(); updateTagList();
window.qBittorrent.TransferList.contextMenu.updateTagsSubMenu(tagMap); window.qBittorrent.TransferList.contextMenu.updateTagsSubMenu(window.qBittorrent.Client.tagMap);
} }
if (updateTrackers) if (updateTrackers)
updateTrackerList(); updateTrackerList();

View file

@ -449,7 +449,7 @@ window.qBittorrent.ContextMenu ??= (() => {
this.setEnabled("copyInfohash2", thereAreV2Hashes); this.setEnabled("copyInfohash2", thereAreV2Hashes);
const contextTagList = document.getElementById("contextTagList"); const contextTagList = document.getElementById("contextTagList");
for (const tag of tagMap.keys()) { for (const tag of window.qBittorrent.Client.tagMap.keys()) {
const checkbox = contextTagList.querySelector(`a[href="#Tag/${tag}"] input[type="checkbox"]`); const checkbox = contextTagList.querySelector(`a[href="#Tag/${tag}"] input[type="checkbox"]`);
const count = tagCount.get(tag); const count = tagCount.get(tag);
const hasCount = (count !== undefined); const hasCount = (count !== undefined);
@ -459,7 +459,7 @@ window.qBittorrent.ContextMenu ??= (() => {
} }
const contextCategoryList = document.getElementById("contextCategoryList"); const contextCategoryList = document.getElementById("contextCategoryList");
for (const category of categoryMap.keys()) { for (const category of window.qBittorrent.Client.categoryMap.keys()) {
const categoryIcon = contextCategoryList.querySelector(`a[href$="#Category/${category}"] img`); const categoryIcon = contextCategoryList.querySelector(`a[href$="#Category/${category}"] img`);
const count = categoryCount.get(category); const count = categoryCount.get(category);
const isEqual = ((count !== undefined) && (count === selectedRows.length)); const isEqual = ((count !== undefined) && (count === selectedRows.length));

View file

@ -1630,7 +1630,7 @@ window.qBittorrent.DynamicTable ??= (() => {
return false; return false;
} }
else { else {
const selectedCategory = categoryMap.get(category); const selectedCategory = window.qBittorrent.Client.categoryMap.get(category);
if (selectedCategory !== undefined) { if (selectedCategory !== undefined) {
const selectedCategoryName = `${category}/`; const selectedCategoryName = `${category}/`;
const torrentCategoryName = `${row["full_data"].category}/`; const torrentCategoryName = `${row["full_data"].category}/`;

View file

@ -882,7 +882,7 @@ const initializeWindows = () => {
paddingVertical: 0, paddingVertical: 0,
paddingHorizontal: 0, paddingHorizontal: 0,
width: window.qBittorrent.Dialog.limitWidthToViewport(400), width: window.qBittorrent.Dialog.limitWidthToViewport(400),
height: 150 height: 200
}); });
}; };
@ -923,7 +923,7 @@ const initializeWindows = () => {
paddingVertical: 0, paddingVertical: 0,
paddingHorizontal: 0, paddingHorizontal: 0,
width: window.qBittorrent.Dialog.limitWidthToViewport(400), width: window.qBittorrent.Dialog.limitWidthToViewport(400),
height: 150 height: 200
}); });
}; };
@ -945,7 +945,7 @@ const initializeWindows = () => {
paddingVertical: 0, paddingVertical: 0,
paddingHorizontal: 0, paddingHorizontal: 0,
width: window.qBittorrent.Dialog.limitWidthToViewport(400), width: window.qBittorrent.Dialog.limitWidthToViewport(400),
height: 150 height: 200
}); });
}; };
@ -953,8 +953,7 @@ const initializeWindows = () => {
const contentURL = new URL("newcategory.html", window.location); const contentURL = new URL("newcategory.html", window.location);
contentURL.search = new URLSearchParams({ contentURL.search = new URLSearchParams({
action: "edit", action: "edit",
categoryName: category, categoryName: category
savePath: categoryMap.get(category).savePath
}); });
new MochaUI.Window({ new MochaUI.Window({
id: "editCategoryPage", id: "editCategoryPage",
@ -968,7 +967,7 @@ const initializeWindows = () => {
paddingVertical: 0, paddingVertical: 0,
paddingHorizontal: 0, paddingHorizontal: 0,
width: window.qBittorrent.Dialog.limitWidthToViewport(400), width: window.qBittorrent.Dialog.limitWidthToViewport(400),
height: 150 height: 200
}); });
}; };
@ -990,7 +989,7 @@ const initializeWindows = () => {
deleteUnusedCategoriesFN = () => { deleteUnusedCategoriesFN = () => {
const categories = []; const categories = [];
for (const category of categoryMap.keys()) { for (const category of window.qBittorrent.Client.categoryMap.keys()) {
if (torrentsTable.getFilteredTorrentsNumber("all", category, TAGS_ALL, TRACKERS_ALL) === 0) if (torrentsTable.getFilteredTorrentsNumber("all", category, TAGS_ALL, TRACKERS_ALL) === 0)
categories.push(category); categories.push(category);
} }
@ -1095,7 +1094,7 @@ const initializeWindows = () => {
deleteUnusedTagsFN = () => { deleteUnusedTagsFN = () => {
const tags = []; const tags = [];
for (const tag of tagMap.keys()) { for (const tag of window.qBittorrent.Client.tagMap.keys()) {
if (torrentsTable.getFilteredTorrentsNumber("all", CATEGORIES_ALL, tag, TRACKERS_ALL) === 0) if (torrentsTable.getFilteredTorrentsNumber("all", CATEGORIES_ALL, tag, TRACKERS_ALL) === 0)
tags.push(tag); tags.push(tag);
} }