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
= -1;
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").addEventListener("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
129 const elem
= e
.event
.srcElement
;
130 if (elem
.className
.contains("searchInputField")) {
131 document
.getElementById("startSearchButton").click();
136 case "manageSearchPlugins":
137 manageSearchPlugins();
144 // restore search tabs
145 const searchJobs
= JSON
.parse(LocalPreferences
.get("search_jobs", "[]"));
146 for (const { id
, pattern
} of searchJobs
)
147 createSearchTab(id
, pattern
);
150 const numSearchTabs = function() {
151 return $("searchTabs").getElements("li").length
;
154 const getSearchIdFromTab = function(tab
) {
155 return Number(tab
.id
.substring(searchTabIdPrefix
.length
));
158 const createSearchTab = function(searchId
, pattern
) {
159 const newTabId
= `${searchTabIdPrefix}${searchId}`;
160 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(event, this);",
173 closeTabElem
.inject(tabElem
, "top");
175 tabElem
.appendChild(getStatusIconElement("QBT_TR(Searching...)QBT_TR[CONTEXT=SearchJobWidget]", "images/queued.svg"));
177 const listItem
= document
.createElement("li");
178 listItem
.id
= newTabId
;
179 listItem
.classList
.add("selected");
180 listItem
.addEventListener("click", (e
) => {
181 setActiveTab(listItem
);
182 document
.getElementById("startSearchButton").lastChild
.textContent
= "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]";
184 listItem
.appendChild(tabElem
);
185 $("searchTabs").appendChild(listItem
);
187 // unhide the results elements
188 if (numSearchTabs() >= 1) {
189 $("searchResultsNoSearches").style
.display
= "none";
190 $("searchResultsFilters").style
.display
= "block";
191 $("searchResultsTableContainer").style
.display
= "block";
192 $("searchTabsToolbar").style
.display
= "block";
196 setActiveTab(listItem
);
198 searchResultsTable
.clear();
201 searchState
.set(searchId
, {
202 searchPattern
: pattern
,
203 filterPattern
: searchText
.filterPattern
,
204 seedsFilter
: { min
: searchSeedsFilter
.min
, max
: searchSeedsFilter
.max
},
205 sizeFilter
: { min
: searchSizeFilter
.min
, minUnit
: searchSizeFilter
.minUnit
, max
: searchSizeFilter
.max
, maxUnit
: searchSizeFilter
.maxUnit
},
206 searchIn
: getSearchInTorrentName(),
211 loadResultsTimer
: -1,
212 sort
: { column
: searchResultsTable
.sortedColumn
, reverse
: searchResultsTable
.reverseSort
},
214 updateSearchResultsData(searchId
);
217 const closeSearchTab = function(e
, el
) {
220 const tab
= el
.parentElement
.parentElement
;
221 const searchId
= getSearchIdFromTab(tab
);
222 const isTabSelected
= tab
.hasClass("selected");
223 const newTabToSelect
= isTabSelected
? (tab
.nextSibling
|| tab
.previousSibling
) : null;
225 const currentSearchId
= getSelectedSearchId();
226 const state
= searchState
.get(currentSearchId
);
227 // don't bother sending a stop request if already stopped
228 if (state
&& state
.running
)
229 stopSearch(searchId
);
234 url
: new URI("api/v2/search/delete"),
241 const searchJobs
= JSON
.parse(LocalPreferences
.get("search_jobs", "[]"));
242 const jobIndex
= searchJobs
.findIndex((job
) => job
.id
=== searchId
);
244 searchJobs
.splice(jobIndex
, 1);
245 LocalPreferences
.set("search_jobs", JSON
.stringify(searchJobs
));
248 if (numSearchTabs() === 0) {
252 $("numSearchResultsVisible").textContent
= 0;
253 $("numSearchResultsTotal").textContent
= 0;
254 $("searchResultsNoSearches").style
.display
= "block";
255 $("searchResultsFilters").style
.display
= "none";
256 $("searchResultsTableContainer").style
.display
= "none";
257 $("searchTabsToolbar").style
.display
= "none";
259 else if (isTabSelected
&& newTabToSelect
) {
260 setActiveTab(newTabToSelect
);
261 document
.getElementById("startSearchButton").lastChild
.textContent
= "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]";
265 const saveCurrentTabState = function() {
266 const currentSearchId
= getSelectedSearchId();
267 if (!currentSearchId
)
270 const state
= searchState
.get(currentSearchId
);
274 state
.filterPattern
= searchText
.filterPattern
;
275 state
.seedsFilter
= {
276 min
: searchSeedsFilter
.min
,
277 max
: searchSeedsFilter
.max
,
280 min
: searchSizeFilter
.min
,
281 minUnit
: searchSizeFilter
.minUnit
,
282 max
: searchSizeFilter
.max
,
283 maxUnit
: searchSizeFilter
.maxUnit
,
285 state
.searchIn
= getSearchInTorrentName();
288 column
: searchResultsTable
.sortedColumn
,
289 reverse
: searchResultsTable
.reverseSort
,
292 // we must copy the array to avoid taking a reference to it
293 state
.selectedRowIds
= [...searchResultsTable
.selectedRows
];
296 const setActiveTab = function(tab
) {
297 const searchId
= getSearchIdFromTab(tab
);
298 if (searchId
=== getSelectedSearchId())
301 saveCurrentTabState();
303 MochaUI
.selected(tab
, "searchTabs");
305 const state
= searchState
.get(searchId
);
306 let rowsToSelect
= [];
308 // restore table rows
309 searchResultsTable
.clear();
311 for (const row
of state
.rows
)
312 searchResultsTable
.updateRowData(row
);
314 rowsToSelect
= state
.selectedRowIds
;
317 searchText
.pattern
= state
.searchPattern
;
318 searchText
.filterPattern
= state
.filterPattern
;
319 $("searchInNameFilter").value
= state
.filterPattern
;
321 searchSeedsFilter
.min
= state
.seedsFilter
.min
;
322 searchSeedsFilter
.max
= state
.seedsFilter
.max
;
323 $("searchMinSeedsFilter").value
= state
.seedsFilter
.min
;
324 $("searchMaxSeedsFilter").value
= state
.seedsFilter
.max
;
326 searchSizeFilter
.min
= state
.sizeFilter
.min
;
327 searchSizeFilter
.minUnit
= state
.sizeFilter
.minUnit
;
328 searchSizeFilter
.max
= state
.sizeFilter
.max
;
329 searchSizeFilter
.maxUnit
= state
.sizeFilter
.maxUnit
;
330 $("searchMinSizeFilter").value
= state
.sizeFilter
.min
;
331 $("searchMinSizePrefix").value
= state
.sizeFilter
.minUnit
;
332 $("searchMaxSizeFilter").value
= state
.sizeFilter
.max
;
333 $("searchMaxSizePrefix").value
= state
.sizeFilter
.maxUnit
;
335 const currentSearchPattern
= $("searchPattern").value
.trim();
336 if (state
.running
&& (state
.searchPattern
=== currentSearchPattern
)) {
337 // allow search to be stopped
338 document
.getElementById("startSearchButton").lastChild
.textContent
= "QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]";
339 searchPatternChanged
= false;
342 searchResultsTable
.setSortedColumn(state
.sort
.column
, state
.sort
.reverse
);
344 $("searchInTorrentName").value
= state
.searchIn
;
347 // must restore all filters before calling updateTable
348 searchResultsTable
.updateTable();
350 // must reselect rows after calling updateTable
351 if (rowsToSelect
.length
> 0)
352 searchResultsTable
.reselectRows(rowsToSelect
);
354 $("numSearchResultsVisible").textContent
= searchResultsTable
.getFilteredAndSortedRows().length
;
355 $("numSearchResultsTotal").textContent
= 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
.alt
= text
;
377 statusIcon
.title
= text
;
378 statusIcon
.src
= image
;
382 const startSearch = function(pattern
, category
, plugins
) {
383 searchPatternChanged
= false;
385 const url
= new URI("api/v2/search/start");
394 onSuccess
: (response
) => {
395 document
.getElementById("startSearchButton").lastChild
.textContent
= "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").value
.trim();
433 const category
= $("categorySelect").value
;
434 const plugins
= $("pluginsSelect").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]",
496 icon
: "images/qbittorrent-tray.svg",
498 contentURL
: "views/searchplugins.html",
502 paddingHorizontal
: 0,
503 width
: loadWindowWidth(id
, 600),
504 height
: loadWindowHeight(id
, 360),
505 onResize: function() {
508 onBeforeBuild: function() {
511 onClose: function() {
512 clearTimeout(loadSearchPluginsTimer
);
513 loadSearchPluginsTimer
= -1;
519 const loadSearchPlugins = function() {
521 loadSearchPluginsTimer
= loadSearchPlugins
.delay(2000);
524 const onSearchPatternChanged = function() {
525 const currentSearchId
= getSelectedSearchId();
526 const state
= searchState
.get(currentSearchId
);
527 const currentSearchPattern
= $("searchPattern").value
.trim();
528 // start a new search if pattern has changed, otherwise allow the search to be stopped
529 if (state
&& (state
.searchPattern
=== currentSearchPattern
)) {
530 searchPatternChanged
= false;
531 document
.getElementById("startSearchButton").lastChild
.textContent
= "QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]";
534 searchPatternChanged
= true;
535 document
.getElementById("startSearchButton").lastChild
.textContent
= "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]";
539 const categorySelected = function() {
540 selectedCategory
= $("categorySelect").value
;
543 const pluginSelected = function() {
544 selectedPlugin
= $("pluginsSelect").value
;
546 if (selectedPlugin
!== prevSelectedPlugin
) {
547 prevSelectedPlugin
= selectedPlugin
;
548 getSearchCategories();
552 const reselectCategory = function() {
553 for (let i
= 0; i
< $("categorySelect").options
.length
; ++i
) {
554 if ($("categorySelect").options
[i
].get("value") === selectedCategory
)
555 $("categorySelect").options
[i
].selected
= true;
561 const reselectPlugin = function() {
562 for (let i
= 0; i
< $("pluginsSelect").options
.length
; ++i
) {
563 if ($("pluginsSelect").options
[i
].get("value") === selectedPlugin
)
564 $("pluginsSelect").options
[i
].selected
= true;
570 const resetSearchState = function(searchId
) {
571 document
.getElementById("startSearchButton").lastChild
.textContent
= "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]";
572 const state
= searchState
.get(searchId
);
574 state
.running
= false;
575 clearTimeout(state
.loadResultsTimer
);
576 state
.loadResultsTimer
= -1;
580 const getSearchCategories
= () => {
581 const populateCategorySelect
= (categories
) => {
582 const categoryOptions
= [];
584 for (const category
of categories
) {
585 const option
= document
.createElement("option");
586 option
.value
= category
.id
;
587 option
.textContent
= category
.name
;
588 categoryOptions
.push(option
);
591 // first category is "All Categories"
592 if (categoryOptions
.length
> 1) {
594 const option
= document
.createElement("option");
595 option
.disabled
= true;
596 option
.textContent
= "──────────";
597 categoryOptions
.splice(1, 0, option
);
600 $("categorySelect").replaceChildren(...categoryOptions
);
603 const selectedPlugin
= $("pluginsSelect").value
;
605 if ((selectedPlugin
=== "all") || (selectedPlugin
=== "enabled")) {
606 const uniqueCategories
= {};
607 for (const plugin
of searchPlugins
) {
608 if ((selectedPlugin
=== "enabled") && !plugin
.enabled
)
610 for (const category
of plugin
.supportedCategories
) {
611 if (uniqueCategories
[category
.id
] === undefined)
612 uniqueCategories
[category
.id
] = category
;
615 // we must sort the ids to maintain consistent order.
616 const categories
= Object
.keys(uniqueCategories
).sort().map(id
=> uniqueCategories
[id
]);
617 populateCategorySelect(categories
);
620 const plugin
= getPlugin(selectedPlugin
);
621 const plugins
= (plugin
=== null) ? [] : plugin
.supportedCategories
;
622 populateCategorySelect(plugins
);
628 const getPlugins = function() {
630 url
: new URI("api/v2/search/plugins"),
633 onSuccess
: (response
) => {
634 const createOption
= (text
, value
, disabled
= false) => {
635 const option
= document
.createElement("option");
636 if (value
!== undefined)
637 option
.value
= value
;
638 option
.textContent
= text
;
639 option
.disabled
= disabled
;
643 if (response
!== prevSearchPluginsResponse
) {
644 prevSearchPluginsResponse
= response
;
645 searchPlugins
.length
= 0;
646 response
.forEach((plugin
) => {
647 searchPlugins
.push(plugin
);
650 const pluginOptions
= [];
651 pluginOptions
.push(createOption("QBT_TR(Only enabled)QBT_TR[CONTEXT=SearchEngineWidget]", "enabled"));
652 pluginOptions
.push(createOption("QBT_TR(All plugins)QBT_TR[CONTEXT=SearchEngineWidget]", "all"));
654 const searchPluginsEmpty
= (searchPlugins
.length
=== 0);
655 if (!searchPluginsEmpty
) {
656 $("searchResultsNoPlugins").style
.display
= "none";
657 if (numSearchTabs() === 0)
658 $("searchResultsNoSearches").style
.display
= "block";
660 // sort plugins alphabetically
661 const allPlugins
= searchPlugins
.sort((left
, right
) => {
662 const leftName
= left
.fullName
;
663 const rightName
= right
.fullName
;
664 return window
.qBittorrent
.Misc
.naturalSortCollator
.compare(leftName
, rightName
);
667 allPlugins
.each((plugin
) => {
668 if (plugin
.enabled
=== true)
669 pluginOptions
.push(createOption(plugin
.fullName
, plugin
.name
));
672 if (pluginOptions
.length
> 2)
673 pluginOptions
.splice(2, 0, createOption("──────────", undefined, true));
676 $("pluginsSelect").replaceChildren(...pluginOptions
);
678 $("searchPattern").disabled
= searchPluginsEmpty
;
679 $("categorySelect").disabled
= searchPluginsEmpty
;
680 $("pluginsSelect").disabled
= searchPluginsEmpty
;
681 document
.getElementById("startSearchButton").disabled
= searchPluginsEmpty
;
683 if (window
.qBittorrent
.SearchPlugins
!== undefined)
684 window
.qBittorrent
.SearchPlugins
.updateTable();
692 const getPlugin = function(name
) {
693 for (let i
= 0; i
< searchPlugins
.length
; ++i
) {
694 if (searchPlugins
[i
].name
=== name
)
695 return searchPlugins
[i
];
701 const resetFilters = function() {
702 searchText
.filterPattern
= "";
703 $("searchInNameFilter").value
= "";
705 searchSeedsFilter
.min
= 0;
706 searchSeedsFilter
.max
= 0;
707 $("searchMinSeedsFilter").value
= searchSeedsFilter
.min
;
708 $("searchMaxSeedsFilter").value
= searchSeedsFilter
.max
;
710 searchSizeFilter
.min
= 0.00;
711 searchSizeFilter
.minUnit
= 2; // B = 0, KiB = 1, MiB = 2, GiB = 3, TiB = 4, PiB = 5, EiB = 6
712 searchSizeFilter
.max
= 0.00;
713 searchSizeFilter
.maxUnit
= 3;
714 $("searchMinSizeFilter").value
= searchSizeFilter
.min
;
715 $("searchMinSizePrefix").value
= searchSizeFilter
.minUnit
;
716 $("searchMaxSizeFilter").value
= searchSizeFilter
.max
;
717 $("searchMaxSizePrefix").value
= searchSizeFilter
.maxUnit
;
720 const getSearchInTorrentName = function() {
721 return ($("searchInTorrentName").value
=== "names") ? "names" : "everywhere";
724 const searchInTorrentName = function() {
725 LocalPreferences
.set("search_in_filter", getSearchInTorrentName());
726 searchFilterChanged();
729 const searchSeedsFilterChanged = function() {
730 searchSeedsFilter
.min
= $("searchMinSeedsFilter").value
;
731 searchSeedsFilter
.max
= $("searchMaxSeedsFilter").value
;
733 searchFilterChanged();
736 const searchSizeFilterChanged = function() {
737 searchSizeFilter
.min
= $("searchMinSizeFilter").value
;
738 searchSizeFilter
.minUnit
= $("searchMinSizePrefix").value
;
739 searchSizeFilter
.max
= $("searchMaxSizeFilter").value
;
740 searchSizeFilter
.maxUnit
= $("searchMaxSizePrefix").value
;
742 searchFilterChanged();
745 const searchSizeFilterPrefixChanged = function() {
746 if ((Number($("searchMinSizeFilter").value
) !== 0) || (Number($("searchMaxSizeFilter").value
) !== 0))
747 searchSizeFilterChanged();
750 const searchFilterChanged = function() {
751 searchResultsTable
.updateTable();
752 $("numSearchResultsVisible").textContent
= searchResultsTable
.getFilteredAndSortedRows().length
;
755 const setupSearchTableEvents = function(enable
) {
757 $$(".searchTableRow").each((target
) => {
758 target
.addEventListener("dblclick", downloadSearchTorrent
, false);
762 $$(".searchTableRow").each((target
) => {
763 target
.removeEventListener("dblclick", downloadSearchTorrent
, false);
768 const loadSearchResultsData = function(searchId
) {
769 const state
= searchState
.get(searchId
);
771 const maxResults
= 500;
772 const url
= new URI("api/v2/search/results");
782 onFailure: function(response
) {
783 if ((response
.status
=== 400) || (response
.status
=== 404)) {
784 // bad params. search id is invalid
785 resetSearchState(searchId
);
786 updateStatusIconElement(searchId
, "QBT_TR(An error occurred during search...)QBT_TR[CONTEXT=SearchJobWidget]", "images/error.svg");
789 clearTimeout(state
.loadResultsTimer
);
790 state
.loadResultsTimer
= loadSearchResultsData
.delay(3000, this, searchId
);
793 onSuccess: function(response
) {
794 $("error_div").textContent
= "";
796 const state
= searchState
.get(searchId
);
797 // check if user stopped the search prior to receiving the response
798 if (!state
.running
) {
799 clearTimeout(state
.loadResultsTimer
);
800 updateStatusIconElement(searchId
, "QBT_TR(Search aborted)QBT_TR[CONTEXT=SearchJobWidget]", "images/task-reject.svg");
805 setupSearchTableEvents(false);
807 const state
= searchState
.get(searchId
);
810 if (response
.results
) {
811 const results
= response
.results
;
812 for (let i
= 0; i
< results
.length
; ++i
) {
813 const result
= results
[i
];
816 descrLink
: result
.descrLink
,
817 fileName
: result
.fileName
,
818 fileSize
: result
.fileSize
,
819 fileUrl
: result
.fileUrl
,
820 nbLeechers
: result
.nbLeechers
,
821 nbSeeders
: result
.nbSeeders
,
822 siteUrl
: result
.siteUrl
,
823 pubDate
: result
.pubDate
,
827 state
.rows
.push(row
);
832 // only update table if this search is currently being displayed
833 if (searchId
=== getSelectedSearchId()) {
834 for (const row
of newRows
)
835 searchResultsTable
.updateRowData(row
);
837 $("numSearchResultsVisible").textContent
= searchResultsTable
.getFilteredAndSortedRows().length
;
838 $("numSearchResultsTotal").textContent
= searchResultsTable
.getRowIds().length
;
840 searchResultsTable
.updateTable();
843 setupSearchTableEvents(true);
845 if ((response
.status
=== "Stopped") && (state
.rowId
>= response
.total
)) {
846 resetSearchState(searchId
);
847 updateStatusIconElement(searchId
, "QBT_TR(Search has finished)QBT_TR[CONTEXT=SearchJobWidget]", "images/task-complete.svg");
852 clearTimeout(state
.loadResultsTimer
);
853 state
.loadResultsTimer
= loadSearchResultsData
.delay(2000, this, searchId
);
858 const updateSearchResultsData = function(searchId
) {
859 const state
= searchState
.get(searchId
);
860 clearTimeout(state
.loadResultsTimer
);
861 state
.loadResultsTimer
= loadSearchResultsData
.delay(500, this, searchId
);
864 new ClipboardJS(".copySearchDataToClipboard", {
865 text: function(trigger
) {
866 switch (trigger
.id
) {
867 case "copySearchTorrentName":
868 return copySearchTorrentName();
869 case "copySearchTorrentDownloadLink":
870 return copySearchTorrentDownloadLink();
871 case "copySearchTorrentDescriptionUrl":
872 return copySearchTorrentDescriptionUrl();
881 Object
.freeze(window
.qBittorrent
.Search
);