diff --git a/src/webui/www/private/css/style.css b/src/webui/www/private/css/style.css index 668e48468..9e6947f7a 100644 --- a/src/webui/www/private/css/style.css +++ b/src/webui/www/private/css/style.css @@ -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; diff --git a/src/webui/www/private/newcategory.html b/src/webui/www/private/newcategory.html index 700b0d1af..224afa3ac 100644 --- a/src/webui/www/private/newcategory.html +++ b/src/webui/www/private/newcategory.html @@ -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; diff --git a/src/webui/www/private/scripts/client.js b/src/webui/www/private/scripts/client.js index eff713e9c..9a8df230e 100644 --- a/src/webui/www/private/scripts/client.js +++ b/src/webui/www/private/scripts/client.js @@ -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)); }; diff --git a/src/webui/www/private/scripts/mocha-init.js b/src/webui/www/private/scripts/mocha-init.js index c08ceaded..7e3c68fcb 100644 --- a/src/webui/www/private/scripts/mocha-init.js +++ b/src/webui/www/private/scripts/mocha-init.js @@ -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) { diff --git a/src/webui/www/private/views/filters.html b/src/webui/www/private/views/filters.html index 632881cb9..dab2df3d9 100644 --- a/src/webui/www/private/views/filters.html +++ b/src/webui/www/private/views/filters.html @@ -44,7 +44,9 @@ @@ -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);