Use enabled search plugins by default in WebUI
[qBittorrent.git] / src / webui / www / private / scripts / search.js
blobc8611e3f6152f9882bd4cc1061cb87f879b30b88
1 /*
2 * MIT License
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
21 * THE SOFTWARE.
24 "use strict";
26 if (window.qBittorrent === undefined)
27 window.qBittorrent = {};
29 window.qBittorrent.Search = (function() {
30 const exports = function() {
31 return {
32 startStopSearch: startStopSearch,
33 manageSearchPlugins: manageSearchPlugins,
34 searchPlugins: searchPlugins,
35 searchText: searchText,
36 searchSeedsFilter: searchSeedsFilter,
37 searchSizeFilter: searchSizeFilter,
38 init: init,
39 getPlugin: getPlugin,
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},
67 * searchIn: string,
68 * rows: [],
69 * rowId: number,
70 * selectedRowIds: number[],
71 * running: boolean,
72 * loadResultsTimer: Timer,
73 * sort: {column: string, reverse: string},
74 * }> **/
75 const searchState = new Map();
76 const searchText = {
77 pattern: "",
78 filterPattern: ""
80 const searchSeedsFilter = {
81 min: 0,
82 max: 0
84 const searchSizeFilter = {
85 min: 0.00,
86 minUnit: 2, // B = 0, KiB = 1, MiB = 2, GiB = 3, TiB = 4, PiB = 5, EiB = 6
87 max: 0.00,
88 maxUnit: 3
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",
97 actions: {
98 Download: downloadSearchTorrent,
99 OpenDescriptionUrl: openSearchTorrentDescriptionUrl
101 offsets: {
102 x: -15,
103 y: -53
106 searchResultsTable = new window.qBittorrent.DynamicTable.SearchResultsTable();
107 searchResultsTable.setup("searchResultsTableDiv", "searchResultsTableFixedHeaderDiv", searchResultsTableContextMenu);
108 getPlugins();
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);
123 new Keyboard({
124 defaultEventType: "keydown",
125 events: {
126 "Enter": function(e) {
127 // accept enter key as a click
128 new Event(e).stop();
130 const elem = e.event.srcElement;
131 if (elem.className.contains("searchInputField")) {
132 $("startSearchButton").click();
133 return;
136 switch (elem.id) {
137 case "manageSearchPlugins":
138 manageSearchPlugins();
139 break;
143 }).activate();
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", {
162 text: pattern,
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",
168 width: "8",
169 height: "8",
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", {
176 id: newTabId,
177 class: "selected",
178 html: tabElem.outerHTML,
179 }));
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";
189 // reinitialize tabs
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]");
193 setActiveTab(this);
196 // select new tab
197 setActiveTab($(newTabId));
199 searchResultsTable.clear();
200 resetFilters();
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(),
208 rows: [],
209 rowId: 0,
210 selectedRowIds: [],
211 running: true,
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);
230 tab.destroy();
232 new Request({
233 url: new URI("api/v2/search/delete"),
234 method: "post",
235 data: {
236 id: searchId
238 }).send();
240 const searchJobs = JSON.parse(LocalPreferences.get("search_jobs", "[]"));
241 const jobIndex = searchJobs.findIndex((job) => job.id === searchId);
242 if (jobIndex >= 0) {
243 searchJobs.splice(jobIndex, 1);
244 LocalPreferences.set("search_jobs", JSON.stringify(searchJobs));
247 if (numSearchTabs() === 0) {
248 resetSearchState();
249 resetFilters();
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)
267 return;
269 const state = searchState.get(currentSearchId);
270 if (!state)
271 return;
273 state.filterPattern = searchText.filterPattern;
274 state.seedsFilter = {
275 min: searchSeedsFilter.min,
276 max: searchSeedsFilter.max,
278 state.sizeFilter = {
279 min: searchSizeFilter.min,
280 minUnit: searchSizeFilter.minUnit,
281 max: searchSizeFilter.max,
282 maxUnit: searchSizeFilter.maxUnit,
284 state.searchIn = getSearchInTorrentName();
286 state.sort = {
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())
298 return;
300 saveCurrentTabState();
302 MochaUI.selected(tab, "searchTabs");
304 const state = searchState.get(searchId);
305 let rowsToSelect = [];
307 // restore table rows
308 searchResultsTable.clear();
309 if (state) {
310 for (const row of state.rows)
311 searchResultsTable.updateRowData(row);
313 rowsToSelect = state.selectedRowIds;
315 // restore filters
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", {
362 alt: text,
363 title: text,
364 src: image,
365 class: "statusIcon",
366 width: "10",
367 height: "10",
368 style: "margin-bottom: -2px; margin-left: 7px",
372 const updateStatusIconElement = function(searchId, text, image) {
373 const searchTab = $(`${searchTabIdPrefix}${searchId}`);
374 if (searchTab) {
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");
386 new Request.JSON({
387 url: url,
388 method: "post",
389 data: {
390 pattern: pattern,
391 category: category,
392 plugins: plugins
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));
403 }).send();
406 const stopSearch = function(searchId) {
407 const url = new URI("api/v2/search/stop");
408 new Request({
409 url: url,
410 method: "post",
411 data: {
412 id: searchId
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");
419 }).send();
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)
437 return;
439 searchText.pattern = pattern;
440 startSearch(pattern, category, plugins);
442 else {
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() {
454 const names = [];
455 searchResultsTable.selectedRowsIds().each((rowId) => {
456 names.push(searchResultsTable.rows.get(rowId).full_data.fileName);
458 return names.join("\n");
461 const copySearchTorrentDownloadLink = function() {
462 const urls = [];
463 searchResultsTable.selectedRowsIds().each((rowId) => {
464 urls.push(searchResultsTable.rows.get(rowId).full_data.fileUrl);
466 return urls.join("\n");
469 const copySearchTorrentDescriptionUrl = function() {
470 const urls = [];
471 searchResultsTable.selectedRowsIds().each((rowId) => {
472 urls.push(searchResultsTable.rows.get(rowId).full_data.descrLink);
474 return urls.join("\n");
477 const downloadSearchTorrent = function() {
478 const urls = [];
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
484 if (!urls.length)
485 return;
487 showDownloadPage(urls);
490 const manageSearchPlugins = function() {
491 const id = "searchPlugins";
492 if (!$(id)) {
493 new MochaUI.Window({
494 id: id,
495 title: "QBT_TR(Search plugins)QBT_TR[CONTEXT=PluginSelectDlg]",
496 loadMethod: "xhr",
497 contentURL: "views/searchplugins.html",
498 scrollbars: false,
499 maximizable: false,
500 paddingVertical: 0,
501 paddingHorizontal: 0,
502 width: loadWindowWidth(id, 600),
503 height: loadWindowHeight(id, 360),
504 onResize: function() {
505 saveWindowSize(id);
507 onBeforeBuild: function() {
508 loadSearchPlugins();
510 onClose: function() {
511 clearTimeout(loadSearchPluginsTimer);
517 const loadSearchPlugins = function() {
518 getPlugins();
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]");
531 else {
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;
556 categorySelected();
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;
565 pluginSelected();
568 const resetSearchState = function(searchId) {
569 $("startSearchButton").set("text", "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]");
570 const state = searchState.get(searchId);
571 if (state) {
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) {
589 // add separator
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)
605 continue;
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);
615 else {
616 const plugin = getPlugin(selectedPlugin);
617 const plugins = (plugin === null) ? [] : plugin.supportedCategories;
618 populateCategorySelect(plugins);
621 reselectCategory();
624 const getPlugins = function() {
625 new Request.JSON({
626 url: new URI("api/v2/search/plugins"),
627 method: "get",
628 noCache: true,
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();
673 reselectPlugin();
676 }).send();
679 const getPlugin = function(name) {
680 for (let i = 0; i < searchPlugins.length; ++i) {
681 if (searchPlugins[i].name === name)
682 return searchPlugins[i];
685 return null;
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) {
743 if (enable) {
744 $$(".searchTableRow").each((target) => {
745 target.addEventListener("dblclick", downloadSearchTorrent, false);
748 else {
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");
760 new Request.JSON({
761 url: url,
762 method: "get",
763 noCache: true,
764 data: {
765 id: searchId,
766 limit: maxResults,
767 offset: state.rowId
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");
775 else {
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");
788 return;
791 if (response) {
792 setupSearchTableEvents(false);
794 const state = searchState.get(searchId);
795 const newRows = [];
797 if (response.results) {
798 const results = response.results;
799 for (let i = 0; i < results.length; ++i) {
800 const result = results[i];
801 const row = {
802 rowId: state.rowId,
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,
813 newRows.push(row);
814 state.rows.push(row);
815 state.rowId += 1;
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");
836 return;
840 clearTimeout(state.loadResultsTimer);
841 state.loadResultsTimer = loadSearchResultsData.delay(2000, this, searchId);
843 }).send();
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();
861 default:
862 return "";
867 return exports();
868 })();
870 Object.freeze(window.qBittorrent.Search);