mirror of
https://github.com/qbittorrent/qBittorrent.git
synced 2024-10-22 10:46:04 +03:00
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:
parent
f00c5c9fa3
commit
1b53fdf9ee
5 changed files with 179 additions and 72 deletions
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue