WebUI: Add missing icons
[qBittorrent.git] / src / webui / www / private / scripts / search.js
blob7db6064b0258616c2fdd0502ce9ed3e7d05f35f2
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 = -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},
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").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);
121 new Keyboard({
122 defaultEventType: "keydown",
123 events: {
124 "Enter": function(e) {
125 // accept enter key as a click
126 e.preventDefault();
127 e.stopPropagation();
129 const elem = e.event.srcElement;
130 if (elem.className.contains("searchInputField")) {
131 document.getElementById("startSearchButton").click();
132 return;
135 switch (elem.id) {
136 case "manageSearchPlugins":
137 manageSearchPlugins();
138 break;
142 }).activate();
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", {
161 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(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";
195 // select new tab
196 setActiveTab(listItem);
198 searchResultsTable.clear();
199 resetFilters();
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(),
207 rows: [],
208 rowId: 0,
209 selectedRowIds: [],
210 running: true,
211 loadResultsTimer: -1,
212 sort: { column: searchResultsTable.sortedColumn, reverse: searchResultsTable.reverseSort },
214 updateSearchResultsData(searchId);
217 const closeSearchTab = function(e, el) {
218 e.stopPropagation();
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);
231 tab.destroy();
233 new Request({
234 url: new URI("api/v2/search/delete"),
235 method: "post",
236 data: {
237 id: searchId
239 }).send();
241 const searchJobs = JSON.parse(LocalPreferences.get("search_jobs", "[]"));
242 const jobIndex = searchJobs.findIndex((job) => job.id === searchId);
243 if (jobIndex >= 0) {
244 searchJobs.splice(jobIndex, 1);
245 LocalPreferences.set("search_jobs", JSON.stringify(searchJobs));
248 if (numSearchTabs() === 0) {
249 resetSearchState();
250 resetFilters();
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)
268 return;
270 const state = searchState.get(currentSearchId);
271 if (!state)
272 return;
274 state.filterPattern = searchText.filterPattern;
275 state.seedsFilter = {
276 min: searchSeedsFilter.min,
277 max: searchSeedsFilter.max,
279 state.sizeFilter = {
280 min: searchSizeFilter.min,
281 minUnit: searchSizeFilter.minUnit,
282 max: searchSizeFilter.max,
283 maxUnit: searchSizeFilter.maxUnit,
285 state.searchIn = getSearchInTorrentName();
287 state.sort = {
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())
299 return;
301 saveCurrentTabState();
303 MochaUI.selected(tab, "searchTabs");
305 const state = searchState.get(searchId);
306 let rowsToSelect = [];
308 // restore table rows
309 searchResultsTable.clear();
310 if (state) {
311 for (const row of state.rows)
312 searchResultsTable.updateRowData(row);
314 rowsToSelect = state.selectedRowIds;
316 // restore filters
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", {
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.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");
386 new Request.JSON({
387 url: url,
388 method: "post",
389 data: {
390 pattern: pattern,
391 category: category,
392 plugins: plugins
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));
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").value.trim();
433 const category = $("categorySelect").value;
434 const plugins = $("pluginsSelect").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 icon: "images/qbittorrent-tray.svg",
497 loadMethod: "xhr",
498 contentURL: "views/searchplugins.html",
499 scrollbars: false,
500 maximizable: false,
501 paddingVertical: 0,
502 paddingHorizontal: 0,
503 width: loadWindowWidth(id, 600),
504 height: loadWindowHeight(id, 360),
505 onResize: function() {
506 saveWindowSize(id);
508 onBeforeBuild: function() {
509 loadSearchPlugins();
511 onClose: function() {
512 clearTimeout(loadSearchPluginsTimer);
513 loadSearchPluginsTimer = -1;
519 const loadSearchPlugins = function() {
520 getPlugins();
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]";
533 else {
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;
558 categorySelected();
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;
567 pluginSelected();
570 const resetSearchState = function(searchId) {
571 document.getElementById("startSearchButton").lastChild.textContent = "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]";
572 const state = searchState.get(searchId);
573 if (state) {
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) {
593 // add separator
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)
609 continue;
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);
619 else {
620 const plugin = getPlugin(selectedPlugin);
621 const plugins = (plugin === null) ? [] : plugin.supportedCategories;
622 populateCategorySelect(plugins);
625 reselectCategory();
628 const getPlugins = function() {
629 new Request.JSON({
630 url: new URI("api/v2/search/plugins"),
631 method: "get",
632 noCache: true,
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;
640 return option;
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();
686 reselectPlugin();
689 }).send();
692 const getPlugin = function(name) {
693 for (let i = 0; i < searchPlugins.length; ++i) {
694 if (searchPlugins[i].name === name)
695 return searchPlugins[i];
698 return null;
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) {
756 if (enable) {
757 $$(".searchTableRow").each((target) => {
758 target.addEventListener("dblclick", downloadSearchTorrent, false);
761 else {
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");
773 new Request.JSON({
774 url: url,
775 method: "get",
776 noCache: true,
777 data: {
778 id: searchId,
779 limit: maxResults,
780 offset: state.rowId
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");
788 else {
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");
801 return;
804 if (response) {
805 setupSearchTableEvents(false);
807 const state = searchState.get(searchId);
808 const newRows = [];
810 if (response.results) {
811 const results = response.results;
812 for (let i = 0; i < results.length; ++i) {
813 const result = results[i];
814 const row = {
815 rowId: state.rowId,
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,
826 newRows.push(row);
827 state.rows.push(row);
828 state.rowId += 1;
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");
848 return;
852 clearTimeout(state.loadResultsTimer);
853 state.loadResultsTimer = loadSearchResultsData.delay(2000, this, searchId);
855 }).send();
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();
873 default:
874 return "";
879 return exports();
880 })();
881 Object.freeze(window.qBittorrent.Search);