Improve WebUI responsiveness

This migrates Category and Tag to `Map` type from `Object` type. And done some algorithm and data structure optimization.

PR #20297.
This commit is contained in:
Chocobo1 2024-01-27 22:04:39 +08:00 committed by GitHub
commit 9bfb447dd3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 218 additions and 230 deletions

View file

@ -46,18 +46,18 @@ let clipboardEvent;
const CATEGORIES_ALL = 1; const CATEGORIES_ALL = 1;
const CATEGORIES_UNCATEGORIZED = 2; const CATEGORIES_UNCATEGORIZED = 2;
let category_list = {}; const category_list = new Map();
let selected_category = CATEGORIES_ALL; let selected_category = Number(LocalPreferences.get('selected_category', CATEGORIES_ALL));
let setCategoryFilter = function() {}; let setCategoryFilter = function() {};
/* Tags filter */ /* Tags filter */
const TAGS_ALL = 1; const TAGS_ALL = 1;
const TAGS_UNTAGGED = 2; const TAGS_UNTAGGED = 2;
let tagList = {}; const tagList = new Map();
let selectedTag = TAGS_ALL; let selectedTag = Number(LocalPreferences.get('selected_tag', TAGS_ALL));
let setTagFilter = function() {}; let setTagFilter = function() {};
/* Trackers filter */ /* Trackers filter */
@ -74,16 +74,6 @@ let selected_filter = LocalPreferences.get('selected_filter', 'all');
let setFilter = function() {}; let setFilter = function() {};
let toggleFilterDisplay = function() {}; let toggleFilterDisplay = function() {};
const loadSelectedCategory = function() {
selected_category = LocalPreferences.get('selected_category', CATEGORIES_ALL);
};
loadSelectedCategory();
const loadSelectedTag = function() {
selectedTag = LocalPreferences.get('selected_tag', TAGS_ALL);
};
loadSelectedTag();
const loadSelectedTracker = function() { const loadSelectedTracker = function() {
selectedTracker = LocalPreferences.get('selected_tracker', TRACKERS_ALL); selectedTracker = LocalPreferences.get('selected_tracker', TRACKERS_ALL);
}; };
@ -244,7 +234,7 @@ window.addEvent('load', function() {
}; };
setTagFilter = function(hash) { setTagFilter = function(hash) {
selectedTag = hash.toString(); selectedTag = hash;
LocalPreferences.set('selected_tag', selectedTag); LocalPreferences.set('selected_tag', selectedTag);
highlightSelectedTag(); highlightSelectedTag();
if (torrentsTable.tableBody !== undefined) if (torrentsTable.tableBody !== undefined)
@ -358,14 +348,11 @@ window.addEvent('load', function() {
return false; return false;
let removed = false; let removed = false;
for (const key in category_list) { category_list.forEach((category) => {
if (!Object.hasOwn(category_list, key))
continue;
const category = category_list[key];
const deleteResult = category.torrents.delete(hash); const deleteResult = category.torrents.delete(hash);
removed ||= deleteResult; removed ||= deleteResult;
} });
return removed; return removed;
}; };
@ -379,16 +366,19 @@ window.addEvent('load', function() {
removeTorrentFromCategoryList(hash); removeTorrentFromCategoryList(hash);
return true; return true;
} }
const categoryHash = genHash(category); const categoryHash = genHash(category);
if (!category_list[categoryHash]) { // This should not happen if (!category_list.has(categoryHash)) { // This should not happen
category_list[categoryHash] = { category_list.set(categoryHash, {
name: category, name: category,
torrents: [] torrents: new Set()
}; });
} }
if (!category_list[categoryHash].torrents.has(hash)) {
const torrents = category_list.get(categoryHash).torrents;
if (!torrents.has(hash)) {
removeTorrentFromCategoryList(hash); removeTorrentFromCategoryList(hash);
category_list[categoryHash].torrents.add(hash); torrents.add(hash);
return true; return true;
} }
return false; return false;
@ -399,14 +389,11 @@ window.addEvent('load', function() {
return false; return false;
let removed = false; let removed = false;
for (const key in tagList) { tagList.forEach((tag) => {
if (!Object.hasOwn(tagList, key))
continue;
const tag = tagList[key];
const deleteResult = tag.torrents.delete(hash); const deleteResult = tag.torrents.delete(hash);
removed ||= deleteResult; removed ||= deleteResult;
} });
return removed; return removed;
}; };
@ -424,14 +411,16 @@ window.addEvent('load', function() {
let added = false; let added = false;
for (let i = 0; i < tags.length; ++i) { for (let i = 0; i < tags.length; ++i) {
const tagHash = genHash(tags[i].trim()); const tagHash = genHash(tags[i].trim());
if (!tagList[tagHash]) { // This should not happen if (!tagList.has(tagHash)) { // This should not happen
tagList[tagHash] = { tagList.set(tagHash, {
name: tags, name: tags,
torrents: new Set() torrents: new Set()
}; });
} }
if (!tagList[tagHash].torrents.has(hash)) {
tagList[tagHash].torrents.add(hash); const torrents = tagList.get(tagHash).torrents;
if (!torrents.has(hash)) {
torrents.add(hash);
added = true; added = true;
} }
} }
@ -474,7 +463,7 @@ window.addEvent('load', function() {
margin_left = (category_path.length - 1) * 20; margin_left = (category_path.length - 1) * 20;
} }
const html = '<a href="#" style="margin-left: ' + margin_left + 'px" onclick="setCategoryFilter(' + hash + ');return false;">' const html = `<a href="#" style="margin-left: ${margin_left}px;" onclick="setCategoryFilter(${hash}); return false;">`
+ '<img src="images/view-categories.svg"/>' + '<img src="images/view-categories.svg"/>'
+ window.qBittorrent.Misc.escapeHtml(display_name) + ' (' + count + ')' + '</a>'; + window.qBittorrent.Misc.escapeHtml(display_name) + ' (' + count + ')' + '</a>';
const el = new Element('li', { const el = new Element('li', {
@ -487,20 +476,26 @@ window.addEvent('load', function() {
const all = torrentsTable.getRowIds().length; const all = torrentsTable.getRowIds().length;
let uncategorized = 0; let uncategorized = 0;
Object.each(torrentsTable.rows, function(row) { for (const key in torrentsTable.rows) {
if (!Object.hasOwn(torrentsTable.rows, key))
continue;
const row = torrentsTable.rows[key];
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_ALL, 'QBT_TR(All)QBT_TR[CONTEXT=CategoryFilterModel]', all));
categoryList.appendChild(create_link(CATEGORIES_UNCATEGORIZED, 'QBT_TR(Uncategorized)QBT_TR[CONTEXT=CategoryFilterModel]', uncategorized)); categoryList.appendChild(create_link(CATEGORIES_UNCATEGORIZED, 'QBT_TR(Uncategorized)QBT_TR[CONTEXT=CategoryFilterModel]', uncategorized));
const sortedCategories = []; const sortedCategories = [];
Object.each(category_list, function(category) { category_list.forEach((category, hash) => sortedCategories.push({
sortedCategories.push(category.name); categoryName: category.name,
}); categoryHash: hash,
sortedCategories.sort((leftCategory, rightCategory) => { categoryCount: category.torrents.size
const leftSegments = leftCategory.split('/'); }));
const rightSegments = rightCategory.split('/'); sortedCategories.sort((left, right) => {
const leftSegments = left.categoryName.split('/');
const rightSegments = right.categoryName.split('/');
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(
@ -513,15 +508,13 @@ window.addEvent('load', function() {
}); });
for (let i = 0; i < sortedCategories.length; ++i) { for (let i = 0; i < sortedCategories.length; ++i) {
const categoryName = sortedCategories[i]; const { categoryName, categoryHash } = sortedCategories[i];
const categoryHash = genHash(categoryName); let { categoryCount } = sortedCategories[i];
let categoryCount = category_list[categoryHash].torrents.size;
if (useSubcategories) { if (useSubcategories) {
for (let j = (i + 1); for (let j = (i + 1);
(j < sortedCategories.length) && sortedCategories[j].startsWith(categoryName + "/"); ++j) { ((j < sortedCategories.length) && sortedCategories[j].categoryName.startsWith(categoryName + "/")); ++j) {
const hash = genHash(sortedCategories[j]); categoryCount += sortedCategories[j].categoryCount;
categoryCount += category_list[hash].torrents.size;
} }
} }
@ -537,7 +530,7 @@ window.addEvent('load', function() {
return; return;
const children = categoryList.childNodes; const children = categoryList.childNodes;
for (let i = 0; i < children.length; ++i) { for (let i = 0; i < children.length; ++i) {
if (children[i].id == selected_category) if (Number(children[i].id) === selected_category)
children[i].className = "selectedFilter"; children[i].className = "selectedFilter";
else else
children[i].className = ""; children[i].className = "";
@ -552,7 +545,7 @@ window.addEvent('load', function() {
tagFilterList.getChildren().each(c => c.destroy()); tagFilterList.getChildren().each(c => c.destroy());
const createLink = function(hash, text, count) { const createLink = function(hash, text, count) {
const html = '<a href="#" onclick="setTagFilter(' + hash + ');return false;">' const html = `<a href="#" onclick="setTagFilter(${hash}); return false;">`
+ '<img src="images/tags.svg"/>' + '<img src="images/tags.svg"/>'
+ window.qBittorrent.Misc.escapeHtml(text) + ' (' + count + ')' + '</a>'; + window.qBittorrent.Misc.escapeHtml(text) + ' (' + count + ')' + '</a>';
const el = new Element('li', { const el = new Element('li', {
@ -573,16 +566,15 @@ window.addEvent('load', function() {
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 key in tagList) tagList.forEach((tag, hash) => sortedTags.push({
sortedTags.push(tagList[key].name); tagName: tag.name,
sortedTags.sort(window.qBittorrent.Misc.naturalSortCollator.compare); tagHash: hash,
tagSize: tag.torrents.size
}));
sortedTags.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.tagName, right.tagName));
for (let i = 0; i < sortedTags.length; ++i) { for (const { tagName, tagHash, tagSize } of sortedTags)
const tagName = sortedTags[i]; tagFilterList.appendChild(createLink(tagHash, tagName, tagSize));
const tagHash = genHash(tagName);
const tagCount = tagList[tagHash].torrents.size;
tagFilterList.appendChild(createLink(tagHash, tagName, tagCount));
}
highlightSelectedTag(); highlightSelectedTag();
}; };
@ -594,7 +586,7 @@ window.addEvent('load', function() {
const children = tagFilterList.childNodes; const children = tagFilterList.childNodes;
for (let i = 0; i < children.length; ++i) for (let i = 0; i < children.length; ++i)
children[i].className = (children[i].id === selectedTag) ? "selectedFilter" : ""; children[i].className = (Number(children[i].id) === selectedTag) ? "selectedFilter" : "";
}; };
const updateTrackerList = function() { const updateTrackerList = function() {
@ -626,14 +618,15 @@ window.addEvent('load', function() {
trackerFilterList.appendChild(createLink(TRACKERS_TRACKERLESS, 'QBT_TR(Trackerless (%1))QBT_TR[CONTEXT=TrackerFiltersList]', trackerlessTorrentsCount)); trackerFilterList.appendChild(createLink(TRACKERS_TRACKERLESS, 'QBT_TR(Trackerless (%1))QBT_TR[CONTEXT=TrackerFiltersList]', trackerlessTorrentsCount));
// Sort trackers by hostname // Sort trackers by hostname
const sortedList = [...trackerList.entries()].sort((left, right) => { const sortedList = [];
const leftHost = getHost(left[1].url); trackerList.forEach((tracker, hash) => sortedList.push({
const rightHost = getHost(right[1].url); trackerHost: getHost(tracker.url),
return window.qBittorrent.Misc.naturalSortCollator.compare(leftHost, rightHost); trackerHash: hash,
}); trackerCount: tracker.torrents.length
for (const [hash, tracker] of sortedList) { }));
trackerFilterList.appendChild(createLink(hash, (getHost(tracker.url) + ' (%1)'), tracker.torrents.length)); sortedList.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.trackerHost, right.trackerHost));
} for (const { trackerHost, trackerHash, trackerCount } of sortedList)
trackerFilterList.appendChild(createLink(trackerHash, (trackerHost + ' (%1)'), trackerCount));
highlightSelectedTracker(); highlightSelectedTracker();
}; };
@ -675,26 +668,30 @@ window.addEvent('load', function() {
if (full_update) { if (full_update) {
torrentsTableSelectedRows = torrentsTable.selectedRowsIds(); torrentsTableSelectedRows = torrentsTable.selectedRowsIds();
torrentsTable.clear(); torrentsTable.clear();
category_list = {}; category_list.clear();
tagList = {}; tagList.clear();
} }
if (response['rid']) { if (response['rid']) {
syncMainDataLastResponseId = response['rid']; syncMainDataLastResponseId = response['rid'];
} }
if (response['categories']) { if (response['categories']) {
for (const key in response['categories']) { for (const key in response['categories']) {
const category = response['categories'][key]; if (!Object.hasOwn(response['categories'], key))
continue;
const responseCategory = response['categories'][key];
const categoryHash = genHash(key); const categoryHash = genHash(key);
if (category_list[categoryHash] !== undefined) { const category = category_list.get(categoryHash);
if (category !== undefined) {
// only the save path can change for existing categories // only the save path can change for existing categories
category_list[categoryHash].savePath = category.savePath; category.savePath = responseCategory.savePath;
} }
else { else {
category_list[categoryHash] = { category_list.set(categoryHash, {
name: category.name, name: responseCategory.name,
savePath: category.savePath, savePath: responseCategory.savePath,
torrents: new Set() torrents: new Set()
}; });
} }
} }
update_categories = true; update_categories = true;
@ -702,18 +699,18 @@ window.addEvent('load', function() {
if (response['categories_removed']) { if (response['categories_removed']) {
response['categories_removed'].each(function(category) { response['categories_removed'].each(function(category) {
const categoryHash = genHash(category); const categoryHash = genHash(category);
delete category_list[categoryHash]; category_list.delete(categoryHash);
}); });
update_categories = true; update_categories = true;
} }
if (response['tags']) { if (response['tags']) {
for (const tag of response['tags']) { for (const tag of response['tags']) {
const tagHash = genHash(tag); const tagHash = genHash(tag);
if (!tagList[tagHash]) { if (!tagList.has(tagHash)) {
tagList[tagHash] = { tagList.set(tagHash, {
name: tag, name: tag,
torrents: new Set() torrents: new Set()
}; });
} }
} }
updateTags = true; updateTags = true;
@ -721,7 +718,7 @@ window.addEvent('load', function() {
if (response['tags_removed']) { if (response['tags_removed']) {
for (let i = 0; i < response['tags_removed'].length; ++i) { for (let i = 0; i < response['tags_removed'].length; ++i) {
const tagHash = genHash(response['tags_removed'][i]); const tagHash = genHash(response['tags_removed'][i]);
delete tagList[tagHash]; tagList.delete(tagHash);
} }
updateTags = true; updateTags = true;
} }

View file

@ -311,10 +311,10 @@ window.qBittorrent.ContextMenu = (function() {
let all_are_super_seeding = true; let all_are_super_seeding = true;
let all_are_auto_tmm = true; let all_are_auto_tmm = true;
let there_are_auto_tmm = false; let there_are_auto_tmm = false;
const tagsSelectionState = Object.clone(tagList); const tagCount = new Map();
const h = torrentsTable.selectedRowsIds(); const selectedRows = torrentsTable.selectedRowsIds();
h.each(function(item, index) { selectedRows.forEach((item, index) => {
const data = torrentsTable.rows.get(item).full_data; const data = torrentsTable.rows.get(item).full_data;
if (data['seq_dl'] !== true) if (data['seq_dl'] !== true)
@ -348,23 +348,15 @@ window.qBittorrent.ContextMenu = (function() {
all_are_auto_tmm = false; all_are_auto_tmm = false;
const torrentTags = data['tags'].split(', '); const torrentTags = data['tags'].split(', ');
for (const key in tagsSelectionState) { for (const tag of torrentTags) {
const tag = tagsSelectionState[key]; const count = tagCount.get(tag);
const tagExists = torrentTags.contains(tag.name); tagCount.set(tag, ((count !== undefined) ? (count + 1) : 1));
if ((tag.checked !== undefined) && (tag.checked != tagExists))
tag.indeterminate = true;
if (tag.checked === undefined)
tag.checked = tagExists;
else
tag.checked = tag.checked && tagExists;
} }
}); });
let show_seq_dl = true;
// hide renameFiles when more than 1 torrent is selected // hide renameFiles when more than 1 torrent is selected
if (h.length == 1) { if (selectedRows.length == 1) {
const data = torrentsTable.rows.get(h[0]).full_data; const data = torrentsTable.rows.get(selectedRows[0]).full_data;
let metadata_downloaded = !(data['state'] == 'metaDL' || data['state'] == 'forcedMetaDL' || data['total_size'] == -1); let metadata_downloaded = !(data['state'] == 'metaDL' || data['state'] == 'forcedMetaDL' || data['total_size'] == -1);
// hide renameFiles when metadata hasn't been downloaded yet // hide renameFiles when metadata hasn't been downloaded yet
@ -372,16 +364,9 @@ window.qBittorrent.ContextMenu = (function() {
? this.showItem('renameFiles') ? this.showItem('renameFiles')
: this.hideItem('renameFiles'); : this.hideItem('renameFiles');
} }
else else {
this.hideItem('renameFiles'); this.hideItem('renameFiles');
}
if (!all_are_seq_dl && there_are_seq_dl)
show_seq_dl = false;
let show_f_l_piece_prio = true;
if (!all_are_f_l_piece_prio && there_are_f_l_piece_prio)
show_f_l_piece_prio = false;
if (all_are_downloaded) { if (all_are_downloaded) {
this.hideItem('downloadLimit'); this.hideItem('downloadLimit');
@ -392,6 +377,9 @@ window.qBittorrent.ContextMenu = (function() {
this.setItemChecked('superSeeding', all_are_super_seeding); this.setItemChecked('superSeeding', all_are_super_seeding);
} }
else { else {
const show_seq_dl = (all_are_seq_dl || !there_are_seq_dl);
const show_f_l_piece_prio = (all_are_f_l_piece_prio || !there_are_f_l_piece_prio);
if (!show_seq_dl && show_f_l_piece_prio) if (!show_seq_dl && show_f_l_piece_prio)
this.menu.getElement('a[href$=firstLastPiecePrio]').parentNode.addClass('separator'); this.menu.getElement('a[href$=firstLastPiecePrio]').parentNode.addClass('separator');
else else
@ -434,42 +422,45 @@ window.qBittorrent.ContextMenu = (function() {
} }
const contextTagList = $('contextTagList'); const contextTagList = $('contextTagList');
for (const tagHash in tagList) { tagList.forEach((tag, tagHash) => {
const checkbox = contextTagList.getElement('a[href=#Tag/' + tagHash + '] input[type=checkbox]'); const checkbox = contextTagList.getElement(`a[href="#Tag/${tagHash}"] input[type="checkbox"]`);
const checkboxState = tagsSelectionState[tagHash]; const count = tagCount.get(tag.name);
checkbox.indeterminate = checkboxState.indeterminate; const hasCount = (count !== undefined);
checkbox.checked = checkboxState.checked; const isLesser = (count < selectedRows.length);
} checkbox.indeterminate = (hasCount ? isLesser : false);
checkbox.checked = (hasCount ? !isLesser : false);
});
}, },
updateCategoriesSubMenu: function(category_list) { updateCategoriesSubMenu: function(categoryList) {
const categoryList = $('contextCategoryList'); const contextCategoryList = $('contextCategoryList');
categoryList.getChildren().each(c => c.destroy()); contextCategoryList.getChildren().each(c => c.destroy());
categoryList.appendChild(new Element('li', { contextCategoryList.appendChild(new Element('li', {
html: '<a href="javascript:torrentNewCategoryFN();"><img src="images/list-add.svg" alt="QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]"/> QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]</a>' html: '<a href="javascript:torrentNewCategoryFN();"><img src="images/list-add.svg" alt="QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]"/> QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]</a>'
})); }));
categoryList.appendChild(new Element('li', { contextCategoryList.appendChild(new Element('li', {
html: '<a href="javascript:torrentSetCategoryFN(0);"><img src="images/edit-clear.svg" alt="QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]"/> QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]</a>' html: '<a href="javascript:torrentSetCategoryFN(0);"><img src="images/edit-clear.svg" alt="QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]"/> QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]</a>'
})); }));
const sortedCategories = []; const sortedCategories = [];
Object.each(category_list, function(category) { categoryList.forEach((category, hash) => sortedCategories.push({
sortedCategories.push(category.name); categoryName: category.name,
}); categoryHash: hash
sortedCategories.sort(window.qBittorrent.Misc.naturalSortCollator.compare); }));
sortedCategories.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(
left.categoryName, right.categoryName));
let first = true; let first = true;
Object.each(sortedCategories, function(categoryName) { for (const { categoryName, categoryHash } of sortedCategories) {
const categoryHash = genHash(categoryName);
const el = new Element('li', { const el = new Element('li', {
html: '<a href="javascript:torrentSetCategoryFN(\'' + categoryHash + '\');"><img src="images/view-categories.svg"/> ' + window.qBittorrent.Misc.escapeHtml(categoryName) + '</a>' html: `<a href="javascript:torrentSetCategoryFN(${categoryHash});"><img src="images/view-categories.svg"/>${window.qBittorrent.Misc.escapeHtml(categoryName)}</a>`
}); });
if (first) { if (first) {
el.addClass('separator'); el.addClass('separator');
first = false; first = false;
} }
categoryList.appendChild(el); contextCategoryList.appendChild(el);
}); }
}, },
updateTagsSubMenu: function(tagList) { updateTagsSubMenu: function(tagList) {
@ -491,15 +482,16 @@ window.qBittorrent.ContextMenu = (function() {
})); }));
const sortedTags = []; const sortedTags = [];
for (const key in tagList) tagList.forEach((tag, hash) => sortedTags.push({
sortedTags.push(tagList[key].name); tagName: tag.name,
sortedTags.sort(window.qBittorrent.Misc.naturalSortCollator.compare); tagHash: hash
}));
sortedTags.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.tagName, right.tagName));
for (let i = 0; i < sortedTags.length; ++i) { for (let i = 0; i < sortedTags.length; ++i) {
const tagName = sortedTags[i]; const { tagName, tagHash } = sortedTags[i];
const tagHash = genHash(tagName);
const el = new Element('li', { const el = new Element('li', {
html: '<a href="#Tag/' + tagHash + '" onclick="event.preventDefault(); torrentSetTagsFN(\'' + tagHash + '\', !event.currentTarget.getElement(\'input[type=checkbox]\').checked);">' html: `<a href="#Tag/${tagHash}" onclick="event.preventDefault(); torrentSetTagsFN(${tagHash}, !event.currentTarget.getElement('input[type=checkbox]').checked);">`
+ '<input type="checkbox" onclick="this.checked = !this.checked;"> ' + window.qBittorrent.Misc.escapeHtml(tagName) + '<input type="checkbox" onclick="this.checked = !this.checked;"> ' + window.qBittorrent.Misc.escapeHtml(tagName)
+ '</a>' + '</a>'
}); });
@ -513,8 +505,8 @@ window.qBittorrent.ContextMenu = (function() {
const CategoriesFilterContextMenu = new Class({ const CategoriesFilterContextMenu = new Class({
Extends: ContextMenu, Extends: ContextMenu,
updateMenuItems: function() { updateMenuItems: function() {
const id = this.options.element.id; const id = Number(this.options.element.id);
if ((id != CATEGORIES_ALL) && (id != CATEGORIES_UNCATEGORIZED)) { if ((id !== CATEGORIES_ALL) && (id !== CATEGORIES_UNCATEGORIZED)) {
this.showItem('editCategory'); this.showItem('editCategory');
this.showItem('deleteCategory'); this.showItem('deleteCategory');
if (useSubcategories) { if (useSubcategories) {
@ -535,8 +527,8 @@ window.qBittorrent.ContextMenu = (function() {
const TagsFilterContextMenu = new Class({ const TagsFilterContextMenu = new Class({
Extends: ContextMenu, Extends: ContextMenu,
updateMenuItems: function() { updateMenuItems: function() {
const id = this.options.element.id; const id = Number(this.options.element.id);
if ((id !== TAGS_ALL.toString()) && (id !== TAGS_UNTAGGED.toString())) if ((id !== TAGS_ALL) && (id !== TAGS_UNTAGGED))
this.showItem('deleteTag'); this.showItem('deleteTag');
else else
this.hideItem('deleteTag'); this.hideItem('deleteTag');

View file

@ -1386,50 +1386,41 @@ window.qBittorrent.DynamicTable = (function() {
break; break;
} }
const categoryHashInt = parseInt(categoryHash); switch (categoryHash) {
if (!isNaN(categoryHashInt)) { case CATEGORIES_ALL:
switch (categoryHashInt) { break; // do nothing
case CATEGORIES_ALL: case CATEGORIES_UNCATEGORIZED:
break; // do nothing if (row['full_data'].category.length !== 0)
case CATEGORIES_UNCATEGORIZED: return false;
if (row['full_data'].category.length !== 0) break; // do nothing
default:
if (!useSubcategories) {
if (categoryHash !== genHash(row['full_data'].category))
return false; return false;
break; // do nothing }
default: else {
if (!useSubcategories) { const selectedCategoryName = category_list.get(categoryHash).name + "/";
if (categoryHashInt !== genHash(row['full_data'].category)) const torrentCategoryName = row['full_data'].category + "/";
return false; if (!torrentCategoryName.startsWith(selectedCategoryName))
} return false;
else { }
const selectedCategoryName = category_list[categoryHash].name + "/"; break;
const torrentCategoryName = row['full_data'].category + "/";
if (!torrentCategoryName.startsWith(selectedCategoryName))
return false;
}
}
} }
const tagHashInt = parseInt(tagHash); switch (tagHash) {
const isNumber = !isNaN(tagHashInt); case TAGS_ALL:
if (isNumber) { break; // do nothing
switch (tagHashInt) {
case TAGS_ALL:
break; // do nothing
case TAGS_UNTAGGED: case TAGS_UNTAGGED:
if (row['full_data'].tags.length !== 0) if (row['full_data'].tags.length !== 0)
return false; return false;
break; // do nothing break; // do nothing
default: { default: {
let rowTags = row['full_data'].tags.split(', '); const tagHashes = row['full_data'].tags.split(', ').map(tag => genHash(tag));
rowTags = rowTags.map(function(tag) { if (!tagHashes.contains(tagHash))
return genHash(tag); return false;
}); break;
if (!rowTags.contains(tagHashInt))
return false;
break;
}
} }
} }
@ -1460,9 +1451,10 @@ window.qBittorrent.DynamicTable = (function() {
let cnt = 0; let cnt = 0;
const rows = this.rows.getValues(); const rows = this.rows.getValues();
for (let i = 0; i < rows.length; ++i) for (let i = 0; i < rows.length; ++i) {
if (this.applyFilter(rows[i], filterName, categoryHash, tagHash, trackerHash, null)) if (this.applyFilter(rows[i], filterName, categoryHash, tagHash, trackerHash, null))
++cnt; ++cnt;
}
return cnt; return cnt;
}, },
@ -1470,9 +1462,10 @@ window.qBittorrent.DynamicTable = (function() {
const rowsHashes = []; const rowsHashes = [];
const rows = this.rows.getValues(); const rows = this.rows.getValues();
for (let i = 0; i < rows.length; ++i) for (let i = 0; i < rows.length; ++i) {
if (this.applyFilter(rows[i], filterName, categoryHash, tagHash, trackerHash, null)) if (this.applyFilter(rows[i], filterName, categoryHash, tagHash, trackerHash, null))
rowsHashes.push(rows[i]['rowId']); rowsHashes.push(rows[i]['rowId']);
}
return rowsHashes; return rowsHashes;
}, },

View file

@ -573,20 +573,21 @@ const initializeWindows = function() {
}; };
torrentSetCategoryFN = function(categoryHash) { torrentSetCategoryFN = function(categoryHash) {
let categoryName = '';
if (categoryHash != 0)
categoryName = category_list[categoryHash].name;
const hashes = torrentsTable.selectedRowsIds(); const hashes = torrentsTable.selectedRowsIds();
if (hashes.length) { if (hashes.length <= 0)
new Request({ return;
url: 'api/v2/torrents/setCategory',
method: 'post', const categoryName = category_list.has(categoryHash)
data: { ? category_list.get(categoryHash).name
hashes: hashes.join("|"), : '';
category: categoryName new Request({
} url: 'api/v2/torrents/setCategory',
}).send(); method: 'post',
} data: {
hashes: hashes.join("|"),
category: categoryName
}
}).send();
}; };
createCategoryFN = function() { createCategoryFN = function() {
@ -609,7 +610,7 @@ const initializeWindows = function() {
createSubcategoryFN = function(categoryHash) { createSubcategoryFN = function(categoryHash) {
const action = "createSubcategory"; const action = "createSubcategory";
const categoryName = category_list[categoryHash].name + "/"; const categoryName = category_list.get(categoryHash).name + "/";
new MochaUI.Window({ new MochaUI.Window({
id: 'newSubcategoryPage', id: 'newSubcategoryPage',
title: "QBT_TR(New Category)QBT_TR[CONTEXT=CategoryFilterWidget]", title: "QBT_TR(New Category)QBT_TR[CONTEXT=CategoryFilterWidget]",
@ -628,13 +629,12 @@ const initializeWindows = function() {
editCategoryFN = function(categoryHash) { editCategoryFN = function(categoryHash) {
const action = "edit"; const action = "edit";
const categoryName = category_list[categoryHash].name; const category = category_list.get(categoryHash);
const savePath = category_list[categoryHash].savePath;
new MochaUI.Window({ new MochaUI.Window({
id: 'editCategoryPage', id: 'editCategoryPage',
title: "QBT_TR(Edit Category)QBT_TR[CONTEXT=TransferListWidget]", title: "QBT_TR(Edit Category)QBT_TR[CONTEXT=TransferListWidget]",
loadMethod: 'iframe', loadMethod: 'iframe',
contentURL: new URI('newcategory.html').setData("action", action).setData("categoryName", categoryName).setData("savePath", savePath).toString(), contentURL: new URI('newcategory.html').setData("action", action).setData("categoryName", category.name).setData("savePath", category.savePath).toString(),
scrollbars: false, scrollbars: false,
resizable: true, resizable: true,
maximizable: false, maximizable: false,
@ -647,7 +647,7 @@ const initializeWindows = function() {
}; };
removeCategoryFN = function(categoryHash) { removeCategoryFN = function(categoryHash) {
const categoryName = category_list[categoryHash].name; const categoryName = category_list.get(categoryHash).name;
new Request({ new Request({
url: 'api/v2/torrents/removeCategories', url: 'api/v2/torrents/removeCategories',
method: 'post', method: 'post',
@ -660,10 +660,11 @@ const initializeWindows = function() {
deleteUnusedCategoriesFN = function() { deleteUnusedCategoriesFN = function() {
const categories = []; const categories = [];
for (const hash in category_list) { category_list.forEach((category, hash) => {
if (torrentsTable.getFilteredTorrentsNumber('all', hash, TAGS_ALL, TRACKERS_ALL) === 0) if (torrentsTable.getFilteredTorrentsNumber('all', hash, TAGS_ALL, TRACKERS_ALL) === 0)
categories.push(category_list[hash].name); categories.push(category.name);
} });
new Request({ new Request({
url: 'api/v2/torrents/removeCategories', url: 'api/v2/torrents/removeCategories',
method: 'post', method: 'post',
@ -742,18 +743,19 @@ const initializeWindows = function() {
}; };
torrentSetTagsFN = function(tagHash, isSet) { torrentSetTagsFN = function(tagHash, isSet) {
const tagName = ((tagHash === '0') ? '' : tagList[tagHash].name);
const hashes = torrentsTable.selectedRowsIds(); const hashes = torrentsTable.selectedRowsIds();
if (hashes.length) { if (hashes.length <= 0)
new Request({ return;
url: (isSet ? 'api/v2/torrents/addTags' : 'api/v2/torrents/removeTags'),
method: 'post', const tagName = tagList.has(tagHash) ? tagList.get(tagHash).name : '';
data: { new Request({
hashes: hashes.join("|"), url: (isSet ? 'api/v2/torrents/addTags' : 'api/v2/torrents/removeTags'),
tags: tagName, method: 'post',
} data: {
}).send(); hashes: hashes.join("|"),
} tags: tagName,
}
}).send();
}; };
torrentRemoveAllTagsFN = function() { torrentRemoveAllTagsFN = function() {
@ -788,7 +790,7 @@ const initializeWindows = function() {
}; };
removeTagFN = function(tagHash) { removeTagFN = function(tagHash) {
const tagName = tagList[tagHash].name; const tagName = tagList.get(tagHash).name;
new Request({ new Request({
url: 'api/v2/torrents/deleteTags', url: 'api/v2/torrents/deleteTags',
method: 'post', method: 'post',
@ -801,10 +803,10 @@ const initializeWindows = function() {
deleteUnusedTagsFN = function() { deleteUnusedTagsFN = function() {
const tags = []; const tags = [];
for (const hash in tagList) { tagList.forEach((tag, hash) => {
if (torrentsTable.getFilteredTorrentsNumber('all', CATEGORIES_ALL, hash, TRACKERS_ALL) === 0) if (torrentsTable.getFilteredTorrentsNumber('all', CATEGORIES_ALL, hash, TRACKERS_ALL) === 0)
tags.push(tagList[hash].name); tags.push(tag.name);
} });
new Request({ new Request({
url: 'api/v2/torrents/deleteTags', url: 'api/v2/torrents/deleteTags',
method: 'post', method: 'post',

View file

@ -65,25 +65,25 @@
createCategoryFN(); createCategoryFN();
}, },
createSubcategory: function(element, ref) { createSubcategory: function(element, ref) {
createSubcategoryFN(element.id); createSubcategoryFN(Number(element.id));
}, },
editCategory: function(element, ref) { editCategory: function(element, ref) {
editCategoryFN(element.id); editCategoryFN(Number(element.id));
}, },
deleteCategory: function(element, ref) { deleteCategory: function(element, ref) {
removeCategoryFN(element.id); removeCategoryFN(Number(element.id));
}, },
deleteUnusedCategories: function(element, ref) { deleteUnusedCategories: function(element, ref) {
deleteUnusedCategoriesFN(); deleteUnusedCategoriesFN();
}, },
startTorrentsByCategory: function(element, ref) { startTorrentsByCategory: function(element, ref) {
startTorrentsByCategoryFN(element.id); startTorrentsByCategoryFN(Number(element.id));
}, },
pauseTorrentsByCategory: function(element, ref) { pauseTorrentsByCategory: function(element, ref) {
pauseTorrentsByCategoryFN(element.id); pauseTorrentsByCategoryFN(Number(element.id));
}, },
deleteTorrentsByCategory: function(element, ref) { deleteTorrentsByCategory: function(element, ref) {
deleteTorrentsByCategoryFN(element.id); deleteTorrentsByCategoryFN(Number(element.id));
} }
}, },
offsets: { offsets: {
@ -103,19 +103,19 @@
createTagFN(); createTagFN();
}, },
deleteTag: function(element, ref) { deleteTag: function(element, ref) {
removeTagFN(element.id); removeTagFN(Number(element.id));
}, },
deleteUnusedTags: function(element, ref) { deleteUnusedTags: function(element, ref) {
deleteUnusedTagsFN(); deleteUnusedTagsFN();
}, },
startTorrentsByTag: function(element, ref) { startTorrentsByTag: function(element, ref) {
startTorrentsByTagFN(element.id); startTorrentsByTagFN(Number(element.id));
}, },
pauseTorrentsByTag: function(element, ref) { pauseTorrentsByTag: function(element, ref) {
pauseTorrentsByTagFN(element.id); pauseTorrentsByTagFN(Number(element.id));
}, },
deleteTorrentsByTag: function(element, ref) { deleteTorrentsByTag: function(element, ref) {
deleteTorrentsByTagFN(element.id); deleteTorrentsByTagFN(Number(element.id));
} }
}, },
offsets: { offsets: {

View file

@ -2043,7 +2043,11 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
updateExportDirFinEnabled(); updateExportDirFinEnabled();
// Automatically add torrents from // Automatically add torrents from
for (const [folder, folderType] of Object.entries(pref.scan_dirs)) { for (const folder in pref.scan_dirs) {
if (!Object.hasOwn(pref.scan_dirs, folder))
continue;
const folderType = pref.scan_dirs[folder];
let sel = ""; let sel = "";
let other = ""; let other = "";
if (typeof folderType === "number") { if (typeof folderType === "number") {