WebUI: Add support for running concurrent searches

This PR adds support for running multiple concurrent searches in the Web UI. This is already supported in the GUI as well as by the Web API. Behavior mimics the GUI as closely as possible.

All filters and sorting are preserved per-tab, allowing you to apply unique filters and sorts to each of your searches. Row selection is also preserved across tab navigation.

Closes #12840.
PR #20593.
This commit is contained in:
Thomas Piccirello 2024-03-29 00:05:43 -07:00 committed by GitHub
parent f5cac13979
commit eb9e98a4b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 387 additions and 99 deletions

View file

@ -67,7 +67,7 @@ repos:
hooks: hooks:
- id: codespell - id: codespell
name: Check spelling (codespell) name: Check spelling (codespell)
args: ["--ignore-words-list", "additionals,curren,fo,ist,ket,superseeding,te,ths"] args: ["--ignore-words-list", "additionals,curren,fo,ist,ket,searchin,superseeding,te,ths"]
exclude: | exclude: |
(?x)^( (?x)^(
.*\.desktop | .*\.desktop |

View file

@ -667,9 +667,9 @@ td.statusBarSeparator {
} }
#searchResultsTableContainer { #searchResultsTableContainer {
-moz-height: calc(100% - 140px); -moz-height: calc(100% - 177px);
-webkit-height: calc(100% - 140px); -webkit-height: calc(100% - 177px);
height: calc(100% - 140px); height: calc(100% - 177px);
overflow: auto; overflow: auto;
} }

View file

