WebUI: Improve subcategories

Now they should fully match GUI behavior, please let me know if I missed something.
Still plenty of room to improve them further (e.g styling/CSS) but for now I wanted to keep the changes to the minimum.

Also included small tweaks to category context menu actions.

PR #21269.
This commit is contained in:
skomerko 2024-09-08 09:21:11 +02:00 committed by GitHub
parent f00c5c9fa3
commit 1b53fdf9ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 179 additions and 72 deletions

View file

@ -175,26 +175,19 @@ hr {
width: 90%; width: 90%;
} }
#Filters {
overflow-x: hidden !important; /* override for default mocha inline style */
}
#Filters ul { #Filters ul {
list-style-type: none; list-style-type: none;
} }
#Filters ul li {
margin-left: -16px;
}
#Filters ul img { #Filters ul img {
height: 16px; height: 16px;
padding: 0 4px;
vertical-align: middle;
width: 16px; width: 16px;
} }
.selectedFilter {
background-color: var(--color-background-blue) !important;
color: var(--color-text-white) !important;
}
#properties { #properties {
background-color: var(--color-background-default); background-color: var(--color-background-default);
} }
@ -514,57 +507,101 @@ div.formRow {
} }
.filterTitle { .filterTitle {
display: block; box-sizing: border-box;
cursor: pointer;
display: flex;
font-weight: bold; font-weight: bold;
gap: 4px;
overflow: hidden; overflow: hidden;
padding-left: 5px; padding: 4px 0 4px 6px;
padding-top: 5px;
text-overflow: ellipsis; text-overflow: ellipsis;
text-transform: uppercase; text-transform: uppercase;
white-space: nowrap; white-space: nowrap;
} }
.filterTitle img { .filterTitle img {
box-sizing: border-box;
height: 16px; height: 16px;
margin-bottom: -3px; padding: 2px;
padding: 0 5px;
width: 16px; width: 16px;
} }
.collapsedCategory > ul {
display: none;
}
.collapsedCategory .categoryToggle,
.filterTitle img.rotate { .filterTitle img.rotate {
transform: rotate(270deg); transform: rotate(-90deg);
}
ul.filterList * {
box-sizing: border-box;
} }
ul.filterList { ul.filterList {
margin: 0 0 0 16px; margin: 0;
padding-left: 0; padding-left: 0;
} }
ul.filterList li:hover img, ul.filterList span.link:hover :is(img, button),
ul.filterList .selectedFilter img { ul.filterList .selectedFilter > .link :is(img, button) {
filter: var(--color-icon-hover); filter: var(--color-icon-hover);
} }
ul.filterList span.link { ul.filterList span.link {
align-items: center;
color: inherit;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
gap: 5px;
overflow: hidden; overflow: hidden;
padding: 4px 6px; padding: 4px 6px;
white-space: nowrap;
}
ul.filterList span.link:hover {
background-color: var(--color-background-hover);
color: var(--color-text-white);
}
span.link :last-child {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
ul.filterList li { span.link :is(img, button) {
color: var(--color-text-default); flex-shrink: 0;
} }
ul.filterList li:hover { .selectedFilter > span.link {
background-color: var(--color-background-hover); background-color: var(--color-background-blue);
color: var(--color-text-white); color: var(--color-text-white);
} }
.subcategories,
.subcategories ul {
margin: 0;
padding: 0;
}
.subcategories .categoryToggle {
display: inline-block;
visibility: hidden;
}
.categoryToggle {
background: url("../images/go-down.svg") center center / 10px no-repeat
transparent;
border: none;
display: none;
height: 16px;
margin-right: -2px;
padding: 2px;
transition: transform 0.3s;
width: 16px;
}
td.generalLabel { td.generalLabel {
text-align: right; text-align: right;
vertical-align: top; vertical-align: top;

View file

@ -90,13 +90,17 @@
hashes: uriHashes, hashes: uriHashes,
category: categoryName category: categoryName
}, },
onComplete: function() { onSuccess: function() {
window.parent.updateMainData();
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
},
onFailure: function() {
alert("QBT_TR(Unable to set category)QBT_TR[CONTEXT=Category]");
} }
}).send(); }).send();
}, },
onFailure: function() { onFailure: function() {
alert("QBT_TR(Unable to create category)QBT_TR[CONTEXT=HttpServer] " + window.qBittorrent.Misc.escapeHtml(categoryName)); alert("QBT_TR(Unable to create category)QBT_TR[CONTEXT=Category] " + window.qBittorrent.Misc.escapeHtml(categoryName));
} }
}).send(); }).send();
break; break;
@ -112,8 +116,12 @@
category: categoryName, category: categoryName,
savePath: savePath savePath: savePath
}, },
onComplete: function() { onSuccess: function() {
window.parent.updateMainData();
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
},
onFailure: function() {
alert("QBT_TR(Unable to create category)QBT_TR[CONTEXT=Category]");
} }
}).send(); }).send();
break; break;
@ -125,8 +133,12 @@
category: uriCategoryName, // category name can't be changed category: uriCategoryName, // category name can't be changed
savePath: savePath savePath: savePath
}, },
onComplete: function() { onSuccess: function() {
window.parent.updateMainData();
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
},
onFailure: function() {
alert("QBT_TR(Unable to edit category)QBT_TR[CONTEXT=Category]");
} }
}).send(); }).send();
break; break;

