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%;
}
#Filters {
overflow-x: hidden !important; /* override for default mocha inline style */
}
#Filters ul {
list-style-type: none;
}
#Filters ul li {
margin-left: -16px;
}
#Filters ul img {
height: 16px;
padding: 0 4px;
vertical-align: middle;
width: 16px;
}
.selectedFilter {
background-color: var(--color-background-blue) !important;
color: var(--color-text-white) !important;
}
#properties {
background-color: var(--color-background-default);
}
@ -514,57 +507,101 @@ div.formRow {
}
.filterTitle {
display: block;
box-sizing: border-box;
cursor: pointer;
display: flex;
font-weight: bold;
gap: 4px;
overflow: hidden;
padding-left: 5px;
padding-top: 5px;
padding: 4px 0 4px 6px;
text-overflow: ellipsis;
text-transform: uppercase;
white-space: nowrap;
}
.filterTitle img {
box-sizing: border-box;
height: 16px;
margin-bottom: -3px;
padding: 0 5px;
padding: 2px;
width: 16px;
}
.collapsedCategory > ul {
display: none;
}
.collapsedCategory .categoryToggle,
.filterTitle img.rotate {
transform: rotate(270deg);
transform: rotate(-90deg);
}
ul.filterList * {
box-sizing: border-box;
}
ul.filterList {
margin: 0 0 0 16px;
margin: 0;
padding-left: 0;
}
ul.filterList li:hover img,
ul.filterList .selectedFilter img {
ul.filterList span.link:hover :is(img, button),
ul.filterList .selectedFilter > .link :is(img, button) {
filter: var(--color-icon-hover);
}
ul.filterList span.link {
align-items: center;
color: inherit;
cursor: pointer;
display: flex;
gap: 5px;
overflow: hidden;
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;
white-space: nowrap;
}
ul.filterList li {
color: var(--color-text-default);
span.link :is(img, button) {
flex-shrink: 0;
}
ul.filterList li:hover {
background-color: var(--color-background-hover);
.selectedFilter > span.link {
background-color: var(--color-background-blue);
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 {
text-align: right;
vertical-align: top;

View file

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

View file

@ -175,15 +175,14 @@ window.addEventListener("DOMContentLoaded", () => {
MochaUI.Desktop.initialize();
const buildTransfersTab = function() {
const filt_w = Number(LocalPreferences.get("filters_width", 120));
new MochaUI.Column({
id: "filtersColumn",
placement: "left",
onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
saveColumnSizes();
}),
width: filt_w,
resizeLimit: [1, 300]
width: Number(LocalPreferences.get("filters_width", 210)),
resizeLimit: [1, 1000]
});
new MochaUI.Column({
id: "mainColumn",
@ -443,33 +442,47 @@ window.addEventListener("DOMContentLoaded", () => {
};
const updateCategoryList = function() {
const categoryList = $("categoryFilterList");
const categoryList = document.getElementById("categoryFilterList");
if (!categoryList)
return;
categoryList.getChildren().each(c => c.destroy());
const categoryItemTemplate = document.getElementById("categoryFilterItem");
const create_link = function(hash, text, 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 createCategoryLink = (hash, name, count) => {
const categoryFilterItem = categoryItemTemplate.content.cloneNode(true).firstElementChild;
categoryFilterItem.id = hash;
categoryFilterItem.classList.toggle("selectedFilter", hash === selectedCategory);
const span = categoryFilterItem.firstElementChild;
span.style.marginLeft = `${margin_left}px`;
span.lastChild.textContent = `${display_name} (${count})`;
span.lastElementChild.textContent = `${name} (${count})`;
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;
let uncategorized = 0;
for (const key in torrentsTable.rows) {
@ -480,18 +493,22 @@ window.addEventListener("DOMContentLoaded", () => {
if (row["full_data"].category.length === 0)
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 = [];
category_list.forEach((category, hash) => sortedCategories.push({
categoryName: category.name,
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) => {
const leftSegments = left.categoryName.split("/");
const rightSegments = right.categoryName.split("/");
const leftSegments = left.nameSegments;
const rightSegments = right.nameSegments;
for (let i = 0, iMax = Math.min(leftSegments.length, rightSegments.length); i < iMax; ++i) {
const compareResult = window.qBittorrent.Misc.naturalSortCollator.compare(
@ -503,19 +520,39 @@ window.addEventListener("DOMContentLoaded", () => {
return leftSegments.length - rightSegments.length;
});
for (let i = 0; i < sortedCategories.length; ++i) {
const { categoryName, categoryHash } = sortedCategories[i];
let { categoryCount } = sortedCategories[i];
const categoriesFragment = new DocumentFragment();
categoriesFragment.appendChild(createCategoryLink(CATEGORIES_ALL, "QBT_TR(All)QBT_TR[CONTEXT=CategoryFilterModel]", all));
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);
((j < sortedCategories.length) && sortedCategories[j].categoryName.startsWith(categoryName + "/")); ++j)
categoryCount += sortedCategories[j].categoryCount;
}
((j < sortedCategories.length) && sortedCategories[j].categoryName.startsWith(`${category.categoryName}/`)); ++j) {
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();
};
@ -524,7 +561,7 @@ window.addEventListener("DOMContentLoaded", () => {
if (!categoryList)
return;
for (const category of categoryList.children)
for (const category of categoryList.getElementsByTagName("li"))
category.classList.toggle("selectedFilter", (Number(category.id) === selectedCategory));
};

View file

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

View file

@ -44,7 +44,9 @@
<template id="categoryFilterItem">
<li class="categoriesFilterContextMenuTarget">
<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>
</li>
</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({
targets: ".categoriesFilterContextMenuTarget",
menu: "categoriesFilterMenu",
@ -183,11 +190,9 @@
document.getElementById("Filters_pad").addEventListener("click", (event) => {
const filterItem = event.target.closest("li");
if (!filterItem)
if (filterItem?.classList?.contains("selectedFilter"))
return;
event.stopImmediatePropagation();
const { id: filterItemID } = filterItem;
const { id: filterListID } = filterItem.closest("ul[id]");
switch (filterListID) {
@ -218,6 +223,22 @@
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();
})();
Object.freeze(window.qBittorrent.Filters);