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 if (window
.qBittorrent
=== undefined)
27 window
.qBittorrent
= {};
29 window
.qBittorrent
.Search
= (function() {
30 const exports = function() {
32 startStopSearch
: startStopSearch
,
33 manageSearchPlugins
: manageSearchPlugins
,
34 searchPlugins
: searchPlugins
,
35 searchText
: searchText
,
36 searchSeedsFilter
: searchSeedsFilter
,
37 searchSizeFilter
: searchSizeFilter
,
40 searchInTorrentName
: searchInTorrentName
,
41 onSearchPatternChanged
: onSearchPatternChanged
,
42 categorySelected
: categorySelected
,
43 pluginSelected
: pluginSelected
,
44 searchSeedsFilterChanged
: searchSeedsFilterChanged
,
45 searchSizeFilterChanged
: searchSizeFilterChanged
,
46 searchSizeFilterPrefixChanged
: searchSizeFilterPrefixChanged
,
47 closeSearchTab
: closeSearchTab
,
51 const searchTabIdPrefix
= "Search-";
52 let loadSearchPluginsTimer
;
53 const searchPlugins
= [];
54 let prevSearchPluginsResponse
;
55 let selectedCategory
= "QBT_TR(All categories)QBT_TR[CONTEXT=SearchEngineWidget]";
56 let selectedPlugin
= "enabled";
57 let prevSelectedPlugin
;
58 // whether the current search pattern differs from the pattern that the active search was performed with
59 let searchPatternChanged
= false;
61 let searchResultsTable
;
62 /** @type Map<number, {
63 * searchPattern: string,
64 * filterPattern: string,
65 * seedsFilter: {min: number, max: number},
66 * sizeFilter: {min: number, minUnit: number, max: number, maxUnit: number},
70 * selectedRowIds: number[],
72 * loadResultsTimer: Timer,
73 * sort: {column: string, reverse: string},
75 const searchState
= new Map();
80 const searchSeedsFilter
= {
84 const searchSizeFilter
= {
86 minUnit
: 2, // B = 0, KiB = 1, MiB = 2, GiB = 3, TiB = 4, PiB = 5, EiB = 6
91 const init = function() {
92 // load "Search in" preference from local storage
93 $("searchInTorrentName").set("value", (LocalPreferences
.get("search_in_filter") === "names") ? "names" : "everywhere");
94 const searchResultsTableContextMenu
= new window
.qBittorrent
.ContextMenu
.ContextMenu({
95 targets
: ".searchTableRow",
96 menu
: "searchResultsTableMenu",
98 Download
: downloadSearchTorrent
,
99 OpenDescriptionUrl
: openSearchTorrentDescriptionUrl
106 searchResultsTable
= new window
.qBittorrent
.DynamicTable
.SearchResultsTable();
107 searchResultsTable
.setup("searchResultsTableDiv", "searchResultsTableFixedHeaderDiv", searchResultsTableContextMenu
);
110 // listen for changes to searchInNameFilter
111 let searchInNameFilterTimer
= -1;
112 $("searchInNameFilter").addEvent("input", () => {
113 clearTimeout(searchInNameFilterTimer
);
114 searchInNameFilterTimer
= setTimeout(() => {
115 searchInNameFilterTimer
= -1;
117 const value
= $("searchInNameFilter").get("value");
118 searchText
.filterPattern
= value
;
119 searchFilterChanged();
120 }, window
.qBittorrent
.Misc
.FILTER_INPUT_DELAY
);
124 defaultEventType
: "keydown",
126 "Enter": function(e
) {
127 // accept enter key as a click
130 const elem
= e
.event
.srcElement
;
131 if (elem
.className
.contains("searchInputField")) {
132 $("startSearchButton").click();
137 case "manageSearchPlugins":
138 manageSearchPlugins();
145 // restore search tabs
146 const searchJobs
= JSON
.parse(LocalPreferences
.get("search_jobs", "[]"));
147 for (const { id
, pattern
} of searchJobs
)
148 createSearchTab(id
, pattern
);
151 const numSearchTabs = function() {
152 return $("searchTabs").getElements("li").length
;
155 const getSearchIdFromTab = function(tab
) {
156 return Number(tab
.id
.substring(searchTabIdPrefix
.length
));
159 const createSearchTab = function(searchId
, pattern
) {
160 const newTabId
= `${searchTabIdPrefix}${searchId}`;
161 const tabElem
= new Element("a", {
164 const closeTabElem
= new Element("img", {
165 alt
: "QBT_TR(Close tab)QBT_TR[CONTEXT=SearchWidget]",
166 title
: "QBT_TR(Close tab)QBT_TR[CONTEXT=SearchWidget]",
167 src
: "images/application-exit.svg",
170 style
: "padding-right: 7px; margin-bottom: -1px; margin-left: -5px",
171 onclick
: "qBittorrent.Search.closeSearchTab(this)",
173 closeTabElem
.inject(tabElem
, "top");
174 tabElem
.appendChild(getStatusIconElement("QBT_TR(Searching...)QBT_TR[CONTEXT=SearchJobWidget]", "images/queued.svg"));
175 $("searchTabs").appendChild(new Element("li", {
178 html
: tabElem
.outerHTML
,
181 // unhide the results elements
182 if (numSearchTabs() >= 1) {
183 $("searchResultsNoSearches").style
.display
= "none";
184 $("searchResultsFilters").style
.display
= "block";
185 $("searchResultsTableContainer").style
.display
= "block";
186 $("searchTabsToolbar").style
.display
= "block";
190 $("searchTabs").getElements("li").removeEvents("click");
191 $("searchTabs").getElements("li").addEvent("click", function(e
) {
192 $("startSearchButton").set("text", "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]");
197 setActiveTab($(newTabId
));
199 searchResultsTable
.clear();
202 searchState
.set(searchId
, {
203 searchPattern
: pattern
,
204 filterPattern
: searchText
.filterPattern
,
205 seedsFilter
: { min
: searchSeedsFilter
.min
, max
: searchSeedsFilter
.max
},
206 sizeFilter
: { min
: searchSizeFilter
.min
, minUnit
: searchSizeFilter
.minUnit
, max
: searchSizeFilter
.max
, maxUnit
: searchSizeFilter
.maxUnit
},
207 searchIn
: getSearchInTorrentName(),
212 loadResultsTimer
: null,
213 sort
: { column
: searchResultsTable
.sortedColumn
, reverse
: searchResultsTable
.reverseSort
},
215 updateSearchResultsData(searchId
);
218 const closeSearchTab = function(el
) {
219 const tab
= el
.parentElement
.parentElement
;
220 const searchId
= getSearchIdFromTab(tab
);
221 const isTabSelected
= tab
.hasClass("selected");
222 const newTabToSelect
= isTabSelected
? tab
.nextSibling
|| tab
.previousSibling
: null;
224 const currentSearchId
= getSelectedSearchId();
225 const state
= searchState
.get(currentSearchId
);
226 // don't bother sending a stop request if already stopped
227 if (state
&& state
.running
)
228 stopSearch(searchId
);
233 url
: new URI("api/v2/search/delete"),
240 const searchJobs
= JSON
.parse(LocalPreferences
.get("search_jobs", "[]"));
241 const jobIndex
= searchJobs
.findIndex((job
) => job
.id
=== searchId
);
243 searchJobs
.splice(jobIndex
, 1);
244 LocalPreferences
.set("search_jobs", JSON
.stringify(searchJobs
));
247 if (numSearchTabs() === 0) {
251 $("numSearchResultsVisible").set("html", 0);
252 $("numSearchResultsTotal").set("html", 0);
253 $("searchResultsNoSearches").style
.display
= "block";
254 $("searchResultsFilters").style
.display
= "none";
255 $("searchResultsTableContainer").style
.display
= "none";
256 $("searchTabsToolbar").style
.display
= "none";
258 else if (isTabSelected
&& newTabToSelect
) {
259 setActiveTab(newTabToSelect
);
260 $("startSearchButton").set("text", "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]");
264 const saveCurrentTabState = function() {
265 const currentSearchId
= getSelectedSearchId();
266 if (!currentSearchId
)
269 const state
= searchState
.get(currentSearchId
);
273 state
.filterPattern
= searchText
.filterPattern
;
274 state
.seedsFilter
= {
275 min
: searchSeedsFilter
.min
,
276 max
: searchSeedsFilter
.max
,
279 min
: searchSizeFilter
.min
,
280 minUnit
: searchSizeFilter
.minUnit
,
281 max
: searchSizeFilter
.max
,
282 maxUnit
: searchSizeFilter
.maxUnit
,
284 state
.searchIn
= getSearchInTorrentName();
287 column
: searchResultsTable
.sortedColumn
,
288 reverse
: searchResultsTable
.reverseSort
,
291 // we must copy the array to avoid taking a reference to it
292 state
.selectedRowIds
= [...searchResultsTable
.selectedRows
];
295 const setActiveTab = function(tab
) {
296 const searchId
= getSearchIdFromTab(tab
);
297 if (searchId
=== getSelectedSearchId())
300 saveCurrentTabState();
302 MochaUI
.selected(tab
, "searchTabs");
304 const state
= searchState
.get(searchId
);
305 let rowsToSelect
= [];
307 // restore table rows
308 searchResultsTable
.clear();
310 for (const row
of state
.rows
)
311 searchResultsTable
.updateRowData(row
);
313 rowsToSelect
= state
.selectedRowIds
;
316 searchText
.pattern
= state
.searchPattern
;
317 searchText
.filterPattern
= state
.filterPattern
;
318 $("searchInNameFilter").set("value", state
.filterPattern
);
320 searchSeedsFilter
.min
= state
.seedsFilter
.min
;
321 searchSeedsFilter
.max
= state
.seedsFilter
.max
;
322 $("searchMinSeedsFilter").set("value", state
.seedsFilter
.min
);
323 $("searchMaxSeedsFilter").set("value", state
.seedsFilter
.max
);
325 searchSizeFilter
.min
= state
.sizeFilter
.min
;
326 searchSizeFilter
.minUnit
= state
.sizeFilter
.minUnit
;
327 searchSizeFilter
.max
= state
.sizeFilter
.max
;
328 searchSizeFilter
.maxUnit
= state
.sizeFilter
.maxUnit
;
329 $("searchMinSizeFilter").set("value", state
.sizeFilter
.min
);
330 $("searchMinSizePrefix").set("value", state
.sizeFilter
.minUnit
);
331 $("searchMaxSizeFilter").set("value", state
.sizeFilter
.max
);
332 $("searchMaxSizePrefix").set("value", state
.sizeFilter
.maxUnit
);
334 const currentSearchPattern
= $("searchPattern").getProperty("value").trim();
335 if (state
.running
&& (state
.searchPattern
=== currentSearchPattern
)) {
336 // allow search to be stopped
337 $("startSearchButton").set("text", "QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]");
338 searchPatternChanged
= false;
341 searchResultsTable
.setSortedColumn(state
.sort
.column
, state
.sort
.reverse
);
343 $("searchInTorrentName").set("value", state
.searchIn
);
346 // must restore all filters before calling updateTable
347 searchResultsTable
.updateTable();
348 searchResultsTable
.altRow();
350 // must reselect rows after calling updateTable
351 if (rowsToSelect
.length
> 0)
352 searchResultsTable
.reselectRows(rowsToSelect
);
354 $("numSearchResultsVisible").set("html", searchResultsTable
.getFilteredAndSortedRows().length
);
355 $("numSearchResultsTotal").set("html", searchResultsTable
.getRowIds().length
);
357 setupSearchTableEvents(true);
360 const getStatusIconElement = function(text
, image
) {
361 return new Element("img", {
368 style
: "margin-bottom: -2px; margin-left: 7px",
372 const updateStatusIconElement = function(searchId
, text
, image
) {
373 const searchTab
= $(`${searchTabIdPrefix}${searchId}`);
375 const statusIcon
= searchTab
.getElement(".statusIcon");
376 statusIcon
.set("alt", text
);
377 statusIcon
.set("title", text
);
378 statusIcon
.set("src", image
);
382 const startSearch = function(pattern
, category
, plugins
) {
383 searchPatternChanged
= false;
385 const url
= new URI("api/v2/search/start");
394 onSuccess: function(response
) {
395 $("startSearchButton").set("text", "QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]");
396 const searchId
= response
.id
;
397 createSearchTab(searchId
, pattern
);
399 const searchJobs
= JSON
.parse(LocalPreferences
.get("search_jobs", "[]"));
400 searchJobs
.push({ id
: searchId
, pattern
: pattern
});
401 LocalPreferences
.set("search_jobs", JSON
.stringify(searchJobs
));
406 const stopSearch = function(searchId
) {
407 const url
= new URI("api/v2/search/stop");
414 onSuccess: function(response
) {
415 resetSearchState(searchId
);
416 // not strictly necessary to do this when the tab is being closed, but there's no harm in it
417 updateStatusIconElement(searchId
, "QBT_TR(Search aborted)QBT_TR[CONTEXT=SearchJobWidget]", "images/task-reject.svg");
422 const getSelectedSearchId = function() {
423 const selectedTab
= $("searchTabs").getElement("li.selected");
424 return selectedTab
? getSearchIdFromTab(selectedTab
) : null;
427 const startStopSearch = function() {
428 const currentSearchId
= getSelectedSearchId();
429 const state
= searchState
.get(currentSearchId
);
430 const isSearchRunning
= state
&& state
.running
;
431 if (!isSearchRunning
|| searchPatternChanged
) {
432 const pattern
= $("searchPattern").getProperty("value").trim();
433 const category
= $("categorySelect").getProperty("value");
434 const plugins
= $("pluginsSelect").getProperty("value");
436 if (!pattern
|| !category
|| !plugins
)
439 searchText
.pattern
= pattern
;
440 startSearch(pattern
, category
, plugins
);
443 stopSearch(currentSearchId
);
447 const openSearchTorrentDescriptionUrl = function() {
448 searchResultsTable
.selectedRowsIds().each((rowId
) => {
449 window
.open(searchResultsTable
.rows
.get(rowId
).full_data
.descrLink
, "_blank");
453 const copySearchTorrentName = function() {
455 searchResultsTable
.selectedRowsIds().each((rowId
) => {
456 names
.push(searchResultsTable
.rows
.get(rowId
).full_data
.fileName
);
458 return names
.join("\n");
461 const copySearchTorrentDownloadLink = function() {
463 searchResultsTable
.selectedRowsIds().each((rowId
) => {
464 urls
.push(searchResultsTable
.rows
.get(rowId
).full_data
.fileUrl
);
466 return urls
.join("\n");
469 const copySearchTorrentDescriptionUrl = function() {
471 searchResultsTable
.selectedRowsIds().each((rowId
) => {
472 urls
.push(searchResultsTable
.rows
.get(rowId
).full_data
.descrLink
);
474 return urls
.join("\n");
477 const downloadSearchTorrent = function() {
479 searchResultsTable
.selectedRowsIds().each((rowId
) => {
480 urls
.push(searchResultsTable
.rows
.get(rowId
).full_data
.fileUrl
);
483 // only proceed if at least 1 row was selected
487 showDownloadPage(urls
);
490 const manageSearchPlugins = function() {
491 const id
= "searchPlugins";
495 title
: "QBT_TR(Search plugins)QBT_TR[CONTEXT=PluginSelectDlg]",
497 contentURL
: "views/searchplugins.html",
501 paddingHorizontal
: 0,
502 width
: loadWindowWidth(id
, 600),
503 height
: loadWindowHeight(id
, 360),
504 onResize: function() {
507 onBeforeBuild: function() {
510 onClose: function() {
511 clearTimeout(loadSearchPluginsTimer
);
517 const loadSearchPlugins = function() {
519 loadSearchPluginsTimer
= loadSearchPlugins
.delay(2000);
522 const onSearchPatternChanged = function() {
523 const currentSearchId
= getSelectedSearchId();
524 const state
= searchState
.get(currentSearchId
);
525 const currentSearchPattern
= $("searchPattern").getProperty("value").trim();
526 // start a new search if pattern has changed, otherwise allow the search to be stopped
527 if (state
&& (state
.searchPattern
=== currentSearchPattern
)) {
528 searchPatternChanged
= false;
529 $("startSearchButton").set("text", "QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]");
532 searchPatternChanged
= true;
533 $("startSearchButton").set("text", "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]");
537 const categorySelected = function() {
538 selectedCategory
= $("categorySelect").get("value");
541 const pluginSelected = function() {
542 selectedPlugin
= $("pluginsSelect").get("value");
544 if (selectedPlugin
!== prevSelectedPlugin
) {
545 prevSelectedPlugin
= selectedPlugin
;
546 getSearchCategories();
550 const reselectCategory = function() {
551 for (let i
= 0; i
< $("categorySelect").options
.length
; ++i
) {
552 if ($("categorySelect").options
[i
].get("value") === selectedCategory
)
553 $("categorySelect").options
[i
].selected
= true;
559 const reselectPlugin = function() {
560 for (let i
= 0; i
< $("pluginsSelect").options
.length
; ++i
) {
561 if ($("pluginsSelect").options
[i
].get("value") === selectedPlugin
)
562 $("pluginsSelect").options
[i
].selected
= true;
568 const resetSearchState = function(searchId
) {
569 $("startSearchButton").set("text", "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]");
570 const state
= searchState
.get(searchId
);
572 state
.running
= false;
573 clearTimeout(state
.loadResultsTimer
);
577 const getSearchCategories = function() {
578 const populateCategorySelect = function(categories
) {
579 const categoryHtml
= [];
580 categories
.each((category
) => {
581 const option
= new Element("option");
582 option
.set("value", category
.id
);
583 option
.set("html", category
.name
);
584 categoryHtml
.push(option
.outerHTML
);
587 // first category is "All Categories"
588 if (categoryHtml
.length
> 1) {
590 const option
= new Element("option");
591 option
.set("disabled", true);
592 option
.set("html", "──────────");
593 categoryHtml
.splice(1, 0, option
.outerHTML
);
596 $("categorySelect").set("html", categoryHtml
.join(""));
599 const selectedPlugin
= $("pluginsSelect").get("value");
601 if ((selectedPlugin
=== "all") || (selectedPlugin
=== "enabled")) {
602 const uniqueCategories
= {};
603 for (const plugin
of searchPlugins
) {
604 if ((selectedPlugin
=== "enabled") && !plugin
.enabled
)
606 for (const category
of plugin
.supportedCategories
) {
607 if (uniqueCategories
[category
.id
] === undefined)
608 uniqueCategories
[category
.id
] = category
;
611 // we must sort the ids to maintain consistent order.
612 const categories
= Object
.keys(uniqueCategories
).sort().map(id
=> uniqueCategories
[id
]);
613 populateCategorySelect(categories
);
616 const plugin
= getPlugin(selectedPlugin
);
617 const plugins
= (plugin
=== null) ? [] : plugin
.supportedCategories
;
618 populateCategorySelect(plugins
);
624 const getPlugins = function() {
626 url
: new URI("api/v2/search/plugins"),
629 onSuccess: function(response
) {
630 if (response
!== prevSearchPluginsResponse
) {
631 prevSearchPluginsResponse
= response
;
632 searchPlugins
.length
= 0;
633 response
.forEach((plugin
) => {
634 searchPlugins
.push(plugin
);
637 const pluginsHtml
= [];
638 pluginsHtml
.push('<option value="enabled">QBT_TR(Only enabled)QBT_TR[CONTEXT=SearchEngineWidget]</option>');
639 pluginsHtml
.push('<option value="all">QBT_TR(All plugins)QBT_TR[CONTEXT=SearchEngineWidget]</option>');
641 const searchPluginsEmpty
= (searchPlugins
.length
=== 0);
642 if (!searchPluginsEmpty
) {
643 $("searchResultsNoPlugins").style
.display
= "none";
644 if (numSearchTabs() === 0)
645 $("searchResultsNoSearches").style
.display
= "block";
647 // sort plugins alphabetically
648 const allPlugins
= searchPlugins
.sort((left
, right
) => {
649 const leftName
= left
.fullName
;
650 const rightName
= right
.fullName
;
651 return window
.qBittorrent
.Misc
.naturalSortCollator
.compare(leftName
, rightName
);
654 allPlugins
.each((plugin
) => {
655 if (plugin
.enabled
=== true)
656 pluginsHtml
.push("<option value='" + window
.qBittorrent
.Misc
.escapeHtml(plugin
.name
) + "'>" + window
.qBittorrent
.Misc
.escapeHtml(plugin
.fullName
) + "</option>");
659 if (pluginsHtml
.length
> 2)
660 pluginsHtml
.splice(2, 0, "<option disabled>──────────</option>");
663 $("pluginsSelect").set("html", pluginsHtml
.join(""));
665 $("searchPattern").setProperty("disabled", searchPluginsEmpty
);
666 $("categorySelect").setProperty("disabled", searchPluginsEmpty
);
667 $("pluginsSelect").setProperty("disabled", searchPluginsEmpty
);
668 $("startSearchButton").setProperty("disabled", searchPluginsEmpty
);
670 if (window
.qBittorrent
.SearchPlugins
!== undefined)
671 window
.qBittorrent
.SearchPlugins
.updateTable();
679 const getPlugin = function(name
) {
680 for (let i
= 0; i
< searchPlugins
.length
; ++i
) {
681 if (searchPlugins
[i
].name
=== name
)
682 return searchPlugins
[i
];
688 const resetFilters = function() {
689 searchText
.filterPattern
= "";
690 $("searchInNameFilter").set("value", "");
692 searchSeedsFilter
.min
= 0;
693 searchSeedsFilter
.max
= 0;
694 $("searchMinSeedsFilter").set("value", searchSeedsFilter
.min
);
695 $("searchMaxSeedsFilter").set("value", searchSeedsFilter
.max
);
697 searchSizeFilter
.min
= 0.00;
698 searchSizeFilter
.minUnit
= 2; // B = 0, KiB = 1, MiB = 2, GiB = 3, TiB = 4, PiB = 5, EiB = 6
699 searchSizeFilter
.max
= 0.00;
700 searchSizeFilter
.maxUnit
= 3;
701 $("searchMinSizeFilter").set("value", searchSizeFilter
.min
);
702 $("searchMinSizePrefix").set("value", searchSizeFilter
.minUnit
);
703 $("searchMaxSizeFilter").set("value", searchSizeFilter
.max
);
704 $("searchMaxSizePrefix").set("value", searchSizeFilter
.maxUnit
);
707 const getSearchInTorrentName = function() {
708 return $("searchInTorrentName").get("value") === "names" ? "names" : "everywhere";
711 const searchInTorrentName = function() {
712 LocalPreferences
.set("search_in_filter", getSearchInTorrentName());
713 searchFilterChanged();
716 const searchSeedsFilterChanged = function() {
717 searchSeedsFilter
.min
= $("searchMinSeedsFilter").get("value");
718 searchSeedsFilter
.max
= $("searchMaxSeedsFilter").get("value");
720 searchFilterChanged();
723 const searchSizeFilterChanged = function() {
724 searchSizeFilter
.min
= $("searchMinSizeFilter").get("value");
725 searchSizeFilter
.minUnit
= $("searchMinSizePrefix").get("value");
726 searchSizeFilter
.max
= $("searchMaxSizeFilter").get("value");
727 searchSizeFilter
.maxUnit
= $("searchMaxSizePrefix").get("value");
729 searchFilterChanged();
732 const searchSizeFilterPrefixChanged = function() {
733 if ((Number($("searchMinSizeFilter").get("value")) !== 0) || (Number($("searchMaxSizeFilter").get("value")) !== 0))
734 searchSizeFilterChanged();
737 const searchFilterChanged = function() {
738 searchResultsTable
.updateTable();
739 $("numSearchResultsVisible").set("html", searchResultsTable
.getFilteredAndSortedRows().length
);
742 const setupSearchTableEvents = function(enable
) {
744 $$(".searchTableRow").each((target
) => {
745 target
.addEventListener("dblclick", downloadSearchTorrent
, false);
749 $$(".searchTableRow").each((target
) => {
750 target
.removeEventListener("dblclick", downloadSearchTorrent
, false);
755 const loadSearchResultsData = function(searchId
) {
756 const state
= searchState
.get(searchId
);
758 const maxResults
= 500;
759 const url
= new URI("api/v2/search/results");
769 onFailure: function(response
) {
770 if ((response
.status
=== 400) || (response
.status
=== 404)) {
771 // bad params. search id is invalid
772 resetSearchState(searchId
);
773 updateStatusIconElement(searchId
, "QBT_TR(An error occurred during search...)QBT_TR[CONTEXT=SearchJobWidget]", "images/error.svg");
776 clearTimeout(state
.loadResultsTimer
);
777 state
.loadResultsTimer
= loadSearchResultsData
.delay(3000, this, searchId
);
780 onSuccess: function(response
) {
781 $("error_div").set("html", "");
783 const state
= searchState
.get(searchId
);
784 // check if user stopped the search prior to receiving the response
785 if (!state
.running
) {
786 clearTimeout(state
.loadResultsTimer
);
787 updateStatusIconElement(searchId
, "QBT_TR(Search aborted)QBT_TR[CONTEXT=SearchJobWidget]", "images/task-reject.svg");
792 setupSearchTableEvents(false);
794 const state
= searchState
.get(searchId
);
797 if (response
.results
) {
798 const results
= response
.results
;
799 for (let i
= 0; i
< results
.length
; ++i
) {
800 const result
= results
[i
];
803 descrLink
: result
.descrLink
,
804 fileName
: result
.fileName
,
805 fileSize
: result
.fileSize
,
806 fileUrl
: result
.fileUrl
,
807 nbLeechers
: result
.nbLeechers
,
808 nbSeeders
: result
.nbSeeders
,
809 siteUrl
: result
.siteUrl
,
810 pubDate
: result
.pubDate
,
814 state
.rows
.push(row
);
819 // only update table if this search is currently being displayed
820 if (searchId
=== getSelectedSearchId()) {
821 for (const row
of newRows
)
822 searchResultsTable
.updateRowData(row
);
824 $("numSearchResultsVisible").set("html", searchResultsTable
.getFilteredAndSortedRows().length
);
825 $("numSearchResultsTotal").set("html", searchResultsTable
.getRowIds().length
);
827 searchResultsTable
.updateTable();
828 searchResultsTable
.altRow();
831 setupSearchTableEvents(true);
833 if ((response
.status
=== "Stopped") && (state
.rowId
>= response
.total
)) {
834 resetSearchState(searchId
);
835 updateStatusIconElement(searchId
, "QBT_TR(Search has finished)QBT_TR[CONTEXT=SearchJobWidget]", "images/task-complete.svg");
840 clearTimeout(state
.loadResultsTimer
);
841 state
.loadResultsTimer
= loadSearchResultsData
.delay(2000, this, searchId
);
846 const updateSearchResultsData = function(searchId
) {
847 const state
= searchState
.get(searchId
);
848 clearTimeout(state
.loadResultsTimer
);
849 state
.loadResultsTimer
= loadSearchResultsData
.delay(500, this, searchId
);
852 new ClipboardJS(".copySearchDataToClipboard", {
853 text: function(trigger
) {
854 switch (trigger
.id
) {
855 case "copySearchTorrentName":
856 return copySearchTorrentName();
857 case "copySearchTorrentDownloadLink":
858 return copySearchTorrentDownloadLink();
859 case "copySearchTorrentDescriptionUrl":
860 return copySearchTorrentDescriptionUrl();
870 Object
.freeze(window
.qBittorrent
.Search
);