View file

@ -175,15 +175,14 @@ window.addEventListener("DOMContentLoaded", () => {
MochaUI.Desktop.initialize(); MochaUI.Desktop.initialize();
const buildTransfersTab = function() { const buildTransfersTab = function() {
const filt_w = Number(LocalPreferences.get("filters_width", 120));
new MochaUI.Column({ new MochaUI.Column({
id: "filtersColumn", id: "filtersColumn",
placement: "left", placement: "left",
onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => { onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
saveColumnSizes(); saveColumnSizes();
}), }),
width: filt_w, width: Number(LocalPreferences.get("filters_width", 210)),
resizeLimit: [1, 300] resizeLimit: [1, 1000]
}); });
new MochaUI.Column({ new MochaUI.Column({
id: "mainColumn", id: "mainColumn",
@ -443,33 +442,47 @@ window.addEventListener("DOMContentLoaded", () => {
}; };
const updateCategoryList = function() { const updateCategoryList = function() {
const categoryList = $("categoryFilterList"); const categoryList = document.getElementById("categoryFilterList");
if (!categoryList) if (!categoryList)
return; return;
categoryList.getChildren().each(c => c.destroy()); categoryList.getChildren().each(c => c.destroy());
const categoryItemTemplate = document.getElementById("categoryFilterItem"); const categoryItemTemplate = document.getElementById("categoryFilterItem");
const create_link = function(hash, text, count) { const createCategoryLink = (hash, name, count) => {
let display_name = text;
let margin_left = 0;
if (useSubcategories) {
const category_path = text.split("/");
display_name = category_path[category_path.length - 1];
margin_left = (category_path.length - 1) * 20;
}
const categoryFilterItem = categoryItemTemplate.content.cloneNode(true).firstElementChild; const categoryFilterItem = categoryItemTemplate.content.cloneNode(true).firstElementChild;
categoryFilterItem.id = hash; categoryFilterItem.id = hash;
categoryFilterItem.classList.toggle("selectedFilter", hash === selectedCategory); categoryFilterItem.classList.toggle("selectedFilter", hash === selectedCategory);
const span = categoryFilterItem.firstElementChild; const span = categoryFilterItem.firstElementChild;
span.style.marginLeft = `${margin_left}px`; span.lastElementChild.textContent = `${name} (${count})`;
span.lastChild.textContent = `${display_name} (${count})`;
return categoryFilterItem; return categoryFilterItem;
}; };
const createCategoryTree = (category) => {
const stack = [{ parent: categoriesFragment, category: category }];
while (stack.length > 0) {
const { parent, category } = stack.pop();
const displayName = category.nameSegments.at(-1);
const listItem = createCategoryLink(category.categoryHash, displayName, category.categoryCount);
listItem.firstElementChild.style.paddingLeft = `${(category.nameSegments.length - 1) * 20 + 6}px`;
parent.appendChild(listItem);
if (category.children.length > 0) {
listItem.querySelector(".categoryToggle").style.visibility = "visible";
const unorderedList = document.createElement("ul");
listItem.appendChild(unorderedList);
for (const subcategory of category.children.reverse())
stack.push({ parent: unorderedList, category: subcategory });
}
const categoryLocalPref = `category_${category.categoryHash}_collapsed`;
const isCollapsed = !category.forceExpand && (LocalPreferences.get(categoryLocalPref, "false") === "true");
LocalPreferences.set(categoryLocalPref, listItem.classList.toggle("collapsedCategory", isCollapsed).toString());
}
};
const all = torrentsTable.getRowIds().length; const all = torrentsTable.getRowIds().length;
let uncategorized = 0; let uncategorized = 0;
for (const key in torrentsTable.rows) { for (const key in torrentsTable.rows) {
@ -480,18 +493,22 @@ window.addEventListener("DOMContentLoaded", () => {
if (row["full_data"].category.length === 0) if (row["full_data"].category.length === 0)
uncategorized += 1; uncategorized += 1;
} }
categoryList.appendChild(create_link(CATEGORIES_ALL, "QBT_TR(All)QBT_TR[CONTEXT=CategoryFilterModel]", all));
categoryList.appendChild(create_link(CATEGORIES_UNCATEGORIZED, "QBT_TR(Uncategorized)QBT_TR[CONTEXT=CategoryFilterModel]", uncategorized));
const sortedCategories = []; const sortedCategories = [];
category_list.forEach((category, hash) => sortedCategories.push({ category_list.forEach((category, hash) => sortedCategories.push({
categoryName: category.name, categoryName: category.name,
categoryHash: hash, categoryHash: hash,
categoryCount: category.torrents.size categoryCount: category.torrents.size,
nameSegments: category.name.split("/"),
...(useSubcategories && {
children: [],
parentID: null,
forceExpand: LocalPreferences.get(`category_${hash}_collapsed`) === null
})
})); }));
sortedCategories.sort((left, right) => { sortedCategories.sort((left, right) => {
const leftSegments = left.categoryName.split("/"); const leftSegments = left.nameSegments;
const rightSegments = right.categoryName.split("/"); const rightSegments = right.nameSegments;
for (let i = 0, iMax = Math.min(leftSegments.length, rightSegments.length); i < iMax; ++i) { for (let i = 0, iMax = Math.min(leftSegments.length, rightSegments.length); i < iMax; ++i) {
const compareResult = window.qBittorrent.Misc.naturalSortCollator.compare( const compareResult = window.qBittorrent.Misc.naturalSortCollator.compare(
@ -503,19 +520,39 @@ window.addEventListener("DOMContentLoaded", () => {
return leftSegments.length - rightSegments.length; return leftSegments.length - rightSegments.length;
}); });
for (let i = 0; i < sortedCategories.length; ++i) { const categoriesFragment = new DocumentFragment();
const { categoryName, categoryHash } = sortedCategories[i]; categoriesFragment.appendChild(createCategoryLink(CATEGORIES_ALL, "QBT_TR(All)QBT_TR[CONTEXT=CategoryFilterModel]", all));
let { categoryCount } = sortedCategories[i]; categoriesFragment.appendChild(createCategoryLink(CATEGORIES_UNCATEGORIZED, "QBT_TR(Uncategorized)QBT_TR[CONTEXT=CategoryFilterModel]", uncategorized));
if (useSubcategories) { if (useSubcategories) {
categoryList.classList.add("subcategories");
for (let i = 0; i < sortedCategories.length; ++i) {
const category = sortedCategories[i];
for (let j = (i + 1); for (let j = (i + 1);
((j < sortedCategories.length) && sortedCategories[j].categoryName.startsWith(categoryName + "/")); ++j) ((j < sortedCategories.length) && sortedCategories[j].categoryName.startsWith(`${category.categoryName}/`)); ++j) {
categoryCount += sortedCategories[j].categoryCount; const subcategory = sortedCategories[j];
} category.categoryCount += subcategory.categoryCount;
category.forceExpand ||= subcategory.forceExpand;
categoryList.appendChild(create_link(categoryHash, categoryName, categoryCount)); const isDirectSubcategory = (subcategory.nameSegments.length - category.nameSegments.length) === 1;
if (isDirectSubcategory) {
subcategory.parentID = category.categoryHash;
category.children.push(subcategory);
}
}
}
for (const category of sortedCategories) {
if (category.parentID === null)
createCategoryTree(category);
}
}
else {
categoryList.classList.remove("subcategories");
for (const { categoryHash, categoryName, categoryCount } of sortedCategories)
categoriesFragment.appendChild(createCategoryLink(categoryHash, categoryName, categoryCount));
} }
categoryList.appendChild(categoriesFragment);
window.qBittorrent.Filters.categoriesFilterContextMenu.searchAndAddTargets(); window.qBittorrent.Filters.categoriesFilterContextMenu.searchAndAddTargets();
}; };
@ -524,7 +561,7 @@ window.addEventListener("DOMContentLoaded", () => {
if (!categoryList) if (!categoryList)
return; return;
for (const category of categoryList.children) for (const category of categoryList.getElementsByTagName("li"))
category.classList.toggle("selectedFilter", (Number(category.id) === selectedCategory)); category.classList.toggle("selectedFilter", (Number(category.id) === selectedCategory));
}; };

View file

@ -597,10 +597,7 @@ const initializeWindows = function() {
paddingVertical: 0, paddingVertical: 0,
paddingHorizontal: 0, paddingHorizontal: 0,
width: 400, width: 400,
height: 150, height: 150
onCloseComplete: function() {
updateMainData();
}
}); });
} }
}; };
@ -642,7 +639,6 @@ const initializeWindows = function() {
width: 400, width: 400,
height: 150 height: 150
}); });
updateMainData();
}; };
createSubcategoryFN = function(categoryHash) { createSubcategoryFN = function(categoryHash) {
@ -662,7 +658,6 @@ const initializeWindows = function() {
width: 400, width: 400,
height: 150 height: 150
}); });
updateMainData();
}; };
editCategoryFN = function(categoryHash) { editCategoryFN = function(categoryHash) {
@ -682,7 +677,6 @@ const initializeWindows = function() {
width: 400, width: 400,
height: 150 height: 150
}); });
updateMainData();
}; };
removeCategoryFN = function(categoryHash) { removeCategoryFN = function(categoryHash) {
@ -692,9 +686,12 @@ const initializeWindows = function() {
method: "post", method: "post",
data: { data: {
categories: categoryName categories: categoryName
},
onSuccess: function() {
setCategoryFilter(CATEGORIES_ALL);
updateMainData();
} }
}).send(); }).send();
setCategoryFilter(CATEGORIES_ALL);
}; };
deleteUnusedCategoriesFN = function() { deleteUnusedCategoriesFN = function() {
@ -709,9 +706,12 @@ const initializeWindows = function() {
method: "post", method: "post",
data: { data: {
categories: categories.join("\n") categories: categories.join("\n")
},
onSuccess: function() {
setCategoryFilter(CATEGORIES_ALL);
updateMainData();
} }
}).send(); }).send();
setCategoryFilter(CATEGORIES_ALL);
}; };
startTorrentsByCategoryFN = function(categoryHash) { startTorrentsByCategoryFN = function(categoryHash) {

View file

@ -44,7 +44,9 @@
<template id="categoryFilterItem"> <template id="categoryFilterItem">
<li class="categoriesFilterContextMenuTarget"> <li class="categoriesFilterContextMenuTarget">
<span class="link"> <span class="link">
<img src="images/view-categories.svg" alt=""> <button class="categoryToggle" type="button" aria-label="QBT_TR(Collapse/expand category)QBT_TR[CONTEXT=TransferListFiltersWidget]"></button>
<img src="images/view-categories.svg" width="16" height="16" alt="">
<span></span>
</span> </span>
</li> </li>
</template> </template>
@ -76,6 +78,11 @@
}; };
}; };
const toggleCategoryDisplay = (filterItemID) => {
const filterItem = document.getElementById(filterItemID);
LocalPreferences.set(`category_${filterItemID}_collapsed`, filterItem.classList.toggle("collapsedCategory").toString());
};
const categoriesFilterContextMenu = new window.qBittorrent.ContextMenu.CategoriesFilterContextMenu({ const categoriesFilterContextMenu = new window.qBittorrent.ContextMenu.CategoriesFilterContextMenu({
targets: ".categoriesFilterContextMenuTarget", targets: ".categoriesFilterContextMenuTarget",
menu: "categoriesFilterMenu", menu: "categoriesFilterMenu",
@ -183,11 +190,9 @@
document.getElementById("Filters_pad").addEventListener("click", (event) => { document.getElementById("Filters_pad").addEventListener("click", (event) => {
const filterItem = event.target.closest("li"); const filterItem = event.target.closest("li");
if (!filterItem) if (filterItem?.classList?.contains("selectedFilter"))
return; return;
event.stopImmediatePropagation();
const { id: filterItemID } = filterItem; const { id: filterItemID } = filterItem;
const { id: filterListID } = filterItem.closest("ul[id]"); const { id: filterListID } = filterItem.closest("ul[id]");
switch (filterListID) { switch (filterListID) {
@ -218,6 +223,22 @@
toggleFilterDisplay(filterList); toggleFilterDisplay(filterList);
}); });
document.getElementById("categoryFilterList").addEventListener("click", (event) => {
if (event.target.className !== "categoryToggle")
return;
event.stopPropagation();
toggleCategoryDisplay(event.target.closest("li").id);
});
document.getElementById("categoryFilterList").addEventListener("dblclick", function(event) {
const filterItem = event.target.closest("li");
if (!this.classList.contains("subcategories") || !(filterItem?.querySelector("ul")))
return;
toggleCategoryDisplay(filterItem.id);
});
return exports(); return exports();
})(); })();
Object.freeze(window.qBittorrent.Filters); Object.freeze(window.qBittorrent.Filters);