WebUI: Add ability to toggle alternating row colors in tables
[qBittorrent.git] / src / webui / www / private / scripts / search.js
blob5f2eed76ee631d4dd449a4f7297dc0888359b648
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 window.qBittorrent ??= {};
27 window.qBittorrent.Search ??= (() => {
28 const exports = () => {
29 return {
30 startStopSearch: startStopSearch,
31 manageSearchPlugins: manageSearchPlugins,
32 searchPlugins: searchPlugins,
33 searchText: searchText,
34 searchSeedsFilter: searchSeedsFilter,
35 searchSizeFilter: searchSizeFilter,
36 init: init,
37 getPlugin: getPlugin,
38 searchInTorrentName: searchInTorrentName,
39 onSearchPatternChanged: onSearchPatternChanged,
40 categorySelected: categorySelected,
41 pluginSelected: pluginSelected,
42 searchSeedsFilterChanged: searchSeedsFilterChanged,
43 searchSizeFilterChanged: searchSizeFilterChanged,
44 searchSizeFilterPrefixChanged: searchSizeFilterPrefixChanged,
45 closeSearchTab: closeSearchTab,
49 const searchTabIdPrefix = "Search-";
50 let loadSearchPluginsTimer;
51 const searchPlugins = [];
52 let prevSearchPluginsResponse;
53 let selectedCategory = "QBT_TR(All categories)QBT_TR[CONTEXT=SearchEngineWidget]";
54 let selectedPlugin = "enabled";
55 let prevSelectedPlugin;
56 // whether the current search pattern differs from the pattern that the active search was performed with
57 let searchPatternChanged = false;
59 let searchResultsTable;
60 /** @type Map<number, {
61 * searchPattern: string,
62 * filterPattern: string,
63 * seedsFilter: {min: number, max: number},
64 * sizeFilter: {min: number, minUnit: number, max: number, maxUnit: number},
65 * searchIn: string,
66 * rows: [],
67 * rowId: number,
68 * selectedRowIds: number[],
69 * running: boolean,
70 * loadResultsTimer: Timer,
71 * sort: {column: string, reverse: string},
72 * }> **/
73 const searchState = new Map();
74 const searchText = {
75 pattern: "",
76 filterPattern: ""
78 const searchSeedsFilter = {
79 min: 0,
80 max: 0
82 const searchSizeFilter = {
83 min: 0.00,
84 minUnit: 2, // B = 0, KiB = 1, MiB = 2, GiB = 3, TiB = 4, PiB = 5, EiB = 6
85 max: 0.00,
86 maxUnit: 3
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",
95 actions: {
96 Download: downloadSearchTorrent,
97 OpenDescriptionUrl: openSearchTorrentDescriptionUrl
99 offsets: {
100 x: -15,
101 y: -53
104 searchResultsTable = new window.qBittorrent.DynamicTable.SearchResultsTable();
105 searchResultsTable.setup("searchResultsTableDiv", "searchResultsTableFixedHeaderDiv", searchResultsTableContextMenu);
106 getPlugins();
108 // listen for changes to searchInNameFilter
109 let searchInNameFilterTimer = -1;
110 $("searchInNameFilter").addEvent("input", () => {
111 clearTimeout(searchInNameFilterTimer);
112 searchInNameFilterTimer = setTimeout(() => {
113 searchInNameFilterTimer = -1;
115 const value = $("searchInNameFilter").value;
116 searchText.filterPattern = value;
117 searchFilterChanged();
118 }, window.qBittorrent.Misc.FILTER_INPUT_DELAY);
121 new Keyboard({
122 defaultEventType: "keydown",
123 events: {
124 "Enter": function(e) {
125 // accept enter key as a click
126 new Event(e).stop();
128 const elem = e.event.srcElement;
129 if (elem.className.contains("searchInputField")) {
130 $("startSearchButton").click();
131 return;
134 switch (elem.id) {
135 case "manageSearchPlugins":
136 manageSearchPlugins();
137 break;
141 }).activate();
143 // restore search tabs
144 const searchJobs = JSON.parse(LocalPreferences.get("search_jobs", "[]"));
145 for (const { id, pattern } of searchJobs)
146 createSearchTab(id, pattern);
149 const numSearchTabs = function() {
150 return $("searchTabs").getElements("li").length;
153 const getSearchIdFromTab = function(tab) {
154 return Number(tab.id.substring(searchTabIdPrefix.length));
157 const createSearchTab = function(searchId, pattern) {
158 const newTabId = `${searchTabIdPrefix}${searchId}`;
159 const tabElem = new Element("a", {
160 text: pattern,
162 const closeTabElem = new Element("img", {
163 alt: "QBT_TR(Close tab)QBT_TR[CONTEXT=SearchWidget]",
164 title: "QBT_TR(Close tab)QBT_TR[CONTEXT=SearchWidget]",
165 src: "images/application-exit.svg",
166 width: "8",
167 height: "8",
168 style: "padding-right: 7px; margin-bottom: -1px; margin-left: -5px",
169 onclick: "qBittorrent.Search.closeSearchTab(this)",
171 closeTabElem.inject(tabElem, "top");
172 tabElem.appendChild(getStatusIconElement("QBT_TR(Searching...)QBT_TR[CONTEXT=SearchJobWidget]", "images/queued.svg"));
173 $("searchTabs").appendChild(new Element("li", {
174 id: newTabId,
175 class: "selected",
176 html: tabElem.outerHTML,
177 }));
179 // unhide the results elements
180 if (numSearchTabs() >= 1) {
181 $("searchResultsNoSearches").style.display = "none";
182 $("searchResultsFilters").style.display = "block";
183 $("searchResultsTableContainer").style.display = "block";
184 $("searchTabsToolbar").style.display = "block";
187 // reinitialize tabs
188 $("searchTabs").getElements("li").removeEvents("click");
189 $("searchTabs").getElements("li").addEvent("click", function(e) {
190 $("startSearchButton").textContent = "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]";
191 setActiveTab(this);
194 // select new tab
195 setActiveTab($(newTabId));
197 searchResultsTable.clear();
198 resetFilters();
200 searchState.set(searchId, {
201 searchPattern: pattern,
202 filterPattern: searchText.filterPattern,
203 seedsFilter: { min: searchSeedsFilter.min, max: searchSeedsFilter.max },
204 sizeFilter: { min: searchSizeFilter.min, minUnit: searchSizeFilter.minUnit, max: searchSizeFilter.max, maxUnit: searchSizeFilter.maxUnit },
205 searchIn: getSearchInTorrentName(),
206 rows: [],
207 rowId: 0,
208 selectedRowIds: [],
209 running: true,
210 loadResultsTimer: null,
211 sort: { column: searchResultsTable.sortedColumn, reverse: searchResultsTable.reverseSort },
213 updateSearchResultsData(searchId);
216 const closeSearchTab = function(el) {
217 const tab = el.parentElement.parentElement;
218 const searchId = getSearchIdFromTab(tab);
219 const isTabSelected = tab.hasClass("selected");
220 const newTabToSelect = isTabSelected ? tab.nextSibling || tab.previousSibling : null;
222 const currentSearchId = getSelectedSearchId();
223 const state = searchState.get(currentSearchId);
224 // don't bother sending a stop request if already stopped
225 if (state && state.running)
226 stopSearch(searchId);
228 tab.destroy();
230 new Request({
231 url: new URI("api/v2/search/delete"),
232 method: "post",
233 data: {
234 id: searchId
236 }).send();
238 const searchJobs = JSON.parse(LocalPreferences.get("search_jobs", "[]"));
239 const jobIndex = searchJobs.findIndex((job) => job.id === searchId);
240 if (jobIndex >= 0) {
241 searchJobs.splice(jobIndex, 1);
242 LocalPreferences.set("search_jobs", JSON.stringify(searchJobs));
245 if (numSearchTabs() === 0) {
246 resetSearchState();
247 resetFilters();
249 $("numSearchResultsVisible").textContent = 0;
250 $("numSearchResultsTotal").textContent = 0;
251 $("searchResultsNoSearches").style.display = "block";
252 $("searchResultsFilters").style.display = "none";
253 $("searchResultsTableContainer").style.display = "none";
254 $("searchTabsToolbar").style.display = "none";
256 else if (isTabSelected && newTabToSelect) {
257 setActiveTab(newTabToSelect);
258 $("startSearchButton").textContent = "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]";
262 const saveCurrentTabState = function() {
263 const currentSearchId = getSelectedSearchId();
264 if (!currentSearchId)
265 return;
267 const state = searchState.get(currentSearchId);
268 if (!state)
269 return;
271 state.filterPattern = searchText.filterPattern;
272 state.seedsFilter = {
273 min: searchSeedsFilter.min,
274 max: searchSeedsFilter.max,
276 state.sizeFilter = {
277 min: searchSizeFilter.min,
278 minUnit: searchSizeFilter.minUnit,
279 max: searchSizeFilter.max,
280 maxUnit: searchSizeFilter.maxUnit,
282 state.searchIn = getSearchInTorrentName();
284 state.sort = {
285 column: searchResultsTable.sortedColumn,
286 reverse: searchResultsTable.reverseSort,
289 // we must copy the array to avoid taking a reference to it
290 state.selectedRowIds = [...searchResultsTable.selectedRows];
293 const setActiveTab = function(tab) {
294 const searchId = getSearchIdFromTab(tab);
295 if (searchId === getSelectedSearchId())
296 return;
298 saveCurrentTabState();
300 MochaUI.selected(tab, "searchTabs");
302 const state = searchState.get(searchId);
303 let rowsToSelect = [];
305 // restore table rows
306 searchResultsTable.clear();
307 if (state) {
308 for (const row of state.rows)
309 searchResultsTable.updateRowData(row);
311 rowsToSelect = state.selectedRowIds;
313 // restore filters
314 searchText.pattern = state.searchPattern;
315 searchText.filterPattern = state.filterPattern;
316 $("searchInNameFilter").value = state.filterPattern;
318 searchSeedsFilter.min = state.seedsFilter.min;
319 searchSeedsFilter.max = state.seedsFilter.max;
320 $("searchMinSeedsFilter").value = state.seedsFilter.min;
321 $("searchMaxSeedsFilter").value = state.seedsFilter.max;
323 searchSizeFilter.min = state.sizeFilter.min;
324 searchSizeFilter.minUnit = state.sizeFilter.minUnit;
325 searchSizeFilter.max = state.sizeFilter.max;
326 searchSizeFilter.maxUnit = state.sizeFilter.maxUnit;
327 $("searchMinSizeFilter").value = state.sizeFilter.min;
328 $("searchMinSizePrefix").value = state.sizeFilter.minUnit;
329 $("searchMaxSizeFilter").value = state.sizeFilter.max;
330 $("searchMaxSizePrefix").value = state.sizeFilter.maxUnit;
332 const currentSearchPattern = $("searchPattern").value.trim();
333 if (state.running && (state.searchPattern === currentSearchPattern)) {
334 // allow search to be stopped
335 $("startSearchButton").textContent = "QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]";
336 searchPatternChanged = false;
339 searchResultsTable.setSortedColumn(state.sort.column, state.sort.reverse);
341 $("searchInTorrentName").value = state.searchIn;
344 // must restore all filters before calling updateTable
345 searchResultsTable.updateTable();
347 // must reselect rows after calling updateTable
348 if (rowsToSelect.length > 0)
349 searchResultsTable.reselectRows(rowsToSelect);
351 $("numSearchResultsVisible").textContent = searchResultsTable.getFilteredAndSortedRows().length;
352 $("numSearchResultsTotal").textContent = searchResultsTable.getRowIds().length;
354 setupSearchTableEvents(true);
357 const getStatusIconElement = function(text, image) {
358 return new Element("img", {
359 alt: text,
360 title: text,
361 src: image,
362 class: "statusIcon",
363 width: "10",
364 height: "10",
365 style: "margin-bottom: -2px; margin-left: 7px",
369 const updateStatusIconElement = function(searchId, text, image) {
370 const searchTab = $(`${searchTabIdPrefix}${searchId}`);
371 if (searchTab) {
372 const statusIcon = searchTab.getElement(".statusIcon");
373 statusIcon.alt = text;
374 statusIcon.title = text;
375 statusIcon.src = image;
379 const startSearch = function(pattern, category, plugins) {
380 searchPatternChanged = false;
382 const url = new URI("api/v2/search/start");
383 new Request.JSON({
384 url: url,
385 method: "post",
386 data: {
387 pattern: pattern,
388 category: category,
389 plugins: plugins
391 onSuccess: function(response) {
392 $("startSearchButton").textContent = "QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]";
393 const searchId = response.id;
394 createSearchTab(searchId, pattern);
396 const searchJobs = JSON.parse(LocalPreferences.get("search_jobs", "[]"));
397 searchJobs.push({ id: searchId, pattern: pattern });
398 LocalPreferences.set("search_jobs", JSON.stringify(searchJobs));
400 }).send();
403 const stopSearch = function(searchId) {
404 const url = new URI("api/v2/search/stop");
405 new Request({
406 url: url,
407 method: "post",
408 data: {
409 id: searchId
411 onSuccess: function(response) {
412 resetSearchState(searchId);
413 // not strictly necessary to do this when the tab is being closed, but there's no harm in it
414 updateStatusIconElement(searchId, "QBT_TR(Search aborted)QBT_TR[CONTEXT=SearchJobWidget]", "images/task-reject.svg");
416 }).send();
419 const getSelectedSearchId = function() {
420 const selectedTab = $("searchTabs").getElement("li.selected");
421 return selectedTab ? getSearchIdFromTab(selectedTab) : null;
424 const startStopSearch = function() {
425 const currentSearchId = getSelectedSearchId();
426 const state = searchState.get(currentSearchId);
427 const isSearchRunning = state && state.running;
428 if (!isSearchRunning || searchPatternChanged) {
429 const pattern = $("searchPattern").value.trim();
430 const category = $("categorySelect").value;
431 const plugins = $("pluginsSelect").value;
433 if (!pattern || !category || !plugins)
434 return;
436 searchText.pattern = pattern;
437 startSearch(pattern, category, plugins);
439 else {
440 stopSearch(currentSearchId);
444 const openSearchTorrentDescriptionUrl = function() {
445 searchResultsTable.selectedRowsIds().each((rowId) => {
446 window.open(searchResultsTable.rows.get(rowId).full_data.descrLink, "_blank");
450 const copySearchTorrentName = function() {
451 const names = [];
452 searchResultsTable.selectedRowsIds().each((rowId) => {
453 names.push(searchResultsTable.rows.get(rowId).full_data.fileName);
455 return names.join("\n");
458 const copySearchTorrentDownloadLink = function() {
459 const urls = [];
460 searchResultsTable.selectedRowsIds().each((rowId) => {
461 urls.push(searchResultsTable.rows.get(rowId).full_data.fileUrl);
463 return urls.join("\n");
466 const copySearchTorrentDescriptionUrl = function() {
467 const urls = [];
468 searchResultsTable.selectedRowsIds().each((rowId) => {
469 urls.push(searchResultsTable.rows.get(rowId).full_data.descrLink);
471 return urls.join("\n");
474 const downloadSearchTorrent = function() {
475 const urls = [];
476 searchResultsTable.selectedRowsIds().each((rowId) => {
477 urls.push(searchResultsTable.rows.get(rowId).full_data.fileUrl);
480 // only proceed if at least 1 row was selected
481 if (!urls.length)
482 return;
484 showDownloadPage(urls);
487 const manageSearchPlugins = function() {
488 const id = "searchPlugins";
489 if (!$(id)) {
490 new MochaUI.Window({
491 id: id,
492 title: "QBT_TR(Search plugins)QBT_TR[CONTEXT=PluginSelectDlg]",
493 loadMethod: "xhr",
494 contentURL: "views/searchplugins.html",
495 scrollbars: false,
496 maximizable: false,
497 paddingVertical: 0,
498 paddingHorizontal: 0,
499 width: loadWindowWidth(id, 600),
500 height: loadWindowHeight(id, 360),
501 onResize: function() {
502 saveWindowSize(id);
504 onBeforeBuild: function() {
505 loadSearchPlugins();
507 onClose: function() {
508 clearTimeout(loadSearchPluginsTimer);
514 const loadSearchPlugins = function() {
515 getPlugins();
516 loadSearchPluginsTimer = loadSearchPlugins.delay(2000);
519 const onSearchPatternChanged = function() {
520 const currentSearchId = getSelectedSearchId();
521 const state = searchState.get(currentSearchId);
522 const currentSearchPattern = $("searchPattern").value.trim();
523 // start a new search if pattern has changed, otherwise allow the search to be stopped
524 if (state && (state.searchPattern === currentSearchPattern)) {
525 searchPatternChanged = false;
526 $("startSearchButton").textContent = "QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]";
528 else {
529 searchPatternChanged = true;
530 $("startSearchButton").textContent = "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]";
534 const categorySelected = function() {
535 selectedCategory = $("categorySelect").value;
538 const pluginSelected = function() {
539 selectedPlugin = $("pluginsSelect").value;
541 if (selectedPlugin !== prevSelectedPlugin) {
542 prevSelectedPlugin = selectedPlugin;
543 getSearchCategories();
547 const reselectCategory = function() {
548 for (let i = 0; i < $("categorySelect").options.length; ++i) {
549 if ($("categorySelect").options[i].get("value") === selectedCategory)
550 $("categorySelect").options[i].selected = true;
553 categorySelected();
556 const reselectPlugin = function() {
557 for (let i = 0; i < $("pluginsSelect").options.length; ++i) {
558 if ($("pluginsSelect").options[i].get("value") === selectedPlugin)
559 $("pluginsSelect").options[i].selected = true;
562 pluginSelected();
565 const resetSearchState = function(searchId) {
566 $("startSearchButton").textContent = "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]";
567 const state = searchState.get(searchId);
568 if (state) {
569 state.running = false;
570 clearTimeout(state.loadResultsTimer);
574 const getSearchCategories = function() {
575 const populateCategorySelect = function(categories) {
576 const categoryHtml = [];
577 categories.each((category) => {
578 const option = new Element("option");
579 option.value = category.id;
580 option.textContent = category.name;
581 categoryHtml.push(option.outerHTML);
584 // first category is "All Categories"
585 if (categoryHtml.length > 1) {
586 // add separator
587 const option = new Element("option");
588 option.disabled = true;
589 option.textContent = "──────────";
590 categoryHtml.splice(1, 0, option.outerHTML);
593 $("categorySelect").innerHTML = categoryHtml.join("");
596 const selectedPlugin = $("pluginsSelect").value;
598 if ((selectedPlugin === "all") || (selectedPlugin === "enabled")) {
599 const uniqueCategories = {};
600 for (const plugin of searchPlugins) {
601 if ((selectedPlugin === "enabled") && !plugin.enabled)
602 continue;
603 for (const category of plugin.supportedCategories) {
604 if (uniqueCategories[category.id] === undefined)
605 uniqueCategories[category.id] = category;
608 // we must sort the ids to maintain consistent order.
609 const categories = Object.keys(uniqueCategories).sort().map(id => uniqueCategories[id]);
610 populateCategorySelect(categories);
612 else {
613 const plugin = getPlugin(selectedPlugin);
614 const plugins = (plugin === null) ? [] : plugin.supportedCategories;
615 populateCategorySelect(plugins);
618 reselectCategory();
621 const getPlugins = function() {
622 new Request.JSON({
623 url: new URI("api/v2/search/plugins"),
624 method: "get",
625 noCache: true,
626 onSuccess: function(response) {
627 if (response !== prevSearchPluginsResponse) {
628 prevSearchPluginsResponse = response;
629 searchPlugins.length = 0;
630 response.forEach((plugin) => {
631 searchPlugins.push(plugin);
634 const pluginsHtml = [];
635 pluginsHtml.push('<option value="enabled">QBT_TR(Only enabled)QBT_TR[CONTEXT=SearchEngineWidget]</option>');
636 pluginsHtml.push('<option value="all">QBT_TR(All plugins)QBT_TR[CONTEXT=SearchEngineWidget]</option>');
638 const searchPluginsEmpty = (searchPlugins.length === 0);
639 if (!searchPluginsEmpty) {
640 $("searchResultsNoPlugins").style.display = "none";
641 if (numSearchTabs() === 0)
642 $("searchResultsNoSearches").style.display = "block";
644 // sort plugins alphabetically
645 const allPlugins = searchPlugins.sort((left, right) => {
646 const leftName = left.fullName;
647 const rightName = right.fullName;
648 return window.qBittorrent.Misc.naturalSortCollator.compare(leftName, rightName);
651 allPlugins.each((plugin) => {
652 if (plugin.enabled === true)
653 pluginsHtml.push("<option value='" + window.qBittorrent.Misc.escapeHtml(plugin.name) + "'>" + window.qBittorrent.Misc.escapeHtml(plugin.fullName) + "</option>");
656 if (pluginsHtml.length > 2)
657 pluginsHtml.splice(2, 0, "<option disabled>──────────</option>");
660 $("pluginsSelect").innerHTML = pluginsHtml.join("");
662 $("searchPattern").disabled = searchPluginsEmpty;
663 $("categorySelect").disabled = searchPluginsEmpty;
664 $("pluginsSelect").disabled = searchPluginsEmpty;
665 $("startSearchButton").disabled = searchPluginsEmpty;
667 if (window.qBittorrent.SearchPlugins !== undefined)
668 window.qBittorrent.SearchPlugins.updateTable();
670 reselectPlugin();
673 }).send();
676 const getPlugin = function(name) {
677 for (let i = 0; i < searchPlugins.length; ++i) {
678 if (searchPlugins[i].name === name)
679 return searchPlugins[i];
682 return null;
685 const resetFilters = function() {
686 searchText.filterPattern = "";
687 $("searchInNameFilter").value = "";
689 searchSeedsFilter.min = 0;
690 searchSeedsFilter.max = 0;
691 $("searchMinSeedsFilter").value = searchSeedsFilter.min;
692 $("searchMaxSeedsFilter").value = searchSeedsFilter.max;
694 searchSizeFilter.min = 0.00;
695 searchSizeFilter.minUnit = 2; // B = 0, KiB = 1, MiB = 2, GiB = 3, TiB = 4, PiB = 5, EiB = 6
696 searchSizeFilter.max = 0.00;
697 searchSizeFilter.maxUnit = 3;
698 $("searchMinSizeFilter").value = searchSizeFilter.min;
699 $("searchMinSizePrefix").value = searchSizeFilter.minUnit;
700 $("searchMaxSizeFilter").value = searchSizeFilter.max;
701 $("searchMaxSizePrefix").value = searchSizeFilter.maxUnit;
704 const getSearchInTorrentName = function() {
705 return ($("searchInTorrentName").value === "names") ? "names" : "everywhere";
708 const searchInTorrentName = function() {
709 LocalPreferences.set("search_in_filter", getSearchInTorrentName());
710 searchFilterChanged();
713 const searchSeedsFilterChanged = function() {
714 searchSeedsFilter.min = $("searchMinSeedsFilter").value;
715 searchSeedsFilter.max = $("searchMaxSeedsFilter").value;
717 searchFilterChanged();
720 const searchSizeFilterChanged = function() {
721 searchSizeFilter.min = $("searchMinSizeFilter").value;
722 searchSizeFilter.minUnit = $("searchMinSizePrefix").value;
723 searchSizeFilter.max = $("searchMaxSizeFilter").value;
724 searchSizeFilter.maxUnit = $("searchMaxSizePrefix").value;
726 searchFilterChanged();
729 const searchSizeFilterPrefixChanged = function() {
730 if ((Number($("searchMinSizeFilter").value) !== 0) || (Number($("searchMaxSizeFilter").value) !== 0))
731 searchSizeFilterChanged();
734 const searchFilterChanged = function() {
735 searchResultsTable.updateTable();
736 $("numSearchResultsVisible").textContent = searchResultsTable.getFilteredAndSortedRows().length;
739 const setupSearchTableEvents = function(enable) {
740 if (enable) {
741 $$(".searchTableRow").each((target) => {
742 target.addEventListener("dblclick", downloadSearchTorrent, false);
745 else {
746 $$(".searchTableRow").each((target) => {
747 target.removeEventListener("dblclick", downloadSearchTorrent, false);
752 const loadSearchResultsData = function(searchId) {
753 const state = searchState.get(searchId);
755 const maxResults = 500;
756 const url = new URI("api/v2/search/results");
757 new Request.JSON({
758 url: url,
759 method: "get",
760 noCache: true,
761 data: {
762 id: searchId,
763 limit: maxResults,
764 offset: state.rowId
766 onFailure: function(response) {
767 if ((response.status === 400) || (response.status === 404)) {
768 // bad params. search id is invalid
769 resetSearchState(searchId);
770 updateStatusIconElement(searchId, "QBT_TR(An error occurred during search...)QBT_TR[CONTEXT=SearchJobWidget]", "images/error.svg");
772 else {
773 clearTimeout(state.loadResultsTimer);
774 state.loadResultsTimer = loadSearchResultsData.delay(3000, this, searchId);
777 onSuccess: function(response) {
778 $("error_div").textContent = "";
780 const state = searchState.get(searchId);
781 // check if user stopped the search prior to receiving the response
782 if (!state.running) {
783 clearTimeout(state.loadResultsTimer);
784 updateStatusIconElement(searchId, "QBT_TR(Search aborted)QBT_TR[CONTEXT=SearchJobWidget]", "images/task-reject.svg");
785 return;
788 if (response) {
789 setupSearchTableEvents(false);
791 const state = searchState.get(searchId);
792 const newRows = [];
794 if (response.results) {
795 const results = response.results;
796 for (let i = 0; i < results.length; ++i) {
797 const result = results[i];
798 const row = {
799 rowId: state.rowId,
800 descrLink: result.descrLink,
801 fileName: result.fileName,
802 fileSize: result.fileSize,
803 fileUrl: result.fileUrl,
804 nbLeechers: result.nbLeechers,
805 nbSeeders: result.nbSeeders,
806 siteUrl: result.siteUrl,
807 pubDate: result.pubDate,
810 newRows.push(row);
811 state.rows.push(row);
812 state.rowId += 1;
816 // only update table if this search is currently being displayed
817 if (searchId === getSelectedSearchId()) {
818 for (const row of newRows)
819 searchResultsTable.updateRowData(row);
821 $("numSearchResultsVisible").textContent = searchResultsTable.getFilteredAndSortedRows().length;
822 $("numSearchResultsTotal").textContent = searchResultsTable.getRowIds().length;
824 searchResultsTable.updateTable();
827 setupSearchTableEvents(true);
829 if ((response.status === "Stopped") && (state.rowId >= response.total)) {
830 resetSearchState(searchId);
831 updateStatusIconElement(searchId, "QBT_TR(Search has finished)QBT_TR[CONTEXT=SearchJobWidget]", "images/task-complete.svg");
832 return;
836 clearTimeout(state.loadResultsTimer);
837 state.loadResultsTimer = loadSearchResultsData.delay(2000, this, searchId);
839 }).send();
842 const updateSearchResultsData = function(searchId) {
843 const state = searchState.get(searchId);
844 clearTimeout(state.loadResultsTimer);
845 state.loadResultsTimer = loadSearchResultsData.delay(500, this, searchId);
848 new ClipboardJS(".copySearchDataToClipboard", {
849 text: function(trigger) {
850 switch (trigger.id) {
851 case "copySearchTorrentName":
852 return copySearchTorrentName();
853 case "copySearchTorrentDownloadLink":
854 return copySearchTorrentDownloadLink();
855 case "copySearchTorrentDescriptionUrl":
856 return copySearchTorrentDescriptionUrl();
857 default:
858 return "";
863 return exports();
864 })();
865 Object.freeze(window.qBittorrent.Search);