@ -516,16 +516,20 @@ window.qBittorrent.DynamicTable = (function() {
return LocalPreferences.get('sorted_column_' + this.dynamicTableDivId); return LocalPreferences.get('sorted_column_' + this.dynamicTableDivId);
}, },
setSortedColumn: function(column) { /**
* @param {string} column name to sort by
* @param {string|null} reverse defaults to implementation-specific behavior when not specified. Should only be passed when restoring previous state.
*/
setSortedColumn: function(column, reverse = null) {
if (column != this.sortedColumn) { if (column != this.sortedColumn) {
const oldColumn = this.sortedColumn; const oldColumn = this.sortedColumn;
this.sortedColumn = column; this.sortedColumn = column;
this.reverseSort = '0'; this.reverseSort = reverse ?? '0';
this.setSortedColumnIcon(column, oldColumn, false); this.setSortedColumnIcon(column, oldColumn, false);
} }
else { else {
// Toggle sort order // Toggle sort order
this.reverseSort = this.reverseSort === '0' ? '1' : '0'; this.reverseSort = reverse ?? (this.reverseSort === '0' ? '1' : '0');
this.setSortedColumnIcon(column, null, (this.reverseSort === '1')); this.setSortedColumnIcon(column, null, (this.reverseSort === '1'));
} }
LocalPreferences.set('sorted_column_' + this.dynamicTableDivId, column); LocalPreferences.set('sorted_column_' + this.dynamicTableDivId, column);

View file

@ -18,14 +18,15 @@
width: 150px; width: 150px;
} }
#searchResultsNoPlugins { #searchResultsNoPlugins,
#searchResultsNoSearches {
height: calc(100% - 110px); height: calc(100% - 110px);
}
#searchResultsNoPlugins table { table {
height: 100%; height: 100%;
width: 100%; width: 100%;
text-align: center; text-align: center;
}
} }
#searchResultsFilters { #searchResultsFilters {
@ -75,9 +76,9 @@
</style> </style>
<div id="searchResults"> <div id="searchResults">
<div style="overflow: hidden; height: 70px;"> <div style="overflow: hidden; height: 60px;">
<div style="margin: 20px 0; height: 30px;"> <div style="margin: 20px 0 10px 0; height: 30px;">
<input type="text" id="searchPattern" class="searchInputField" placeholder="QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]" autocorrect="off" autocomplete="off" autocapitalize="none" /> <input type="text" id="searchPattern" class="searchInputField" placeholder="QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]" autocorrect="off" autocomplete="off" autocapitalize="none" oninput="qBittorrent.Search.onSearchPatternChanged()" />
<select id="categorySelect" class="searchInputField" onchange="qBittorrent.Search.categorySelected()"></select> <select id="categorySelect" class="searchInputField" onchange="qBittorrent.Search.categorySelected()"></select>
<select id="pluginsSelect" class="searchInputField" onchange="qBittorrent.Search.pluginSelected()"></select> <select id="pluginsSelect" class="searchInputField" onchange="qBittorrent.Search.pluginSelected()"></select>
<button type="button" id="startSearchButton" class="searchInputField" onclick="qBittorrent.Search.startStopSearch()">QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]</button> <button type="button" id="startSearchButton" class="searchInputField" onclick="qBittorrent.Search.startStopSearch()">QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]</button>
@ -99,7 +100,25 @@
<span></span> <span></span>
</div> </div>
<div id="searchResultsFilters"> <div id="searchResultsNoSearches" style="display: none">
<table>
<tbody>
<tr>
<td>
QBT_TR(Start a search above.)QBT_TR[CONTEXT=SearchEngineWidget]
</td>
</tr>
</tbody>
</table>
<span></span>
</div>
<div id="searchTabsToolbar" class="toolbarTabs" style="border-bottom: 1px solid var(--color-border-default); display: none">
<ul id="searchTabs" class="tab-menu"></ul>
<div class="clear"></div>
</div>
<div id="searchResultsFilters" style="padding-top: 10px; display: none">
<input type="text" id="searchInNameFilter" placeholder="QBT_TR(Filter)QBT_TR[CONTEXT=SearchEngineWidget]" autocorrect="off" autocapitalize="none" /> <input type="text" id="searchInNameFilter" placeholder="QBT_TR(Filter)QBT_TR[CONTEXT=SearchEngineWidget]" autocorrect="off" autocapitalize="none" />
<span>QBT_TR(Results)QBT_TR[CONTEXT=SearchEngineWidget] (QBT_TR(showing)QBT_TR[CONTEXT=SearchEngineWidget] <span id="numSearchResultsVisible" class="numSearchResults">0</span> QBT_TR(out of)QBT_TR[CONTEXT=SearchEngineWidget] <span id="numSearchResultsTotal" class="numSearchResults">0</span>):</span> <span>QBT_TR(Results)QBT_TR[CONTEXT=SearchEngineWidget] (QBT_TR(showing)QBT_TR[CONTEXT=SearchEngineWidget] <span id="numSearchResultsVisible" class="numSearchResults">0</span> QBT_TR(out of)QBT_TR[CONTEXT=SearchEngineWidget] <span id="numSearchResultsTotal" class="numSearchResults">0</span>):</span>
@ -135,8 +154,8 @@
<select id="searchMaxSizePrefix" onchange="qBittorrent.Search.searchSizeFilterPrefixChanged()"> <select id="searchMaxSizePrefix" onchange="qBittorrent.Search.searchSizeFilterPrefixChanged()">
<option value="0">QBT_TR(B)QBT_TR[CONTEXT=misc]</option> <option value="0">QBT_TR(B)QBT_TR[CONTEXT=misc]</option>
<option value="1">QBT_TR(KiB)QBT_TR[CONTEXT=misc]</option> <option value="1">QBT_TR(KiB)QBT_TR[CONTEXT=misc]</option>
<option value="2" selected>QBT_TR(MiB)QBT_TR[CONTEXT=misc]</option> <option value="2">QBT_TR(MiB)QBT_TR[CONTEXT=misc]</option>
<option value="3">QBT_TR(GiB)QBT_TR[CONTEXT=misc]</option> <option value="3" selected>QBT_TR(GiB)QBT_TR[CONTEXT=misc]</option>
<option value="4">QBT_TR(TiB)QBT_TR[CONTEXT=misc]</option> <option value="4">QBT_TR(TiB)QBT_TR[CONTEXT=misc]</option>
<option value="5">QBT_TR(PiB)QBT_TR[CONTEXT=misc]</option> <option value="5">QBT_TR(PiB)QBT_TR[CONTEXT=misc]</option>
<option value="6">QBT_TR(EiB)QBT_TR[CONTEXT=misc]</option> <option value="6">QBT_TR(EiB)QBT_TR[CONTEXT=misc]</option>
@ -145,7 +164,7 @@
</div> </div>
</div> </div>
<div id="searchResultsTableContainer"> <div id="searchResultsTableContainer" style="display: none">
<div id="searchResultsTableFixedHeaderDiv" class="dynamicTableFixedHeaderDiv"> <div id="searchResultsTableFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
<table class="dynamicTable unselectable" style="position:relative;"> <table class="dynamicTable unselectable" style="position:relative;">
<thead> <thead>
@ -200,22 +219,41 @@
init: init, init: init,
getPlugin: getPlugin, getPlugin: getPlugin,
searchInTorrentName: searchInTorrentName, searchInTorrentName: searchInTorrentName,
onSearchPatternChanged: onSearchPatternChanged,
categorySelected: categorySelected, categorySelected: categorySelected,
pluginSelected: pluginSelected, pluginSelected: pluginSelected,
searchSeedsFilterChanged: searchSeedsFilterChanged, searchSeedsFilterChanged: searchSeedsFilterChanged,
searchSizeFilterChanged: searchSizeFilterChanged, searchSizeFilterChanged: searchSizeFilterChanged,
searchSizeFilterPrefixChanged: searchSizeFilterPrefixChanged searchSizeFilterPrefixChanged: searchSizeFilterPrefixChanged,
closeSearchTab: closeSearchTab,
}; };
}; };
let searchResultsTable; const searchTabIdPrefix = "Search-";
let loadSearchResultsTimer;
let loadSearchPluginsTimer; let loadSearchPluginsTimer;
let searchResultsRowId = 0;
let searchRunning = false;
let requestCount = 0;
const searchPlugins = []; const searchPlugins = [];
let prevSearchPluginsResponse; let prevSearchPluginsResponse;
let selectedCategory = "QBT_TR(All categories)QBT_TR[CONTEXT=SearchEngineWidget]";
let selectedPlugin = "all";
let prevSelectedPlugin;
// whether the current search pattern differs from the pattern that the active search was performed with
let searchPatternChanged = false;
let searchResultsTable;
/** @type Map<number, {
* searchPattern: string,
* filterPattern: string,
* seedsFilter: {min: number, max: number},
* sizeFilter: {min: number, minUnit: number, max: number, maxUnit: number},
* searchIn: string,
* rows: [],
* rowId: number,
* selectedRowIds: number[],
* running: boolean,
* loadResultsTimer: Timer,
* sort: {column: string, reverse: string},
* }> **/
const searchState = new Map();
const searchText = { const searchText = {
pattern: "", pattern: "",
filterPattern: "" filterPattern: ""
@ -230,10 +268,6 @@
max: 0.00, max: 0.00,
maxUnit: 3 maxUnit: 3
}; };
let selectedCategory = "QBT_TR(All categories)QBT_TR[CONTEXT=SearchEngineWidget]";
let selectedPlugin = "all";
let prevSelectedPlugin;
let activeSearchId = null;
const init = function() { const init = function() {
// load "Search in" preference from local storage // load "Search in" preference from local storage
@ -290,13 +324,227 @@
}).activate(); }).activate();
}; };
const startSearch = function(pattern, category, plugins) { const numSearchTabs = function() {
clearTimeout(loadSearchResultsTimer); return $('searchTabs').getElements('li').length;
};
const getSearchIdFromTab = function(tab) {
return Number(tab.id.substring(searchTabIdPrefix.length));
};
const createSearchTab = function(searchId, pattern) {
const newTabId = `${searchTabIdPrefix}${searchId}`;
const tabElem = new Element('a', {
text: pattern,
});
const closeTabElem = new Element('img', {
alt: 'QBT_TR(Close tab)QBT_TR[CONTEXT=SearchWidget]',
title: 'QBT_TR(Close tab)QBT_TR[CONTEXT=SearchWidget]',
src: 'images/application-exit.svg',
width: '8',
height: '8',
style: 'padding-right: 7px; margin-bottom: -1px; margin-left: -5px',
onclick: 'qBittorrent.Search.closeSearchTab(this)',
});
closeTabElem.inject(tabElem, 'top');
tabElem.appendChild(getStatusIconElement('QBT_TR(Searching...)QBT_TR[CONTEXT=SearchJobWidget]', 'images/queued.svg'));
$('searchTabs').appendChild(new Element('li', {
id: newTabId,
class: 'selected',
html: tabElem.outerHTML,
}));
// unhide the results elements
if (numSearchTabs() >= 1) {
$('searchResultsNoSearches').style.display = "none";
$('searchResultsFilters').style.display = "block";
$('searchResultsTableContainer').style.display = "block";
$('searchTabsToolbar').style.display = "block";
}
// reinitialize tabs
$('searchTabs').getElements('li').removeEvents('click');
$('searchTabs').getElements('li').addEvent('click', function(e) {
$('startSearchButton').set('text', 'QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]');
setActiveTab(this);
});
// select new tab
setActiveTab($(newTabId));
searchResultsTable.clear(); searchResultsTable.clear();
resetFilters();
searchState.set(searchId, {
searchPattern: pattern,
filterPattern: searchText.filterPattern,
seedsFilter: { min: searchSeedsFilter.min, max: searchSeedsFilter.max },
sizeFilter: { min: searchSizeFilter.min, minUnit: searchSizeFilter.minUnit, max: searchSizeFilter.max, maxUnit: searchSizeFilter.maxUnit },
searchIn: getSearchInTorrentName(),
rows: [],
rowId: 0,
selectedRowIds: [],
running: true,
loadResultsTimer: null,
sort: { column: searchResultsTable.sortedColumn, reverse: searchResultsTable.reverseSort },
});
updateSearchResultsData(searchId);
};
const closeSearchTab = function(el) {
const tab = el.parentElement.parentElement;
const searchId = getSearchIdFromTab(tab);
const isTabSelected = tab.hasClass('selected');
const newTabToSelect = isTabSelected ? tab.nextSibling || tab.previousSibling : null;
const currentSearchId = getSelectedSearchId();
const state = searchState.get(currentSearchId);
// don't bother sending a stop request if already stopped
if (state && state.running) {
stopSearch(searchId);
}
tab.destroy();
if (numSearchTabs() === 0) {
resetSearchState();
resetFilters();
$('numSearchResultsVisible').set('html', 0);
$('numSearchResultsTotal').set('html', 0);
$('searchResultsNoSearches').style.display = "block";
$('searchResultsFilters').style.display = "none";
$('searchResultsTableContainer').style.display = "none";
$('searchTabsToolbar').style.display = "none";
}
else if (isTabSelected && newTabToSelect) {
setActiveTab(newTabToSelect);
$('startSearchButton').set('text', 'QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]');
}
};
const saveCurrentTabState = function() {
const currentSearchId = getSelectedSearchId();
if (!currentSearchId)
return;
const state = searchState.get(currentSearchId);
if (!state)
return;
state.filterPattern = searchText.filterPattern;
state.seedsFilter = {
min: searchSeedsFilter.min,
max: searchSeedsFilter.max,
};
state.sizeFilter = {
min: searchSizeFilter.min,
minUnit: searchSizeFilter.minUnit,
max: searchSizeFilter.max,
maxUnit: searchSizeFilter.maxUnit,
};
state.searchIn = getSearchInTorrentName();
state.sort = {
column: searchResultsTable.sortedColumn,
reverse: searchResultsTable.reverseSort,
};
// we must copy the array to avoid taking a reference to it
state.selectedRowIds = [...searchResultsTable.selectedRows];
};
const setActiveTab = function(tab) {
const searchId = getSearchIdFromTab(tab);
if (searchId === getSelectedSearchId())
return;
saveCurrentTabState();
MochaUI.selected(tab, 'searchTabs');
const state = searchState.get(searchId);
let rowsToSelect = [];
// restore table rows
searchResultsTable.clear();
if (state) {
for (const row of state.rows) {
searchResultsTable.updateRowData(row);
}
rowsToSelect = state.selectedRowIds;
// restore filters
searchText.pattern = state.searchPattern;
searchText.filterPattern = state.filterPattern;
$('searchInNameFilter').set("value", state.filterPattern);
searchSeedsFilter.min = state.seedsFilter.min;
searchSeedsFilter.max = state.seedsFilter.max;
$('searchMinSeedsFilter').set('value', state.seedsFilter.min);
$('searchMaxSeedsFilter').set('value', state.seedsFilter.max);
searchSizeFilter.min = state.sizeFilter.min;
searchSizeFilter.minUnit = state.sizeFilter.minUnit;
searchSizeFilter.max = state.sizeFilter.max;
searchSizeFilter.maxUnit = state.sizeFilter.maxUnit;
$('searchMinSizeFilter').set('value', state.sizeFilter.min);
$('searchMinSizePrefix').set('value', state.sizeFilter.minUnit);
$('searchMaxSizeFilter').set('value', state.sizeFilter.max);
$('searchMaxSizePrefix').set('value', state.sizeFilter.maxUnit);
const currentSearchPattern = $('searchPattern').getProperty('value').trim();
if (state.running && state.searchPattern === currentSearchPattern) {
// allow search to be stopped
$('startSearchButton').set('text', 'QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]');
searchPatternChanged = false;
}
searchResultsTable.setSortedColumn(state.sort.column, state.sort.reverse);
$('searchInTorrentName').set('value', state.searchIn);
}
// must restore all filters before calling updateTable
searchResultsTable.updateTable();
searchResultsTable.altRow();
// must reselect rows after calling updateTable
if (rowsToSelect.length > 0) {
searchResultsTable.reselectRows(rowsToSelect);
}
$('numSearchResultsVisible').set('html', searchResultsTable.getFilteredAndSortedRows().length); $('numSearchResultsVisible').set('html', searchResultsTable.getFilteredAndSortedRows().length);
$('numSearchResultsTotal').set('html', searchResultsTable.getRowIds().length); $('numSearchResultsTotal').set('html', searchResultsTable.getRowIds().length);
searchResultsRowId = 0;
requestCount = 0; setupSearchTableEvents(true);
};
const getStatusIconElement = function(text, image) {
return new Element('img', {
alt: text,
title: text,
src: image,
class: 'statusIcon',
width: '10',
height: '10',
style: 'margin-bottom: -2px; margin-left: 7px',
});
};
const updateStatusIconElement = function(searchId, text, image) {
const searchTab = $(`${searchTabIdPrefix}${searchId}`);
if (searchTab) {
const statusIcon = searchTab.getElement('.statusIcon');
statusIcon.set('alt', text);
statusIcon.set('title', text);
statusIcon.set('src', image);
}
};
const startSearch = function(pattern, category, plugins) {
searchPatternChanged = false;
const url = new URI('api/v2/search/start'); const url = new URI('api/v2/search/start');
new Request.JSON({ new Request.JSON({
@ -309,29 +557,38 @@
}, },
onSuccess: function(response) { onSuccess: function(response) {
$('startSearchButton').set('text', 'QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]'); $('startSearchButton').set('text', 'QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]');
searchRunning = true; const searchId = response.id;
activeSearchId = response.id; createSearchTab(searchId, pattern);
updateSearchResultsData();
} }
}).send(); }).send();
}; };
const stopSearch = function() { const stopSearch = function(searchId) {
const url = new URI('api/v2/search/stop'); const url = new URI('api/v2/search/stop');
new Request({ new Request({
url: url, url: url,
method: 'post', method: 'post',
data: { data: {
id: activeSearchId id: searchId
}, },
onSuccess: function(response) { onSuccess: function(response) {
resetSearchState(); resetSearchState(searchId);
// not strictly necessary to do this when the tab is being closed, but there's no harm in it
updateStatusIconElement(searchId, 'QBT_TR(Search aborted)QBT_TR[CONTEXT=SearchJobWidget]', 'images/task-reject.svg');
} }
}).send(); }).send();
}; };
const getSelectedSearchId = function() {
const selectedTab = $('searchTabs').getElement('li.selected');
return selectedTab ? getSearchIdFromTab(selectedTab) : null;
};
const startStopSearch = function() { const startStopSearch = function() {
if (!searchRunning || !activeSearchId) { const currentSearchId = getSelectedSearchId();
const state = searchState.get(currentSearchId);
const isSearchRunning = state && state.running;
if (!isSearchRunning || searchPatternChanged) {
const pattern = $('searchPattern').getProperty('value').trim(); const pattern = $('searchPattern').getProperty('value').trim();
let category = $('categorySelect').getProperty('value'); let category = $('categorySelect').getProperty('value');
const plugins = $('pluginsSelect').getProperty('value'); const plugins = $('pluginsSelect').getProperty('value');
@ -339,13 +596,11 @@
if (!pattern || !category || !plugins) if (!pattern || !category || !plugins)
return; return;
resetFilters();
searchText.pattern = pattern; searchText.pattern = pattern;
startSearch(pattern, category, plugins); startSearch(pattern, category, plugins);
} }
else { else {
stopSearch(); stopSearch(currentSearchId);
} }
}; };
@ -423,6 +678,21 @@
loadSearchPluginsTimer = loadSearchPlugins.delay(2000); loadSearchPluginsTimer = loadSearchPlugins.delay(2000);
}; };
const onSearchPatternChanged = function() {
const currentSearchId = getSelectedSearchId();
const state = searchState.get(currentSearchId);
const currentSearchPattern = $('searchPattern').getProperty('value').trim();
// start a new search if pattern has changed, otherwise allow the search to be stopped
if (state && state.searchPattern === currentSearchPattern) {
searchPatternChanged = false;
$('startSearchButton').set('text', 'QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]');
}
else {
searchPatternChanged = true;
$('startSearchButton').set('text', 'QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]');
}
};
const categorySelected = function() { const categorySelected = function() {
selectedCategory = $("categorySelect").get("value"); selectedCategory = $("categorySelect").get("value");
}; };
@ -452,12 +722,13 @@
pluginSelected(); pluginSelected();
}; };
const resetSearchState = function() { const resetSearchState = function(searchId) {
clearTimeout(loadSearchResultsTimer);
$('startSearchButton').set('text', 'QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]'); $('startSearchButton').set('text', 'QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]');
searchResultsRowId = 0; const state = searchState.get(searchId);
searchRunning = false; if (state) {
activeSearchId = null; state.running = false;
clearTimeout(state.loadResultsTimer);
}
}; };
const getSearchCategories = function() { const getSearchCategories = function() {
@ -526,15 +797,9 @@
pluginsHtml.push('<option value="all">QBT_TR(All plugins)QBT_TR[CONTEXT=SearchEngineWidget]</option>'); pluginsHtml.push('<option value="all">QBT_TR(All plugins)QBT_TR[CONTEXT=SearchEngineWidget]</option>');
const searchPluginsEmpty = (searchPlugins.length === 0); const searchPluginsEmpty = (searchPlugins.length === 0);
if (searchPluginsEmpty) { if (!searchPluginsEmpty) {
$('searchResultsNoPlugins').style.display = "block";
$('searchResultsFilters').style.display = "none";
$('searchResultsTableContainer').style.display = "none";
}
else {
$('searchResultsNoPlugins').style.display = "none"; $('searchResultsNoPlugins').style.display = "none";
$('searchResultsFilters').style.display = "block"; $('searchResultsNoSearches').style.display = "block";
$('searchResultsTableContainer').style.display = "block";
// sort plugins alphabetically // sort plugins alphabetically
const allPlugins = searchPlugins.sort((left, right) => { const allPlugins = searchPlugins.sort((left, right) => {
@ -576,23 +841,32 @@
return null; return null;
}; };
const searchInTorrentName = function() { const resetFilters = function() {
if ($('searchInTorrentName').get('value') === "names") searchText.filterPattern = '';
LocalPreferences.set('search_in_filter', "names"); $('searchInNameFilter').set('value', '');
else
LocalPreferences.set('search_in_filter', "everywhere");
searchFilterChanged(); searchSeedsFilter.min = 0;
searchSeedsFilter.max = 0;
$('searchMinSeedsFilter').set('value', searchSeedsFilter.min);
$('searchMaxSeedsFilter').set('value', searchSeedsFilter.max);
searchSizeFilter.min = 0.00;
searchSizeFilter.minUnit = 2; // B = 0, KiB = 1, MiB = 2, GiB = 3, TiB = 4, PiB = 5, EiB = 6
searchSizeFilter.max = 0.00;
searchSizeFilter.maxUnit = 3;
$('searchMinSizeFilter').set('value', searchSizeFilter.min);
$('searchMinSizePrefix').set('value', searchSizeFilter.minUnit);
$('searchMaxSizeFilter').set('value', searchSizeFilter.max);
$('searchMaxSizePrefix').set('value', searchSizeFilter.maxUnit);
}; };
const resetFilters = function() { const getSearchInTorrentName = function() {
// reset filters return $('searchInTorrentName').get('value') === "names" ? "names" : "everywhere";
$('searchMinSeedsFilter').set('value', '0'); };
$('searchMaxSeedsFilter').set('value', '0');
$('searchMinSizeFilter').set('value', '0.00'); const searchInTorrentName = function() {
$('searchMinSizePrefix').set('value', '2'); // MiB LocalPreferences.set('search_in_filter', getSearchInTorrentName());
$('searchMaxSizeFilter').set('value', '0.00'); searchFilterChanged();
$('searchMaxSizePrefix').set('value', '3'); // GiB
}; };
const searchSeedsFilterChanged = function() { const searchSeedsFilterChanged = function() {
@ -632,7 +906,9 @@
}); });
}; };
const loadSearchResultsData = function() { const loadSearchResultsData = function(searchId) {
const state = searchState.get(searchId);
const maxResults = 500; const maxResults = 500;
const url = new URI('api/v2/search/results'); const url = new URI('api/v2/search/results');
new Request.JSON({ new Request.JSON({
@ -640,39 +916,44 @@
method: 'get', method: 'get',
noCache: true, noCache: true,
data: { data: {
id: activeSearchId, id: searchId,
limit: maxResults, limit: maxResults,
offset: searchResultsRowId offset: state.rowId
}, },
onFailure: function(response) { onFailure: function(response) {
if (response.status === 400) { if (response.status === 400) {
// bad params. search id is invalid // bad params. search id is invalid
resetSearchState(); resetSearchState(searchId);
updateStatusIconElement(searchId, 'QBT_TR(An error occurred during search...)QBT_TR[CONTEXT=SearchJobWidget]', 'images/error.svg');
} }
else { else {
clearTimeout(loadSearchResultsTimer); clearTimeout(state.loadResultsTimer);
loadSearchResultsTimer = loadSearchResultsData.delay(3000); state.loadResultsTimer = loadSearchResultsData.delay(3000, this, searchId);
} }
}, },
onSuccess: function(response) { onSuccess: function(response) {
$('error_div').set('html', ''); $('error_div').set('html', '');
const state = searchState.get(searchId);
// check if user stopped the search prior to receiving the response // check if user stopped the search prior to receiving the response
if (!searchRunning) { if (!state.running) {
clearTimeout(loadSearchResultsTimer); clearTimeout(state.loadResultsTimer);
searchResultsRowId = 0; updateStatusIconElement(searchId, 'QBT_TR(Search aborted)QBT_TR[CONTEXT=SearchJobWidget]', 'images/task-reject.svg');
return; return;
} }
if (response) { if (response) {
setupSearchTableEvents(false); setupSearchTableEvents(false);
const state = searchState.get(searchId);
const newRows = [];
if (response.results) { if (response.results) {
const results = response.results; const results = response.results;
for (let i = 0; i < results.length; ++i) { for (let i = 0; i < results.length; ++i) {
const result = results[i]; const result = results[i];
const row = { const row = {
rowId: searchResultsRowId, rowId: state.rowId,
descrLink: result.descrLink, descrLink: result.descrLink,
fileName: result.fileName, fileName: result.fileName,
fileSize: result.fileSize, fileSize: result.fileSize,
@ -682,41 +963,44 @@
siteUrl: result.siteUrl, siteUrl: result.siteUrl,
}; };
newRows.push(row);
state.rows.push(row);
state.rowId += 1;
}
}
// only update table if this search is currently being displayed
if (searchId === getSelectedSearchId()) {
for (const row of newRows) {
searchResultsTable.updateRowData(row); searchResultsTable.updateRowData(row);
++searchResultsRowId;
} }
$('numSearchResultsVisible').set('html', searchResultsTable.getFilteredAndSortedRows().length); $('numSearchResultsVisible').set('html', searchResultsTable.getFilteredAndSortedRows().length);
$('numSearchResultsTotal').set('html', searchResultsTable.getRowIds().length); $('numSearchResultsTotal').set('html', searchResultsTable.getRowIds().length);
searchResultsTable.updateTable();
searchResultsTable.altRow();
} }
searchResultsTable.updateTable(); if ((response.status === "Stopped") && (state.rowId >= response.total)) {
searchResultsTable.altRow(); resetSearchState(searchId);
updateStatusIconElement(searchId, 'QBT_TR(Search has finished)QBT_TR[CONTEXT=SearchJobWidget]', 'images/task-complete.svg');
if ((response.status === "Stopped") && (searchResultsRowId >= response.total)) {
resetSearchState();
return; return;
} }
setupSearchTableEvents(true); setupSearchTableEvents(true);
} }
let timeout = 1000; clearTimeout(state.loadResultsTimer);
if (requestCount > 30) state.loadResultsTimer = loadSearchResultsData.delay(2000, this, searchId);
timeout = 3000;
else if (requestCount > 10)
timeout = 2000;
clearTimeout(loadSearchResultsTimer);
loadSearchResultsTimer = loadSearchResultsData.delay(timeout);
++requestCount;
} }
}).send(); }).send();
}; };
const updateSearchResultsData = function() { const updateSearchResultsData = function(searchId) {
clearTimeout(loadSearchResultsTimer); const state = searchState.get(searchId);
loadSearchResultsTimer = loadSearchResultsData.delay(500); clearTimeout(state.loadResultsTimer);
state.loadResultsTimer = loadSearchResultsData.delay(500, this, searchId);
}; };
new ClipboardJS('.copySearchDataToClipboard', { new ClipboardJS('.copySearchDataToClipboard', {