3 * Copyright (C) 2024 Thomas Piccirello
5 * Permission is hereby granted, free of charge, to any person obtaining a copy
6 * of this software and associated documentation files (the "Software"), to deal
7 * in the Software without restriction, including without limitation the rights
8 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 * copies of the Software, and to permit persons to whom the Software is
10 * furnished to do so, subject to the following conditions:
12 * The above copyright notice and this permission notice shall be included in
13 * all copies or substantial portions of the Software.
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26 window
.qBittorrent
??= {};
27 window
.qBittorrent
.Search
??= (() => {
28 const exports
= () => {
30 startStopSearch
: startStopSearch
,
31 manageSearchPlugins
: manageSearchPlugins
,
32 searchPlugins
: searchPlugins
,
33 searchText
: searchText
,
34 searchSeedsFilter
: searchSeedsFilter
,
35 searchSizeFilter
: searchSizeFilter
,
38 searchInTorrentName
: searchInTorrentName
,
39 onSearchPatternChanged
: onSearchPatternChanged
,
40 categorySelected
: categorySelected
,
41 pluginSelected
: pluginSelected
,
42 searchSeedsFilterChanged
: searchSeedsFilterChanged
,
43 searchSizeFilterChanged
: searchSizeFilterChanged
,
44 searchSizeFilterPrefixChanged
: searchSizeFilterPrefixChanged
,
45 closeSearchTab
: closeSearchTab
,
49 const searchTabIdPrefix
= "Search-";
50 let loadSearchPluginsTimer
;
51 const searchPlugins
= [];
52 let prevSearchPluginsResponse
;
53 let selectedCategory
= "QBT_TR(All categories)QBT_TR[CONTEXT=SearchEngineWidget]";
54 let selectedPlugin
= "enabled";
55 let prevSelectedPlugin
;
56 // whether the current search pattern differs from the pattern that the active search was performed with
57 let searchPatternChanged
= false;
59 let searchResultsTable
;
60 /** @type Map<number, {
61 * searchPattern: string,
62 * filterPattern: string,
63 * seedsFilter: {min: number, max: number},
64 * sizeFilter: {min: number, minUnit: number, max: number, maxUnit: number},
68 * selectedRowIds: number[],
70 * loadResultsTimer: Timer,
71 * sort: {column: string, reverse: string},
73 const searchState
= new Map();
78 const searchSeedsFilter
= {
82 const searchSizeFilter
= {
84 minUnit
: 2, // B = 0, KiB = 1, MiB = 2, GiB = 3, TiB = 4, PiB = 5, EiB = 6
89 const init = function() {
90 // load "Search in" preference from local storage
91 $("searchInTorrentName").value
= (LocalPreferences
.get("search_in_filter") === "names") ? "names" : "everywhere";
92 const searchResultsTableContextMenu
= new window
.qBittorrent
.ContextMenu
.ContextMenu({
93 targets
: ".searchTableRow",
94 menu
: "searchResultsTableMenu",
96 Download
: downloadSearchTorrent
,
97 OpenDescriptionUrl
: openSearchTorrentDescriptionUrl
104 searchResultsTable
= new window
.qBittorrent
.DynamicTable
.SearchResultsTable();
105 searchResultsTable
.setup("searchResultsTableDiv", "searchResultsTableFixedHeaderDiv", searchResultsTableContextMenu
);
108 // listen for changes to searchInNameFilter
109 let searchInNameFilterTimer
= -1;
110 $("searchInNameFilter").addEvent("input", () => {
111 clearTimeout(searchInNameFilterTimer
);
112 searchInNameFilterTimer
= setTimeout(() => {
113 searchInNameFilterTimer
= -1;
115 const value
= $("searchInNameFilter").value
;
116 searchText
.filterPattern
= value
;
117 searchFilterChanged();
118 }, window
.qBittorrent
.Misc
.FILTER_INPUT_DELAY
);
122 defaultEventType
: "keydown",
124 "Enter": function(e
) {
125 // accept enter key as a click
128 const elem
= e
.event
.srcElement
;
129 if (elem
.className
.contains("searchInputField")) {
130 $("startSearchButton").click();
135 case "manageSearchPlugins":
136 manageSearchPlugins();
143 // restore search tabs
144 const searchJobs
= JSON
.parse(LocalPreferences
.get("search_jobs", "[]"));
145 for (const { id
, pattern
} of searchJobs
)
146 createSearchTab(id
, pattern
);
149 const numSearchTabs = function() {
150 return $("searchTabs").getElements("li").length
;
153 const getSearchIdFromTab = function(tab
) {
154 return Number(tab
.id
.substring(searchTabIdPrefix
.length
));
157 const createSearchTab = function(searchId
, pattern
) {
158 const newTabId
= `${searchTabIdPrefix}${searchId}`;
159 const tabElem
= new Element("a", {
162 const closeTabElem
= new Element("img", {
163 alt
: "QBT_TR(Close tab)QBT_TR[CONTEXT=SearchWidget]",
164 title
: "QBT_TR(Close tab)QBT_TR[CONTEXT=SearchWidget]",
165 src
: "images/application-exit.svg",
168 style
: "padding-right: 7px; margin-bottom: -1px; margin-left: -5px",
169 onclick
: "qBittorrent.Search.closeSearchTab(this)",
171 closeTabElem
.inject(tabElem
, "top");
172 tabElem
.appendChild(getStatusIconElement("QBT_TR(Searching...)QBT_TR[CONTEXT=SearchJobWidget]", "images/queued.svg"));
173 $("searchTabs").appendChild(new Element("li", {
176 html
: tabElem
.outerHTML
,
179 // unhide the results elements
180 if (numSearchTabs() >= 1) {
181 $("searchResultsNoSearches").style
.display
= "none";
182 $("searchResultsFilters").style
.display
= "block";
183 $("searchResultsTableContainer").style
.display
= "block";
184 $("searchTabsToolbar").style
.display
= "block";
188 $("searchTabs").getElements("li").removeEvents("click");
189 $("searchTabs").getElements("li").addEvent("click", function(e
) {
190 $("startSearchButton").textContent
= "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]";
195 setActiveTab($(newTabId
));
197 searchResultsTable
.clear();
200 searchState
.set(searchId
, {
201 searchPattern
: pattern
,
202 filterPattern
: searchText
.filterPattern
,
203 seedsFilter
: { min
: searchSeedsFilter
.min
, max
: searchSeedsFilter
.max
},
204 sizeFilter
: { min
: searchSizeFilter
.min
, minUnit
: searchSizeFilter
.minUnit
, max
: searchSizeFilter
.max
, maxUnit
: searchSizeFilter
.maxUnit
},
205 searchIn
: getSearchInTorrentName(),
210 loadResultsTimer
: null,
211 sort
: { column
: searchResultsTable
.sortedColumn
, reverse
: searchResultsTable
.reverseSort
},
213 updateSearchResultsData(searchId
);
216 const closeSearchTab = function(el
) {
217 const tab
= el
.parentElement
.parentElement
;
218 const searchId
= getSearchIdFromTab(tab
);
219 const isTabSelected
= tab
.hasClass("selected");
220 const newTabToSelect
= isTabSelected
? tab
.nextSibling
|| tab
.previousSibling
: null;
222 const currentSearchId
= getSelectedSearchId();
223 const state
= searchState
.get(currentSearchId
);
224 // don't bother sending a stop request if already stopped
225 if (state
&& state
.running
)
226 stopSearch(searchId
);
231 url
: new URI("api/v2/search/delete"),
238 const searchJobs
= JSON
.parse(LocalPreferences
.get("search_jobs", "[]"));
239 const jobIndex
= searchJobs
.findIndex((job
) => job
.id
=== searchId
);
241 searchJobs
.splice(jobIndex
, 1);
242 LocalPreferences
.set("search_jobs", JSON
.stringify(searchJobs
));
245 if (numSearchTabs() === 0) {
249 $("numSearchResultsVisible").textContent
= 0;
250 $("numSearchResultsTotal").textContent
= 0;
251 $("searchResultsNoSearches").style
.display
= "block";
252 $("searchResultsFilters").style
.display
= "none";
253 $("searchResultsTableContainer").style
.display
= "none";
254 $("searchTabsToolbar").style
.display
= "none";
256 else if (isTabSelected
&& newTabToSelect
) {
257 setActiveTab(newTabToSelect
);
258 $("startSearchButton").textContent
= "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]";
262 const saveCurrentTabState = function() {
263 const currentSearchId
= getSelectedSearchId();
264 if (!currentSearchId
)
267 const state
= searchState
.get(currentSearchId
);
271 state
.filterPattern
= searchText
.filterPattern
;
272 state
.seedsFilter
= {
273 min
: searchSeedsFilter
.min
,
274 max
: searchSeedsFilter
.max
,
277 min
: searchSizeFilter
.min
,
278 minUnit
: searchSizeFilter
.minUnit
,
279 max
: searchSizeFilter
.max
,
280 maxUnit
: searchSizeFilter
.maxUnit
,
282 state
.searchIn
= getSearchInTorrentName();
285 column
: searchResultsTable
.sortedColumn
,
286 reverse
: searchResultsTable
.reverseSort
,
289 // we must copy the array to avoid taking a reference to it
290 state
.selectedRowIds
= [...searchResultsTable
.selectedRows
];
293 const setActiveTab = function(tab
) {
294 const searchId
= getSearchIdFromTab(tab
);
295 if (searchId
=== getSelectedSearchId())
298 saveCurrentTabState();
300 MochaUI
.selected(tab
, "searchTabs");
302 const state
= searchState
.get(searchId
);
303 let rowsToSelect
= [];
305 // restore table rows
306 searchResultsTable
.clear();
308 for (const row
of state
.rows
)
309 searchResultsTable
.updateRowData(row
);
311 rowsToSelect
= state
.selectedRowIds
;
314 searchText
.pattern
= state
.searchPattern
;
315 searchText
.filterPattern
= state
.filterPattern
;
316 $("searchInNameFilter").value
= state
.filterPattern
;
318 searchSeedsFilter
.min
= state
.seedsFilter
.min
;
319 searchSeedsFilter
.max
= state
.seedsFilter
.max
;
320 $("searchMinSeedsFilter").value
= state
.seedsFilter
.min
;
321 $("searchMaxSeedsFilter").value
= state
.seedsFilter
.max
;
323 searchSizeFilter
.min
= state
.sizeFilter
.min
;
324 searchSizeFilter
.minUnit
= state
.sizeFilter
.minUnit
;
325 searchSizeFilter
.max
= state
.sizeFilter
.max
;
326 searchSizeFilter
.maxUnit
= state
.sizeFilter
.maxUnit
;
327 $("searchMinSizeFilter").value
= state
.sizeFilter
.min
;
328 $("searchMinSizePrefix").value
= state
.sizeFilter
.minUnit
;
329 $("searchMaxSizeFilter").value
= state
.sizeFilter
.max
;
330 $("searchMaxSizePrefix").value
= state
.sizeFilter
.maxUnit
;
332 const currentSearchPattern
= $("searchPattern").value
.trim();
333 if (state
.running
&& (state
.searchPattern
=== currentSearchPattern
)) {
334 // allow search to be stopped
335 $("startSearchButton").textContent
= "QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]";
336 searchPatternChanged
= false;
339 searchResultsTable
.setSortedColumn(state
.sort
.column
, state
.sort
.reverse
);
341 $("searchInTorrentName").value
= state
.searchIn
;
344 // must restore all filters before calling updateTable
345 searchResultsTable
.updateTable();
347 // must reselect rows after calling updateTable
348 if (rowsToSelect
.length
> 0)
349 searchResultsTable
.reselectRows(rowsToSelect
);
351 $("numSearchResultsVisible").textContent
= searchResultsTable
.getFilteredAndSortedRows().length
;
352 $("numSearchResultsTotal").textContent
= searchResultsTable
.getRowIds().length
;
354 setupSearchTableEvents(true);
357 const getStatusIconElement = function(text
, image
) {
358 return new Element("img", {
365 style
: "margin-bottom: -2px; margin-left: 7px",
369 const updateStatusIconElement = function(searchId
, text
, image
) {
370 const searchTab
= $(`${searchTabIdPrefix}${searchId}`);
372 const statusIcon
= searchTab
.getElement(".statusIcon");
373 statusIcon
.alt
= text
;
374 statusIcon
.title
= text
;
375 statusIcon
.src
= image
;
379 const startSearch = function(pattern
, category
, plugins
) {
380 searchPatternChanged
= false;
382 const url
= new URI("api/v2/search/start");
391 onSuccess: function(response
) {
392 $("startSearchButton").textContent
= "QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]";
393 const searchId
= response
.id
;
394 createSearchTab(searchId
, pattern
);
396 const searchJobs
= JSON
.parse(LocalPreferences
.get("search_jobs", "[]"));
397 searchJobs
.push({ id
: searchId
, pattern
: pattern
});
398 LocalPreferences
.set("search_jobs", JSON
.stringify(searchJobs
));
403 const stopSearch = function(searchId
) {
404 const url
= new URI("api/v2/search/stop");
411 onSuccess: function(response
) {
412 resetSearchState(searchId
);
413 // not strictly necessary to do this when the tab is being closed, but there's no harm in it
414 updateStatusIconElement(searchId
, "QBT_TR(Search aborted)QBT_TR[CONTEXT=SearchJobWidget]", "images/task-reject.svg");
419 const getSelectedSearchId = function() {
420 const selectedTab
= $("searchTabs").getElement("li.selected");
421 return selectedTab
? getSearchIdFromTab(selectedTab
) : null;
424 const startStopSearch = function() {
425 const currentSearchId
= getSelectedSearchId();
426 const state
= searchState
.get(currentSearchId
);
427 const isSearchRunning
= state
&& state
.running
;
428 if (!isSearchRunning
|| searchPatternChanged
) {
429 const pattern
= $("searchPattern").value
.trim();
430 const category
= $("categorySelect").value
;
431 const plugins
= $("pluginsSelect").value
;
433 if (!pattern
|| !category
|| !plugins
)
436 searchText
.pattern
= pattern
;
437 startSearch(pattern
, category
, plugins
);
440 stopSearch(currentSearchId
);
444 const openSearchTorrentDescriptionUrl = function() {
445 searchResultsTable
.selectedRowsIds().each((rowId
) => {
446 window
.open(searchResultsTable
.rows
.get(rowId
).full_data
.descrLink
, "_blank");
450 const copySearchTorrentName = function() {
452 searchResultsTable
.selectedRowsIds().each((rowId
) => {
453 names
.push(searchResultsTable
.rows
.get(rowId
).full_data
.fileName
);
455 return names
.join("\n");
458 const copySearchTorrentDownloadLink = function() {
460 searchResultsTable
.selectedRowsIds().each((rowId
) => {
461 urls
.push(searchResultsTable
.rows
.get(rowId
).full_data
.fileUrl
);
463 return urls
.join("\n");
466 const copySearchTorrentDescriptionUrl = function() {
468 searchResultsTable
.selectedRowsIds().each((rowId
) => {
469 urls
.push(searchResultsTable
.rows
.get(rowId
).full_data
.descrLink
);
471 return urls
.join("\n");
474 const downloadSearchTorrent = function() {
476 searchResultsTable
.selectedRowsIds().each((rowId
) => {
477 urls
.push(searchResultsTable
.rows
.get(rowId
).full_data
.fileUrl
);
480 // only proceed if at least 1 row was selected
484 showDownloadPage(urls
);
487 const manageSearchPlugins = function() {
488 const id
= "searchPlugins";
492 title
: "QBT_TR(Search plugins)QBT_TR[CONTEXT=PluginSelectDlg]",
494 contentURL
: "views/searchplugins.html",
498 paddingHorizontal
: 0,
499 width
: loadWindowWidth(id
, 600),
500 height
: loadWindowHeight(id
, 360),
501 onResize: function() {
504 onBeforeBuild: function() {
507 onClose: function() {
508 clearTimeout(loadSearchPluginsTimer
);
514 const loadSearchPlugins = function() {
516 loadSearchPluginsTimer
= loadSearchPlugins
.delay(2000);
519 const onSearchPatternChanged = function() {
520 const currentSearchId
= getSelectedSearchId();
521 const state
= searchState
.get(currentSearchId
);
522 const currentSearchPattern
= $("searchPattern").value
.trim();
523 // start a new search if pattern has changed, otherwise allow the search to be stopped
524 if (state
&& (state
.searchPattern
=== currentSearchPattern
)) {
525 searchPatternChanged
= false;
526 $("startSearchButton").textContent
= "QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]";
529 searchPatternChanged
= true;
530 $("startSearchButton").textContent
= "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]";
534 const categorySelected = function() {
535 selectedCategory
= $("categorySelect").value
;
538 const pluginSelected = function() {
539 selectedPlugin
= $("pluginsSelect").value
;
541 if (selectedPlugin
!== prevSelectedPlugin
) {
542 prevSelectedPlugin
= selectedPlugin
;
543 getSearchCategories();
547 const reselectCategory = function() {
548 for (let i
= 0; i
< $("categorySelect").options
.length
; ++i
) {
549 if ($("categorySelect").options
[i
].get("value") === selectedCategory
)
550 $("categorySelect").options
[i
].selected
= true;
556 const reselectPlugin = function() {
557 for (let i
= 0; i
< $("pluginsSelect").options
.length
; ++i
) {
558 if ($("pluginsSelect").options
[i
].get("value") === selectedPlugin
)
559 $("pluginsSelect").options
[i
].selected
= true;
565 const resetSearchState = function(searchId
) {
566 $("startSearchButton").textContent
= "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]";
567 const state
= searchState
.get(searchId
);
569 state
.running
= false;
570 clearTimeout(state
.loadResultsTimer
);
574 const getSearchCategories = function() {
575 const populateCategorySelect = function(categories
) {
576 const categoryHtml
= [];
577 categories
.each((category
) => {
578 const option
= new Element("option");
579 option
.value
= category
.id
;
580 option
.textContent
= category
.name
;
581 categoryHtml
.push(option
.outerHTML
);
584 // first category is "All Categories"
585 if (categoryHtml
.length
> 1) {
587 const option
= new Element("option");
588 option
.disabled
= true;
589 option
.textContent
= "──────────";
590 categoryHtml
.splice(1, 0, option
.outerHTML
);
593 $("categorySelect").innerHTML
= categoryHtml
.join("");
596 const selectedPlugin
= $("pluginsSelect").value
;
598 if ((selectedPlugin
=== "all") || (selectedPlugin
=== "enabled")) {
599 const uniqueCategories
= {};
600 for (const plugin
of searchPlugins
) {
601 if ((selectedPlugin
=== "enabled") && !plugin
.enabled
)
603 for (const category
of plugin
.supportedCategories
) {
604 if (uniqueCategories
[category
.id
] === undefined)
605 uniqueCategories
[category
.id
] = category
;
608 // we must sort the ids to maintain consistent order.
609 const categories
= Object
.keys(uniqueCategories
).sort().map(id
=> uniqueCategories
[id
]);
610 populateCategorySelect(categories
);
613 const plugin
= getPlugin(selectedPlugin
);
614 const plugins
= (plugin
=== null) ? [] : plugin
.supportedCategories
;
615 populateCategorySelect(plugins
);
621 const getPlugins = function() {
623 url
: new URI("api/v2/search/plugins"),
626 onSuccess: function(response
) {
627 if (response
!== prevSearchPluginsResponse
) {
628 prevSearchPluginsResponse
= response
;
629 searchPlugins
.length
= 0;
630 response
.forEach((plugin
) => {
631 searchPlugins
.push(plugin
);
634 const pluginsHtml
= [];
635 pluginsHtml
.push('<option value="enabled">QBT_TR(Only enabled)QBT_TR[CONTEXT=SearchEngineWidget]</option>');
636 pluginsHtml
.push('<option value="all">QBT_TR(All plugins)QBT_TR[CONTEXT=SearchEngineWidget]</option>');
638 const searchPluginsEmpty
= (searchPlugins
.length
=== 0);
639 if (!searchPluginsEmpty
) {
640 $("searchResultsNoPlugins").style
.display
= "none";
641 if (numSearchTabs() === 0)
642 $("searchResultsNoSearches").style
.display
= "block";
644 // sort plugins alphabetically
645 const allPlugins
= searchPlugins
.sort((left
, right
) => {
646 const leftName
= left
.fullName
;
647 const rightName
= right
.fullName
;
648 return window
.qBittorrent
.Misc
.naturalSortCollator
.compare(leftName
, rightName
);
651 allPlugins
.each((plugin
) => {
652 if (plugin
.enabled
=== true)
653 pluginsHtml
.push("<option value='" + window
.qBittorrent
.Misc
.escapeHtml(plugin
.name
) + "'>" + window
.qBittorrent
.Misc
.escapeHtml(plugin
.fullName
) + "</option>");
656 if (pluginsHtml
.length
> 2)
657 pluginsHtml
.splice(2, 0, "<option disabled>──────────</option>");
660 $("pluginsSelect").innerHTML
= pluginsHtml
.join("");
662 $("searchPattern").disabled
= searchPluginsEmpty
;
663 $("categorySelect").disabled
= searchPluginsEmpty
;
664 $("pluginsSelect").disabled
= searchPluginsEmpty
;
665 $("startSearchButton").disabled
= searchPluginsEmpty
;
667 if (window
.qBittorrent
.SearchPlugins
!== undefined)
668 window
.qBittorrent
.SearchPlugins
.updateTable();
676 const getPlugin = function(name
) {
677 for (let i
= 0; i
< searchPlugins
.length
; ++i
) {
678 if (searchPlugins
[i
].name
=== name
)
679 return searchPlugins
[i
];
685 const resetFilters = function() {
686 searchText
.filterPattern
= "";
687 $("searchInNameFilter").value
= "";
689 searchSeedsFilter
.min
= 0;
690 searchSeedsFilter
.max
= 0;
691 $("searchMinSeedsFilter").value
= searchSeedsFilter
.min
;
692 $("searchMaxSeedsFilter").value
= searchSeedsFilter
.max
;
694 searchSizeFilter
.min
= 0.00;
695 searchSizeFilter
.minUnit
= 2; // B = 0, KiB = 1, MiB = 2, GiB = 3, TiB = 4, PiB = 5, EiB = 6
696 searchSizeFilter
.max
= 0.00;
697 searchSizeFilter
.maxUnit
= 3;
698 $("searchMinSizeFilter").value
= searchSizeFilter
.min
;
699 $("searchMinSizePrefix").value
= searchSizeFilter
.minUnit
;
700 $("searchMaxSizeFilter").value
= searchSizeFilter
.max
;
701 $("searchMaxSizePrefix").value
= searchSizeFilter
.maxUnit
;
704 const getSearchInTorrentName = function() {
705 return ($("searchInTorrentName").value
=== "names") ? "names" : "everywhere";
708 const searchInTorrentName = function() {
709 LocalPreferences
.set("search_in_filter", getSearchInTorrentName());
710 searchFilterChanged();
713 const searchSeedsFilterChanged = function() {
714 searchSeedsFilter
.min
= $("searchMinSeedsFilter").value
;
715 searchSeedsFilter
.max
= $("searchMaxSeedsFilter").value
;
717 searchFilterChanged();
720 const searchSizeFilterChanged = function() {
721 searchSizeFilter
.min
= $("searchMinSizeFilter").value
;
722 searchSizeFilter
.minUnit
= $("searchMinSizePrefix").value
;
723 searchSizeFilter
.max
= $("searchMaxSizeFilter").value
;
724 searchSizeFilter
.maxUnit
= $("searchMaxSizePrefix").value
;
726 searchFilterChanged();
729 const searchSizeFilterPrefixChanged = function() {
730 if ((Number($("searchMinSizeFilter").value
) !== 0) || (Number($("searchMaxSizeFilter").value
) !== 0))
731 searchSizeFilterChanged();
734 const searchFilterChanged = function() {
735 searchResultsTable
.updateTable();
736 $("numSearchResultsVisible").textContent
= searchResultsTable
.getFilteredAndSortedRows().length
;
739 const setupSearchTableEvents = function(enable
) {
741 $$(".searchTableRow").each((target
) => {
742 target
.addEventListener("dblclick", downloadSearchTorrent
, false);
746 $$(".searchTableRow").each((target
) => {
747 target
.removeEventListener("dblclick", downloadSearchTorrent
, false);
752 const loadSearchResultsData = function(searchId
) {
753 const state
= searchState
.get(searchId
);
755 const maxResults
= 500;
756 const url
= new URI("api/v2/search/results");
766 onFailure: function(response
) {
767 if ((response
.status
=== 400) || (response
.status
=== 404)) {
768 // bad params. search id is invalid
769 resetSearchState(searchId
);
770 updateStatusIconElement(searchId
, "QBT_TR(An error occurred during search...)QBT_TR[CONTEXT=SearchJobWidget]", "images/error.svg");
773 clearTimeout(state
.loadResultsTimer
);
774 state
.loadResultsTimer
= loadSearchResultsData
.delay(3000, this, searchId
);
777 onSuccess: function(response
) {
778 $("error_div").textContent
= "";
780 const state
= searchState
.get(searchId
);
781 // check if user stopped the search prior to receiving the response
782 if (!state
.running
) {
783 clearTimeout(state
.loadResultsTimer
);
784 updateStatusIconElement(searchId
, "QBT_TR(Search aborted)QBT_TR[CONTEXT=SearchJobWidget]", "images/task-reject.svg");
789 setupSearchTableEvents(false);
791 const state
= searchState
.get(searchId
);
794 if (response
.results
) {
795 const results
= response
.results
;
796 for (let i
= 0; i
< results
.length
; ++i
) {
797 const result
= results
[i
];
800 descrLink
: result
.descrLink
,
801 fileName
: result
.fileName
,
802 fileSize
: result
.fileSize
,
803 fileUrl
: result
.fileUrl
,
804 nbLeechers
: result
.nbLeechers
,
805 nbSeeders
: result
.nbSeeders
,
806 siteUrl
: result
.siteUrl
,
807 pubDate
: result
.pubDate
,
811 state
.rows
.push(row
);
816 // only update table if this search is currently being displayed
817 if (searchId
=== getSelectedSearchId()) {
818 for (const row
of newRows
)
819 searchResultsTable
.updateRowData(row
);
821 $("numSearchResultsVisible").textContent
= searchResultsTable
.getFilteredAndSortedRows().length
;
822 $("numSearchResultsTotal").textContent
= searchResultsTable
.getRowIds().length
;
824 searchResultsTable
.updateTable();
827 setupSearchTableEvents(true);
829 if ((response
.status
=== "Stopped") && (state
.rowId
>= response
.total
)) {
830 resetSearchState(searchId
);
831 updateStatusIconElement(searchId
, "QBT_TR(Search has finished)QBT_TR[CONTEXT=SearchJobWidget]", "images/task-complete.svg");
836 clearTimeout(state
.loadResultsTimer
);
837 state
.loadResultsTimer
= loadSearchResultsData
.delay(2000, this, searchId
);
842 const updateSearchResultsData = function(searchId
) {
843 const state
= searchState
.get(searchId
);
844 clearTimeout(state
.loadResultsTimer
);
845 state
.loadResultsTimer
= loadSearchResultsData
.delay(500, this, searchId
);
848 new ClipboardJS(".copySearchDataToClipboard", {
849 text: function(trigger
) {
850 switch (trigger
.id
) {
851 case "copySearchTorrentName":
852 return copySearchTorrentName();
853 case "copySearchTorrentDownloadLink":
854 return copySearchTorrentDownloadLink();
855 case "copySearchTorrentDescriptionUrl":
856 return copySearchTorrentDescriptionUrl();
865 Object
.freeze(window
.qBittorrent
.Search
);