WebUI: Use event delegation to handle common table events
[qBittorrent.git] / src / webui / www / private / scripts / search.js
blob80ab62afce24a61ab51bb886323bc080456d6b07
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 searchResultsTabsContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
90 targets: ".searchTab",
91 menu: "searchResultsTabsMenu",
92 actions: {
93 closeTab: (tab) => { closeSearchTab(tab); },
94 closeAllTabs: () => {
95 for (const tab of document.querySelectorAll("#searchTabs .searchTab"))
96 closeSearchTab(tab);
99 offsets: {
100 x: -15,
101 y: -53
103 onShow: function() {
104 setActiveTab(this.options.element);
108 const init = () => {
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",
114 actions: {
115 Download: downloadSearchTorrent,
116 OpenDescriptionUrl: openSearchTorrentDescriptionUrl
118 offsets: {
119 x: 0,
120 y: -60
123 searchResultsTable = new window.qBittorrent.DynamicTable.SearchResultsTable();
124 searchResultsTable.setup("searchResultsTableDiv", "searchResultsTableFixedHeaderDiv", searchResultsTableContextMenu);
125 getPlugins();
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);
142 new Keyboard({
143 defaultEventType: "keydown",
144 events: {
145 "Enter": function(e) {
146 // accept enter key as a click
147 e.preventDefault();
148 e.stopPropagation();
150 const elem = e.event.srcElement;
151 if (elem.className.contains("searchInputField")) {
152 document.getElementById("startSearchButton").click();
153 return;
156 switch (elem.id) {
157 case "manageSearchPlugins":
158 manageSearchPlugins();
159 break;
163 }).activate();
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", {
182 text: pattern,
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",
189 width: "10",
190 height: "10",
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");
216 // select new tab
217 setActiveTab(listItem);
219 searchResultsTable.clear();
220 resetFilters();
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(),
228 rows: [],
229 rowId: 0,
230 selectedRowIds: [],
231 running: true,
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");
240 if (!tab)
241 return;
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);
253 tab.destroy();
255 new Request({
256 url: new URI("api/v2/search/delete"),
257 method: "post",
258 data: {
259 id: searchId
261 }).send();
263 const searchJobs = JSON.parse(LocalPreferences.get("search_jobs", "[]"));
264 const jobIndex = searchJobs.findIndex((job) => job.id === searchId);
265 if (jobIndex >= 0) {
266 searchJobs.splice(jobIndex, 1);
267 LocalPreferences.set("search_jobs", JSON.stringify(searchJobs));
270 if (numSearchTabs() === 0) {
271 resetSearchState();
272 resetFilters();
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)
290 return;
292 const state = searchState.get(currentSearchId);
293 if (!state)
294 return;
296 state.filterPattern = searchText.filterPattern;
297 state.seedsFilter = {
298 min: searchSeedsFilter.min,
299 max: searchSeedsFilter.max,
301 state.sizeFilter = {
302 min: searchSizeFilter.min,
303 minUnit: searchSizeFilter.minUnit,
304 max: searchSizeFilter.max,
305 maxUnit: searchSizeFilter.maxUnit,
307 state.searchIn = getSearchInTorrentName();
309 state.sort = {
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())
321 return;
323 saveCurrentTabState();
325 MochaUI.selected(tab, "searchTabs");
327 const state = searchState.get(searchId);
328 let rowsToSelect = [];
330 // restore table rows
331 searchResultsTable.clear();
332 if (state) {
333 for (const row of state.rows)
334 searchResultsTable.updateRowData(row);
336 rowsToSelect = state.selectedRowIds;
338 // restore filters
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", {
382 alt: text,
383 title: text,
384 src: image,
385 class: "statusIcon",
386 width: "12",
387 height: "12",
391 const updateStatusIconElement = (searchId, text, image) => {
392 const searchTab = $(`${searchTabIdPrefix}${searchId}`);
393 if (searchTab) {
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");
405 new Request.JSON({
406 url: url,
407 method: "post",
408 data: {
409 pattern: pattern,
410 category: category,
411 plugins: plugins
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));
422 }).send();
425 const stopSearch = (searchId) => {
426 const url = new URI("api/v2/search/stop");
427 new Request({
428 url: url,
429 method: "post",
430 data: {
431 id: searchId
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");
438 }).send();
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)
456 return;
458 searchText.pattern = pattern;
459 startSearch(pattern, category, plugins);
461 else {
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 = () => {
472 const names = [];
473 searchResultsTable.selectedRowsIds().each((rowId) => {
474 names.push(searchResultsTable.getRow(rowId).full_data.fileName);
476 return names.join("\n");
479 const copySearchTorrentDownloadLink = () => {
480 const urls = [];
481 searchResultsTable.selectedRowsIds().each((rowId) => {
482 urls.push(searchResultsTable.getRow(rowId).full_data.fileUrl);
484 return urls.join("\n");
487 const copySearchTorrentDescriptionUrl = () => {
488 const urls = [];
489 searchResultsTable.selectedRowsIds().each((rowId) => {
490 urls.push(searchResultsTable.getRow(rowId).full_data.descrLink);
492 return urls.join("\n");
495 const downloadSearchTorrent = () => {
496 const urls = [];
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
501 if (!urls.length)
502 return;
504 showDownloadPage(urls);
507 const manageSearchPlugins = () => {
508 const id = "searchPlugins";
509 if (!$(id)) {
510 new MochaUI.Window({
511 id: id,
512 title: "QBT_TR(Search plugins)QBT_TR[CONTEXT=PluginSelectDlg]",
513 icon: "images/qbittorrent-tray.svg",
514 loadMethod: "xhr",
515 contentURL: "views/searchplugins.html",
516 scrollbars: false,
517 maximizable: false,
518 paddingVertical: 0,
519 paddingHorizontal: 0,
520 width: loadWindowWidth(id, 600),
521 height: loadWindowHeight(id, 360),
522 onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
523 saveWindowSize(id);
525 onBeforeBuild: () => {
526 loadSearchPlugins();
528 onClose: () => {
529 clearTimeout(loadSearchPluginsTimer);
530 loadSearchPluginsTimer = -1;
536 const loadSearchPlugins = () => {
537 getPlugins();
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]";
550 else {
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;
575 categorySelected();
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;
584 pluginSelected();
587 const resetSearchState = (searchId) => {
588 document.getElementById("startSearchButton").lastChild.textContent = "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]";
589 const state = searchState.get(searchId);
590 if (state) {
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) {
610 // add separator
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)
626 continue;
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);
636 else {
637 const plugin = getPlugin(selectedPlugin);
638 const plugins = (plugin === null) ? [] : plugin.supportedCategories;
639 populateCategorySelect(plugins);
642 reselectCategory();
645 const getPlugins = () => {
646 new Request.JSON({
647 url: new URI("api/v2/search/plugins"),
648 method: "get",
649 noCache: true,
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;
657 return option;
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();
703 reselectPlugin();
706 }).send();
709 const getPlugin = (name) => {
710 for (let i = 0; i < searchPlugins.length; ++i) {
711 if (searchPlugins[i].name === name)
712 return searchPlugins[i];
715 return null;
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");
777 new Request.JSON({
778 url: url,
779 method: "get",
780 noCache: true,
781 data: {
782 id: searchId,
783 limit: maxResults,
784 offset: state.rowId
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");
792 else {
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");
805 return;
808 if (response) {
809 const state = searchState.get(searchId);
810 const newRows = [];
812 if (response.results) {
813 const results = response.results;
814 for (let i = 0; i < results.length; ++i) {
815 const result = results[i];
816 const row = {
817 rowId: state.rowId,
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,
829 newRows.push(row);
830 state.rows.push(row);
831 state.rowId += 1;
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");
849 return;
853 clearTimeout(state.loadResultsTimer);
854 state.loadResultsTimer = loadSearchResultsData.delay(2000, this, searchId);
856 }).send();
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", {
866 text: (trigger) => {
867 switch (trigger.id) {
868 case "copySearchTorrentName":
869 return copySearchTorrentName();
870 case "copySearchTorrentDownloadLink":
871 return copySearchTorrentDownloadLink();
872 case "copySearchTorrentDescriptionUrl":
873 return copySearchTorrentDescriptionUrl();
874 default:
875 return "";
880 return exports();
881 })();
882 Object.freeze(window.qBittorrent.Search);