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 searchResultsTabsContextMenu
= new window
.qBittorrent
.ContextMenu
.ContextMenu({
90 targets
: ".searchTab",
91 menu
: "searchResultsTabsMenu",
93 closeTab
: (tab
) => { closeSearchTab(tab
); },
95 for (const tab
of document
.querySelectorAll("#searchTabs .searchTab"))
104 setActiveTab(this.options
.element
);
109 // load "Search in" preference from local storage
110 $("searchInTorrentName").value
= (LocalPreferences
.get("search_in_filter") === "names") ? "names" : "everywhere";
111 const searchResultsTableContextMenu
= new window
.qBittorrent
.ContextMenu
.ContextMenu({
112 targets
: "#searchResultsTableDiv tr",
113 menu
: "searchResultsTableMenu",
115 Download
: downloadSearchTorrent
,
116 OpenDescriptionUrl
: openSearchTorrentDescriptionUrl
123 searchResultsTable
= new window
.qBittorrent
.DynamicTable
.SearchResultsTable();
124 searchResultsTable
.setup("searchResultsTableDiv", "searchResultsTableFixedHeaderDiv", searchResultsTableContextMenu
);
127 searchResultsTable
.dynamicTableDiv
.addEventListener("dblclick", (e
) => { downloadSearchTorrent(); });
129 // listen for changes to searchInNameFilter
130 let searchInNameFilterTimer
= -1;
131 $("searchInNameFilter").addEventListener("input", () => {
132 clearTimeout(searchInNameFilterTimer
);
133 searchInNameFilterTimer
= setTimeout(() => {
134 searchInNameFilterTimer
= -1;
136 const value
= $("searchInNameFilter").value
;
137 searchText
.filterPattern
= value
;
138 searchFilterChanged();
139 }, window
.qBittorrent
.Misc
.FILTER_INPUT_DELAY
);
143 defaultEventType
: "keydown",
145 "Enter": function(e
) {
146 // accept enter key as a click
150 const elem
= e
.event
.srcElement
;
151 if (elem
.className
.contains("searchInputField")) {
152 document
.getElementById("startSearchButton").click();
157 case "manageSearchPlugins":
158 manageSearchPlugins();
165 // restore search tabs
166 const searchJobs
= JSON
.parse(LocalPreferences
.get("search_jobs", "[]"));
167 for (const { id
, pattern
} of searchJobs
)
168 createSearchTab(id
, pattern
);
171 const numSearchTabs
= () => {
172 return $("searchTabs").getElements("li").length
;
175 const getSearchIdFromTab
= (tab
) => {
176 return Number(tab
.id
.substring(searchTabIdPrefix
.length
));
179 const createSearchTab
= (searchId
, pattern
) => {
180 const newTabId
= `${searchTabIdPrefix}${searchId}`;
181 const tabElem
= new Element("a", {
185 const closeTabElem
= new Element("img", {
186 alt
: "QBT_TR(Close tab)QBT_TR[CONTEXT=SearchWidget]",
187 title
: "QBT_TR(Close tab)QBT_TR[CONTEXT=SearchWidget]",
188 src
: "images/application-exit.svg",
191 onclick
: "qBittorrent.Search.closeSearchTab(this);",
193 closeTabElem
.inject(tabElem
, "top");
195 tabElem
.appendChild(getStatusIconElement("QBT_TR(Searching...)QBT_TR[CONTEXT=SearchJobWidget]", "images/queued.svg"));
197 const listItem
= document
.createElement("li");
198 listItem
.id
= newTabId
;
199 listItem
.classList
.add("selected", "searchTab");
200 listItem
.addEventListener("click", (e
) => {
201 setActiveTab(listItem
);
202 document
.getElementById("startSearchButton").lastChild
.textContent
= "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]";
204 listItem
.appendChild(tabElem
);
205 $("searchTabs").appendChild(listItem
);
206 searchResultsTabsContextMenu
.addTarget(listItem
);
208 // unhide the results elements
209 if (numSearchTabs() >= 1) {
210 $("searchResultsNoSearches").classList
.add("invisible");
211 $("searchResultsFilters").classList
.remove("invisible");
212 $("searchResultsTableContainer").classList
.remove("invisible");
213 $("searchTabsToolbar").classList
.remove("invisible");
217 setActiveTab(listItem
);
219 searchResultsTable
.clear();
222 searchState
.set(searchId
, {
223 searchPattern
: pattern
,
224 filterPattern
: searchText
.filterPattern
,
225 seedsFilter
: { min
: searchSeedsFilter
.min
, max
: searchSeedsFilter
.max
},
226 sizeFilter
: { min
: searchSizeFilter
.min
, minUnit
: searchSizeFilter
.minUnit
, max
: searchSizeFilter
.max
, maxUnit
: searchSizeFilter
.maxUnit
},
227 searchIn
: getSearchInTorrentName(),
232 loadResultsTimer
: -1,
233 sort
: { column
: searchResultsTable
.sortedColumn
, reverse
: searchResultsTable
.reverseSort
},
235 updateSearchResultsData(searchId
);
238 const closeSearchTab
= (el
) => {
239 const tab
= el
.closest("li.searchTab");
243 const searchId
= getSearchIdFromTab(tab
);
244 const isTabSelected
= tab
.hasClass("selected");
245 const newTabToSelect
= isTabSelected
? (tab
.nextSibling
|| tab
.previousSibling
) : null;
247 const currentSearchId
= getSelectedSearchId();
248 const state
= searchState
.get(currentSearchId
);
249 // don't bother sending a stop request if already stopped
250 if (state
&& state
.running
)
251 stopSearch(searchId
);
256 url
: new URI("api/v2/search/delete"),
263 const searchJobs
= JSON
.parse(LocalPreferences
.get("search_jobs", "[]"));
264 const jobIndex
= searchJobs
.findIndex((job
) => job
.id
=== searchId
);
266 searchJobs
.splice(jobIndex
, 1);
267 LocalPreferences
.set("search_jobs", JSON
.stringify(searchJobs
));
270 if (numSearchTabs() === 0) {
274 $("numSearchResultsVisible").textContent
= 0;
275 $("numSearchResultsTotal").textContent
= 0;
276 $("searchResultsNoSearches").classList
.remove("invisible");
277 $("searchResultsFilters").classList
.add("invisible");
278 $("searchResultsTableContainer").classList
.add("invisible");
279 $("searchTabsToolbar").classList
.add("invisible");
281 else if (isTabSelected
&& newTabToSelect
) {
282 setActiveTab(newTabToSelect
);
283 document
.getElementById("startSearchButton").lastChild
.textContent
= "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]";
287 const saveCurrentTabState
= () => {
288 const currentSearchId
= getSelectedSearchId();
289 if (!currentSearchId
)
292 const state
= searchState
.get(currentSearchId
);
296 state
.filterPattern
= searchText
.filterPattern
;
297 state
.seedsFilter
= {
298 min
: searchSeedsFilter
.min
,
299 max
: searchSeedsFilter
.max
,
302 min
: searchSizeFilter
.min
,
303 minUnit
: searchSizeFilter
.minUnit
,
304 max
: searchSizeFilter
.max
,
305 maxUnit
: searchSizeFilter
.maxUnit
,
307 state
.searchIn
= getSearchInTorrentName();
310 column
: searchResultsTable
.sortedColumn
,
311 reverse
: searchResultsTable
.reverseSort
,
314 // we must copy the array to avoid taking a reference to it
315 state
.selectedRowIds
= [...searchResultsTable
.selectedRows
];
318 const setActiveTab
= (tab
) => {
319 const searchId
= getSearchIdFromTab(tab
);
320 if (searchId
=== getSelectedSearchId())
323 saveCurrentTabState();
325 MochaUI
.selected(tab
, "searchTabs");
327 const state
= searchState
.get(searchId
);
328 let rowsToSelect
= [];
330 // restore table rows
331 searchResultsTable
.clear();
333 for (const row
of state
.rows
)
334 searchResultsTable
.updateRowData(row
);
336 rowsToSelect
= state
.selectedRowIds
;
339 searchText
.pattern
= state
.searchPattern
;
340 searchText
.filterPattern
= state
.filterPattern
;
341 $("searchInNameFilter").value
= state
.filterPattern
;
343 searchSeedsFilter
.min
= state
.seedsFilter
.min
;
344 searchSeedsFilter
.max
= state
.seedsFilter
.max
;
345 $("searchMinSeedsFilter").value
= state
.seedsFilter
.min
;
346 $("searchMaxSeedsFilter").value
= state
.seedsFilter
.max
;
348 searchSizeFilter
.min
= state
.sizeFilter
.min
;
349 searchSizeFilter
.minUnit
= state
.sizeFilter
.minUnit
;
350 searchSizeFilter
.max
= state
.sizeFilter
.max
;
351 searchSizeFilter
.maxUnit
= state
.sizeFilter
.maxUnit
;
352 $("searchMinSizeFilter").value
= state
.sizeFilter
.min
;
353 $("searchMinSizePrefix").value
= state
.sizeFilter
.minUnit
;
354 $("searchMaxSizeFilter").value
= state
.sizeFilter
.max
;
355 $("searchMaxSizePrefix").value
= state
.sizeFilter
.maxUnit
;
357 const currentSearchPattern
= $("searchPattern").value
.trim();
358 if (state
.running
&& (state
.searchPattern
=== currentSearchPattern
)) {
359 // allow search to be stopped
360 document
.getElementById("startSearchButton").lastChild
.textContent
= "QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]";
361 searchPatternChanged
= false;
364 searchResultsTable
.setSortedColumn(state
.sort
.column
, state
.sort
.reverse
);
366 $("searchInTorrentName").value
= state
.searchIn
;
369 // must restore all filters before calling updateTable
370 searchResultsTable
.updateTable();
372 // must reselect rows after calling updateTable
373 if (rowsToSelect
.length
> 0)
374 searchResultsTable
.reselectRows(rowsToSelect
);
376 $("numSearchResultsVisible").textContent
= searchResultsTable
.getFilteredAndSortedRows().length
;
377 $("numSearchResultsTotal").textContent
= searchResultsTable
.getRowSize();
380 const getStatusIconElement
= (text
, image
) => {
381 return new Element("img", {
391 const updateStatusIconElement
= (searchId
, text
, image
) => {
392 const searchTab
= $(`${searchTabIdPrefix}${searchId}`);
394 const statusIcon
= searchTab
.getElement(".statusIcon");
395 statusIcon
.alt
= text
;
396 statusIcon
.title
= text
;
397 statusIcon
.src
= image
;
401 const startSearch
= (pattern
, category
, plugins
) => {
402 searchPatternChanged
= false;
404 const url
= new URI("api/v2/search/start");
413 onSuccess
: (response
) => {
414 document
.getElementById("startSearchButton").lastChild
.textContent
= "QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]";
415 const searchId
= response
.id
;
416 createSearchTab(searchId
, pattern
);
418 const searchJobs
= JSON
.parse(LocalPreferences
.get("search_jobs", "[]"));
419 searchJobs
.push({ id
: searchId
, pattern
: pattern
});
420 LocalPreferences
.set("search_jobs", JSON
.stringify(searchJobs
));
425 const stopSearch
= (searchId
) => {
426 const url
= new URI("api/v2/search/stop");
433 onSuccess
: (response
) => {
434 resetSearchState(searchId
);
435 // not strictly necessary to do this when the tab is being closed, but there's no harm in it
436 updateStatusIconElement(searchId
, "QBT_TR(Search aborted)QBT_TR[CONTEXT=SearchJobWidget]", "images/task-reject.svg");
441 const getSelectedSearchId
= () => {
442 const selectedTab
= $("searchTabs").getElement("li.selected");
443 return selectedTab
? getSearchIdFromTab(selectedTab
) : null;
446 const startStopSearch
= () => {
447 const currentSearchId
= getSelectedSearchId();
448 const state
= searchState
.get(currentSearchId
);
449 const isSearchRunning
= state
&& state
.running
;
450 if (!isSearchRunning
|| searchPatternChanged
) {
451 const pattern
= $("searchPattern").value
.trim();
452 const category
= $("categorySelect").value
;
453 const plugins
= $("pluginsSelect").value
;
455 if (!pattern
|| !category
|| !plugins
)
458 searchText
.pattern
= pattern
;
459 startSearch(pattern
, category
, plugins
);
462 stopSearch(currentSearchId
);
466 const openSearchTorrentDescriptionUrl
= () => {
467 for (const rowID
of searchResultsTable
.selectedRowsIds())
468 window
.open(searchResultsTable
.getRow(rowID
).full_data
.descrLink
, "_blank");
471 const copySearchTorrentName
= () => {
473 searchResultsTable
.selectedRowsIds().each((rowId
) => {
474 names
.push(searchResultsTable
.getRow(rowId
).full_data
.fileName
);
476 return names
.join("\n");
479 const copySearchTorrentDownloadLink
= () => {
481 searchResultsTable
.selectedRowsIds().each((rowId
) => {
482 urls
.push(searchResultsTable
.getRow(rowId
).full_data
.fileUrl
);
484 return urls
.join("\n");
487 const copySearchTorrentDescriptionUrl
= () => {
489 searchResultsTable
.selectedRowsIds().each((rowId
) => {
490 urls
.push(searchResultsTable
.getRow(rowId
).full_data
.descrLink
);
492 return urls
.join("\n");
495 const downloadSearchTorrent
= () => {
497 for (const rowID
of searchResultsTable
.selectedRowsIds())
498 urls
.push(searchResultsTable
.getRow(rowID
).full_data
.fileUrl
);
500 // only proceed if at least 1 row was selected
504 showDownloadPage(urls
);
507 const manageSearchPlugins
= () => {
508 const id
= "searchPlugins";
512 title
: "QBT_TR(Search plugins)QBT_TR[CONTEXT=PluginSelectDlg]",
513 icon
: "images/qbittorrent-tray.svg",
515 contentURL
: "views/searchplugins.html",
519 paddingHorizontal
: 0,
520 width
: loadWindowWidth(id
, 600),
521 height
: loadWindowHeight(id
, 360),
522 onResize
: window
.qBittorrent
.Misc
.createDebounceHandler(500, (e
) => {
525 onBeforeBuild
: () => {
529 clearTimeout(loadSearchPluginsTimer
);
530 loadSearchPluginsTimer
= -1;
536 const loadSearchPlugins
= () => {
538 loadSearchPluginsTimer
= loadSearchPlugins
.delay(2000);
541 const onSearchPatternChanged
= () => {
542 const currentSearchId
= getSelectedSearchId();
543 const state
= searchState
.get(currentSearchId
);
544 const currentSearchPattern
= $("searchPattern").value
.trim();
545 // start a new search if pattern has changed, otherwise allow the search to be stopped
546 if (state
&& (state
.searchPattern
=== currentSearchPattern
)) {
547 searchPatternChanged
= false;
548 document
.getElementById("startSearchButton").lastChild
.textContent
= "QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]";
551 searchPatternChanged
= true;
552 document
.getElementById("startSearchButton").lastChild
.textContent
= "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]";
556 const categorySelected
= () => {
557 selectedCategory
= $("categorySelect").value
;
560 const pluginSelected
= () => {
561 selectedPlugin
= $("pluginsSelect").value
;
563 if (selectedPlugin
!== prevSelectedPlugin
) {
564 prevSelectedPlugin
= selectedPlugin
;
565 getSearchCategories();
569 const reselectCategory
= () => {
570 for (let i
= 0; i
< $("categorySelect").options
.length
; ++i
) {
571 if ($("categorySelect").options
[i
].get("value") === selectedCategory
)
572 $("categorySelect").options
[i
].selected
= true;
578 const reselectPlugin
= () => {
579 for (let i
= 0; i
< $("pluginsSelect").options
.length
; ++i
) {
580 if ($("pluginsSelect").options
[i
].get("value") === selectedPlugin
)
581 $("pluginsSelect").options
[i
].selected
= true;
587 const resetSearchState
= (searchId
) => {
588 document
.getElementById("startSearchButton").lastChild
.textContent
= "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]";
589 const state
= searchState
.get(searchId
);
591 state
.running
= false;
592 clearTimeout(state
.loadResultsTimer
);
593 state
.loadResultsTimer
= -1;
597 const getSearchCategories
= () => {
598 const populateCategorySelect
= (categories
) => {
599 const categoryOptions
= [];
601 for (const category
of categories
) {
602 const option
= document
.createElement("option");
603 option
.value
= category
.id
;
604 option
.textContent
= category
.name
;
605 categoryOptions
.push(option
);
608 // first category is "All Categories"
609 if (categoryOptions
.length
> 1) {
611 const option
= document
.createElement("option");
612 option
.disabled
= true;
613 option
.textContent
= "──────────";
614 categoryOptions
.splice(1, 0, option
);
617 $("categorySelect").replaceChildren(...categoryOptions
);
620 const selectedPlugin
= $("pluginsSelect").value
;
622 if ((selectedPlugin
=== "all") || (selectedPlugin
=== "enabled")) {
623 const uniqueCategories
= {};
624 for (const plugin
of searchPlugins
) {
625 if ((selectedPlugin
=== "enabled") && !plugin
.enabled
)
627 for (const category
of plugin
.supportedCategories
) {
628 if (uniqueCategories
[category
.id
] === undefined)
629 uniqueCategories
[category
.id
] = category
;
632 // we must sort the ids to maintain consistent order.
633 const categories
= Object
.keys(uniqueCategories
).sort().map(id
=> uniqueCategories
[id
]);
634 populateCategorySelect(categories
);
637 const plugin
= getPlugin(selectedPlugin
);
638 const plugins
= (plugin
=== null) ? [] : plugin
.supportedCategories
;
639 populateCategorySelect(plugins
);
645 const getPlugins
= () => {
647 url
: new URI("api/v2/search/plugins"),
650 onSuccess
: (response
) => {
651 const createOption
= (text
, value
, disabled
= false) => {
652 const option
= document
.createElement("option");
653 if (value
!== undefined)
654 option
.value
= value
;
655 option
.textContent
= text
;
656 option
.disabled
= disabled
;
660 if (response
!== prevSearchPluginsResponse
) {
661 prevSearchPluginsResponse
= response
;
662 searchPlugins
.length
= 0;
663 response
.forEach((plugin
) => {
664 searchPlugins
.push(plugin
);
667 const pluginOptions
= [];
668 pluginOptions
.push(createOption("QBT_TR(Only enabled)QBT_TR[CONTEXT=SearchEngineWidget]", "enabled"));
669 pluginOptions
.push(createOption("QBT_TR(All plugins)QBT_TR[CONTEXT=SearchEngineWidget]", "all"));
671 const searchPluginsEmpty
= (searchPlugins
.length
=== 0);
672 if (!searchPluginsEmpty
) {
673 $("searchResultsNoPlugins").classList
.add("invisible");
674 if (numSearchTabs() === 0)
675 $("searchResultsNoSearches").classList
.remove("invisible");
677 // sort plugins alphabetically
678 const allPlugins
= searchPlugins
.sort((left
, right
) => {
679 const leftName
= left
.fullName
;
680 const rightName
= right
.fullName
;
681 return window
.qBittorrent
.Misc
.naturalSortCollator
.compare(leftName
, rightName
);
684 allPlugins
.each((plugin
) => {
685 if (plugin
.enabled
=== true)
686 pluginOptions
.push(createOption(plugin
.fullName
, plugin
.name
));
689 if (pluginOptions
.length
> 2)
690 pluginOptions
.splice(2, 0, createOption("──────────", undefined, true));
693 $("pluginsSelect").replaceChildren(...pluginOptions
);
695 $("searchPattern").disabled
= searchPluginsEmpty
;
696 $("categorySelect").disabled
= searchPluginsEmpty
;
697 $("pluginsSelect").disabled
= searchPluginsEmpty
;
698 document
.getElementById("startSearchButton").disabled
= searchPluginsEmpty
;
700 if (window
.qBittorrent
.SearchPlugins
!== undefined)
701 window
.qBittorrent
.SearchPlugins
.updateTable();
709 const getPlugin
= (name
) => {
710 for (let i
= 0; i
< searchPlugins
.length
; ++i
) {
711 if (searchPlugins
[i
].name
=== name
)
712 return searchPlugins
[i
];
718 const resetFilters
= () => {
719 searchText
.filterPattern
= "";
720 $("searchInNameFilter").value
= "";
722 searchSeedsFilter
.min
= 0;
723 searchSeedsFilter
.max
= 0;
724 $("searchMinSeedsFilter").value
= searchSeedsFilter
.min
;
725 $("searchMaxSeedsFilter").value
= searchSeedsFilter
.max
;
727 searchSizeFilter
.min
= 0.00;
728 searchSizeFilter
.minUnit
= 2; // B = 0, KiB = 1, MiB = 2, GiB = 3, TiB = 4, PiB = 5, EiB = 6
729 searchSizeFilter
.max
= 0.00;
730 searchSizeFilter
.maxUnit
= 3;
731 $("searchMinSizeFilter").value
= searchSizeFilter
.min
;
732 $("searchMinSizePrefix").value
= searchSizeFilter
.minUnit
;
733 $("searchMaxSizeFilter").value
= searchSizeFilter
.max
;
734 $("searchMaxSizePrefix").value
= searchSizeFilter
.maxUnit
;
737 const getSearchInTorrentName
= () => {
738 return ($("searchInTorrentName").value
=== "names") ? "names" : "everywhere";
741 const searchInTorrentName
= () => {
742 LocalPreferences
.set("search_in_filter", getSearchInTorrentName());
743 searchFilterChanged();
746 const searchSeedsFilterChanged
= () => {
747 searchSeedsFilter
.min
= $("searchMinSeedsFilter").value
;
748 searchSeedsFilter
.max
= $("searchMaxSeedsFilter").value
;
750 searchFilterChanged();
753 const searchSizeFilterChanged
= () => {
754 searchSizeFilter
.min
= $("searchMinSizeFilter").value
;
755 searchSizeFilter
.minUnit
= $("searchMinSizePrefix").value
;
756 searchSizeFilter
.max
= $("searchMaxSizeFilter").value
;
757 searchSizeFilter
.maxUnit
= $("searchMaxSizePrefix").value
;
759 searchFilterChanged();
762 const searchSizeFilterPrefixChanged
= () => {
763 if ((Number($("searchMinSizeFilter").value
) !== 0) || (Number($("searchMaxSizeFilter").value
) !== 0))
764 searchSizeFilterChanged();
767 const searchFilterChanged
= () => {
768 searchResultsTable
.updateTable();
769 $("numSearchResultsVisible").textContent
= searchResultsTable
.getFilteredAndSortedRows().length
;
772 const loadSearchResultsData = function(searchId
) {
773 const state
= searchState
.get(searchId
);
775 const maxResults
= 500;
776 const url
= new URI("api/v2/search/results");
786 onFailure: function(response
) {
787 if ((response
.status
=== 400) || (response
.status
=== 404)) {
788 // bad params. search id is invalid
789 resetSearchState(searchId
);
790 updateStatusIconElement(searchId
, "QBT_TR(An error occurred during search...)QBT_TR[CONTEXT=SearchJobWidget]", "images/error.svg");
793 clearTimeout(state
.loadResultsTimer
);
794 state
.loadResultsTimer
= loadSearchResultsData
.delay(3000, this, searchId
);
797 onSuccess: function(response
) {
798 $("error_div").textContent
= "";
800 const state
= searchState
.get(searchId
);
801 // check if user stopped the search prior to receiving the response
802 if (!state
.running
) {
803 clearTimeout(state
.loadResultsTimer
);
804 updateStatusIconElement(searchId
, "QBT_TR(Search aborted)QBT_TR[CONTEXT=SearchJobWidget]", "images/task-reject.svg");
809 const state
= searchState
.get(searchId
);
812 if (response
.results
) {
813 const results
= response
.results
;
814 for (let i
= 0; i
< results
.length
; ++i
) {
815 const result
= results
[i
];
818 descrLink
: result
.descrLink
,
819 fileName
: result
.fileName
,
820 fileSize
: result
.fileSize
,
821 fileUrl
: result
.fileUrl
,
822 nbLeechers
: result
.nbLeechers
,
823 nbSeeders
: result
.nbSeeders
,
824 engineName
: result
.engineName
,
825 siteUrl
: result
.siteUrl
,
826 pubDate
: result
.pubDate
,
830 state
.rows
.push(row
);
835 // only update table if this search is currently being displayed
836 if (searchId
=== getSelectedSearchId()) {
837 for (const row
of newRows
)
838 searchResultsTable
.updateRowData(row
);
840 $("numSearchResultsVisible").textContent
= searchResultsTable
.getFilteredAndSortedRows().length
;
841 $("numSearchResultsTotal").textContent
= searchResultsTable
.getRowSize();
843 searchResultsTable
.updateTable();
846 if ((response
.status
=== "Stopped") && (state
.rowId
>= response
.total
)) {
847 resetSearchState(searchId
);
848 updateStatusIconElement(searchId
, "QBT_TR(Search has finished)QBT_TR[CONTEXT=SearchJobWidget]", "images/task-complete.svg");
853 clearTimeout(state
.loadResultsTimer
);
854 state
.loadResultsTimer
= loadSearchResultsData
.delay(2000, this, searchId
);
859 const updateSearchResultsData = function(searchId
) {
860 const state
= searchState
.get(searchId
);
861 clearTimeout(state
.loadResultsTimer
);
862 state
.loadResultsTimer
= loadSearchResultsData
.delay(500, this, searchId
);
865 new ClipboardJS(".copySearchDataToClipboard", {
867 switch (trigger
.id
) {
868 case "copySearchTorrentName":
869 return copySearchTorrentName();
870 case "copySearchTorrentDownloadLink":
871 return copySearchTorrentDownloadLink();
872 case "copySearchTorrentDescriptionUrl":
873 return copySearchTorrentDescriptionUrl();
882 Object
.freeze(window
.qBittorrent
.Search
);