From d2e51638618989f250517ccf90e3290e1bf754ec Mon Sep 17 00:00:00 2001 From: Thomas Piccirello <8296030+Piccirello@users.noreply.github.com> Date: Mon, 27 May 2024 05:51:02 -0700 Subject: [PATCH 01/44] WebUI: Restore previously used tab on load This PR restores the users previously used tab (Transfer, Search, RSS, etc.) when the WebUI is reloaded. PR #20705. --- src/webui/www/private/scripts/client.js | 120 +++- src/webui/www/private/scripts/search.js | 872 ++++++++++++++++++++++++ src/webui/www/private/views/search.html | 852 ----------------------- src/webui/www/webui.qrc | 1 + 4 files changed, 979 insertions(+), 866 deletions(-) create mode 100644 src/webui/www/private/scripts/search.js diff --git a/src/webui/www/private/scripts/client.js b/src/webui/www/private/scripts/client.js index 496ca43da..56b3d2ffc 100644 --- a/src/webui/www/private/scripts/client.js +++ b/src/webui/www/private/scripts/client.js @@ -37,7 +37,13 @@ window.qBittorrent.Client = (() => { getSyncMainDataInterval: getSyncMainDataInterval, isStopped: isStopped, stop: stop, - mainTitle: mainTitle + mainTitle: mainTitle, + showSearchEngine: showSearchEngine, + showRssReader: showRssReader, + showLogViewer: showLogViewer, + isShowSearchEngine: isShowSearchEngine, + isShowRssReader: isShowRssReader, + isShowLogViewer: isShowLogViewer }; }; @@ -77,6 +83,29 @@ window.qBittorrent.Client = (() => { return title; }; + let showingSearchEngine = false; + let showingRssReader = false; + let showingLogViewer = false; + + const showSearchEngine = function(bool) { + showingSearchEngine = bool; + }; + const showRssReader = function(bool) { + showingRssReader = bool; + }; + const showLogViewer = function(bool) { + showingLogViewer = bool; + }; + const isShowSearchEngine = function() { + return showingSearchEngine; + }; + const isShowRssReader = function() { + return showingRssReader; + }; + const isShowLogViewer = function() { + return showingLogViewer; + }; + return exports(); })(); Object.freeze(window.qBittorrent.Client); @@ -128,6 +157,9 @@ let setFilter = function() {}; let toggleFilterDisplay = function() {}; window.addEventListener("DOMContentLoaded", function() { + let isSearchPanelLoaded = false; + let isLogPanelLoaded = false; + const saveColumnSizes = function() { const filters_width = $('Filters').getSize().x; LocalPreferences.set('filters_width', filters_width); @@ -307,9 +339,9 @@ window.addEventListener("DOMContentLoaded", function() { $('speedInBrowserTitleBarLink').firstChild.style.opacity = '0'; // After showing/hiding the toolbar + status bar - let showSearchEngine = LocalPreferences.get('show_search_engine') !== "false"; - let showRssReader = LocalPreferences.get('show_rss_reader') !== "false"; - let showLogViewer = LocalPreferences.get('show_log_viewer') === 'true'; + window.qBittorrent.Client.showSearchEngine(LocalPreferences.get('show_search_engine') !== "false"); + window.qBittorrent.Client.showRssReader(LocalPreferences.get('show_rss_reader') !== "false"); + window.qBittorrent.Client.showLogViewer(LocalPreferences.get('show_log_viewer') === 'true'); // After Show Top Toolbar MochaUI.Desktop.setDesktopSize(); @@ -1070,25 +1102,25 @@ window.addEventListener("DOMContentLoaded", function() { }); $('showSearchEngineLink').addEvent('click', function(e) { - showSearchEngine = !showSearchEngine; - LocalPreferences.set('show_search_engine', showSearchEngine.toString()); + window.qBittorrent.Client.showSearchEngine(!window.qBittorrent.Client.isShowSearchEngine()); + LocalPreferences.set('show_search_engine', window.qBittorrent.Client.isShowSearchEngine().toString()); updateTabDisplay(); }); $('showRssReaderLink').addEvent('click', function(e) { - showRssReader = !showRssReader; - LocalPreferences.set('show_rss_reader', showRssReader.toString()); + window.qBittorrent.Client.showRssReader(!window.qBittorrent.Client.isShowRssReader()); + LocalPreferences.set('show_rss_reader', window.qBittorrent.Client.isShowRssReader().toString()); updateTabDisplay(); }); $('showLogViewerLink').addEvent('click', function(e) { - showLogViewer = !showLogViewer; - LocalPreferences.set('show_log_viewer', showLogViewer.toString()); + window.qBittorrent.Client.showLogViewer(!window.qBittorrent.Client.isShowLogViewer()); + LocalPreferences.set('show_log_viewer', window.qBittorrent.Client.isShowLogViewer().toString()); updateTabDisplay(); }); const updateTabDisplay = function() { - if (showRssReader) { + if (window.qBittorrent.Client.isShowRssReader()) { $('showRssReaderLink').firstChild.style.opacity = '1'; $('mainWindowTabs').removeClass('invisible'); $('rssTabLink').removeClass('invisible'); @@ -1102,7 +1134,7 @@ window.addEventListener("DOMContentLoaded", function() { $("transfersTabLink").click(); } - if (showSearchEngine) { + if (window.qBittorrent.Client.isShowSearchEngine()) { $('showSearchEngineLink').firstChild.style.opacity = '1'; $('mainWindowTabs').removeClass('invisible'); $('searchTabLink').removeClass('invisible'); @@ -1116,7 +1148,7 @@ window.addEventListener("DOMContentLoaded", function() { $("transfersTabLink").click(); } - if (showLogViewer) { + if (window.qBittorrent.Client.isShowLogViewer()) { $('showLogViewerLink').firstChild.style.opacity = '1'; $('mainWindowTabs').removeClass('invisible'); $('logTabLink').removeClass('invisible'); @@ -1131,7 +1163,7 @@ window.addEventListener("DOMContentLoaded", function() { } // display no tabs - if (!showRssReader && !showSearchEngine && !showLogViewer) + if (!window.qBittorrent.Client.isShowRssReader() && !window.qBittorrent.Client.isShowSearchEngine() && !window.qBittorrent.Client.isShowLogViewer()) $('mainWindowTabs').addClass('invisible'); }; @@ -1154,6 +1186,8 @@ window.addEventListener("DOMContentLoaded", function() { hideSearchTab(); hideRssTab(); hideLogTab(); + + LocalPreferences.set('selected_tab', 'transfers'); }; const hideTransfersTab = function() { @@ -1168,6 +1202,16 @@ window.addEventListener("DOMContentLoaded", function() { let searchTabInitialized = false; return () => { + // we must wait until the panel is fully loaded before proceeding. + // this include's the panel's custom js, which is loaded via MochaUI.Panel's 'require' field. + // MochaUI loads these files asynchronously and thus all required libs may not be available immediately + if (!isSearchPanelLoaded) { + setTimeout(() => { + showSearchTab(); + }, 100); + return; + } + if (!searchTabInitialized) { window.qBittorrent.Search.init(); searchTabInitialized = true; @@ -1178,6 +1222,8 @@ window.addEventListener("DOMContentLoaded", function() { hideTransfersTab(); hideRssTab(); hideLogTab(); + + LocalPreferences.set('selected_tab', 'search'); }; })(); @@ -1203,6 +1249,8 @@ window.addEventListener("DOMContentLoaded", function() { hideTransfersTab(); hideSearchTab(); hideLogTab(); + + LocalPreferences.set('selected_tab', 'rss'); }; })(); @@ -1216,6 +1264,16 @@ window.addEventListener("DOMContentLoaded", function() { let logTabInitialized = false; return () => { + // we must wait until the panel is fully loaded before proceeding. + // this include's the panel's custom js, which is loaded via MochaUI.Panel's 'require' field. + // MochaUI loads these files asynchronously and thus all required libs may not be available immediately + if (!isLogPanelLoaded) { + setTimeout(() => { + showLogTab(); + }, 100); + return; + } + if (!logTabInitialized) { window.qBittorrent.Log.init(); logTabInitialized = true; @@ -1229,6 +1287,8 @@ window.addEventListener("DOMContentLoaded", function() { hideTransfersTab(); hideSearchTab(); hideRssTab(); + + LocalPreferences.set('selected_tab', 'log'); }; })(); @@ -1251,6 +1311,12 @@ window.addEventListener("DOMContentLoaded", function() { }, loadMethod: 'xhr', contentURL: 'views/search.html', + require: { + js: ['scripts/search.js'], + onload: () => { + isSearchPanelLoaded = true; + }, + }, content: '', column: 'searchTabColumn', height: null @@ -1292,6 +1358,9 @@ window.addEventListener("DOMContentLoaded", function() { require: { css: ['css/vanillaSelectBox.css'], js: ['scripts/lib/vanillaSelectBox.js'], + onload: () => { + isLogPanelLoaded = true; + }, }, tabsURL: 'views/logTabs.html', tabsOnload: function() { @@ -1594,4 +1663,27 @@ window.addEventListener("load", () => { window.qBittorrent.Cache.buildInfo.init(); window.qBittorrent.Cache.preferences.init(); window.qBittorrent.Cache.qbtVersion.init(); + + // switch to previously used tab + const previouslyUsedTab = LocalPreferences.get('selected_tab', 'transfers'); + switch (previouslyUsedTab) { + case 'search': + if (window.qBittorrent.Client.isShowSearchEngine()) + $('searchTabLink').click(); + break; + case 'rss': + if (window.qBittorrent.Client.isShowRssReader()) + $('rssTabLink').click(); + break; + case 'log': + if (window.qBittorrent.Client.isShowLogViewer()) + $('logTabLink').click(); + break; + case 'transfers': + $('transfersTabLink').click(); + break; + default: + console.error(`Unexpected 'selected_tab' value: ${previouslyUsedTab}`); + $('transfersTabLink').click(); + }; }); diff --git a/src/webui/www/private/scripts/search.js b/src/webui/www/private/scripts/search.js new file mode 100644 index 000000000..f1ce6060d --- /dev/null +++ b/src/webui/www/private/scripts/search.js @@ -0,0 +1,872 @@ +/* + * MIT License + * Copyright (C) 2024 Thomas Piccirello + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +'use strict'; + +if (window.qBittorrent === undefined) { + window.qBittorrent = {}; +} + +window.qBittorrent.Search = (function() { + const exports = function() { + return { + startStopSearch: startStopSearch, + manageSearchPlugins: manageSearchPlugins, + searchPlugins: searchPlugins, + searchText: searchText, + searchSeedsFilter: searchSeedsFilter, + searchSizeFilter: searchSizeFilter, + init: init, + getPlugin: getPlugin, + searchInTorrentName: searchInTorrentName, + onSearchPatternChanged: onSearchPatternChanged, + categorySelected: categorySelected, + pluginSelected: pluginSelected, + searchSeedsFilterChanged: searchSeedsFilterChanged, + searchSizeFilterChanged: searchSizeFilterChanged, + searchSizeFilterPrefixChanged: searchSizeFilterPrefixChanged, + closeSearchTab: closeSearchTab, + }; + }; + + const searchTabIdPrefix = "Search-"; + let loadSearchPluginsTimer; + const searchPlugins = []; + let prevSearchPluginsResponse; + let selectedCategory = "QBT_TR(All categories)QBT_TR[CONTEXT=SearchEngineWidget]"; + let selectedPlugin = "all"; + let prevSelectedPlugin; + // whether the current search pattern differs from the pattern that the active search was performed with + let searchPatternChanged = false; + + let searchResultsTable; + /** @type Map **/ + const searchState = new Map(); + const searchText = { + pattern: "", + filterPattern: "" + }; + const searchSeedsFilter = { + min: 0, + max: 0 + }; + const searchSizeFilter = { + min: 0.00, + minUnit: 2, // B = 0, KiB = 1, MiB = 2, GiB = 3, TiB = 4, PiB = 5, EiB = 6 + max: 0.00, + maxUnit: 3 + }; + + const init = function() { + // load "Search in" preference from local storage + $('searchInTorrentName').set('value', (LocalPreferences.get('search_in_filter') === "names") ? "names" : "everywhere"); + const searchResultsTableContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({ + targets: '.searchTableRow', + menu: 'searchResultsTableMenu', + actions: { + Download: downloadSearchTorrent, + OpenDescriptionUrl: openSearchTorrentDescriptionUrl + }, + offsets: { + x: -15, + y: -53 + } + }); + searchResultsTable = new window.qBittorrent.DynamicTable.SearchResultsTable(); + searchResultsTable.setup('searchResultsTableDiv', 'searchResultsTableFixedHeaderDiv', searchResultsTableContextMenu); + getPlugins(); + + // listen for changes to searchInNameFilter + let searchInNameFilterTimer = -1; + $('searchInNameFilter').addEvent('input', () => { + clearTimeout(searchInNameFilterTimer); + searchInNameFilterTimer = setTimeout(() => { + searchInNameFilterTimer = -1; + + const value = $('searchInNameFilter').get("value"); + searchText.filterPattern = value; + searchFilterChanged(); + }, window.qBittorrent.Misc.FILTER_INPUT_DELAY); + }); + + new Keyboard({ + defaultEventType: 'keydown', + events: { + 'Enter': function(e) { + // accept enter key as a click + new Event(e).stop(); + + const elem = e.event.srcElement; + if (elem.className.contains("searchInputField")) { + $('startSearchButton').click(); + return; + } + + switch (elem.id) { + case "manageSearchPlugins": + manageSearchPlugins(); + break; + } + } + } + }).activate(); + + // restore search tabs + const searchJobs = JSON.parse(LocalPreferences.get('search_jobs', '[]')); + for (const { id, pattern } of searchJobs) { + createSearchTab(id, pattern); + } + }; + + const numSearchTabs = function() { + return $('searchTabs').getElements('li').length; + }; + + const getSearchIdFromTab = function(tab) { + return Number(tab.id.substring(searchTabIdPrefix.length)); + }; + + const createSearchTab = function(searchId, pattern) { + const newTabId = `${searchTabIdPrefix}${searchId}`; + const tabElem = new Element('a', { + text: pattern, + }); + const closeTabElem = new Element('img', { + alt: 'QBT_TR(Close tab)QBT_TR[CONTEXT=SearchWidget]', + title: 'QBT_TR(Close tab)QBT_TR[CONTEXT=SearchWidget]', + src: 'images/application-exit.svg', + width: '8', + height: '8', + style: 'padding-right: 7px; margin-bottom: -1px; margin-left: -5px', + onclick: 'qBittorrent.Search.closeSearchTab(this)', + }); + closeTabElem.inject(tabElem, 'top'); + tabElem.appendChild(getStatusIconElement('QBT_TR(Searching...)QBT_TR[CONTEXT=SearchJobWidget]', 'images/queued.svg')); + $('searchTabs').appendChild(new Element('li', { + id: newTabId, + class: 'selected', + html: tabElem.outerHTML, + })); + + // unhide the results elements + if (numSearchTabs() >= 1) { + $('searchResultsNoSearches').style.display = "none"; + $('searchResultsFilters').style.display = "block"; + $('searchResultsTableContainer').style.display = "block"; + $('searchTabsToolbar').style.display = "block"; + } + + // reinitialize tabs + $('searchTabs').getElements('li').removeEvents('click'); + $('searchTabs').getElements('li').addEvent('click', function(e) { + $('startSearchButton').set('text', 'QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]'); + setActiveTab(this); + }); + + // select new tab + setActiveTab($(newTabId)); + + searchResultsTable.clear(); + resetFilters(); + + searchState.set(searchId, { + searchPattern: pattern, + filterPattern: searchText.filterPattern, + seedsFilter: { min: searchSeedsFilter.min, max: searchSeedsFilter.max }, + sizeFilter: { min: searchSizeFilter.min, minUnit: searchSizeFilter.minUnit, max: searchSizeFilter.max, maxUnit: searchSizeFilter.maxUnit }, + searchIn: getSearchInTorrentName(), + rows: [], + rowId: 0, + selectedRowIds: [], + running: true, + loadResultsTimer: null, + sort: { column: searchResultsTable.sortedColumn, reverse: searchResultsTable.reverseSort }, + }); + updateSearchResultsData(searchId); + }; + + const closeSearchTab = function(el) { + const tab = el.parentElement.parentElement; + const searchId = getSearchIdFromTab(tab); + const isTabSelected = tab.hasClass('selected'); + const newTabToSelect = isTabSelected ? tab.nextSibling || tab.previousSibling : null; + + const currentSearchId = getSelectedSearchId(); + const state = searchState.get(currentSearchId); + // don't bother sending a stop request if already stopped + if (state && state.running) { + stopSearch(searchId); + } + + tab.destroy(); + + new Request({ + url: new URI('api/v2/search/delete'), + method: 'post', + data: { + id: searchId + }, + }).send(); + + const searchJobs = JSON.parse(LocalPreferences.get('search_jobs', '[]')); + const jobIndex = searchJobs.findIndex((job) => job.id === searchId); + if (jobIndex >= 0) { + searchJobs.splice(jobIndex, 1); + LocalPreferences.set('search_jobs', JSON.stringify(searchJobs)); + } + + if (numSearchTabs() === 0) { + resetSearchState(); + resetFilters(); + + $('numSearchResultsVisible').set('html', 0); + $('numSearchResultsTotal').set('html', 0); + $('searchResultsNoSearches').style.display = "block"; + $('searchResultsFilters').style.display = "none"; + $('searchResultsTableContainer').style.display = "none"; + $('searchTabsToolbar').style.display = "none"; + } + else if (isTabSelected && newTabToSelect) { + setActiveTab(newTabToSelect); + $('startSearchButton').set('text', 'QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]'); + } + }; + + const saveCurrentTabState = function() { + const currentSearchId = getSelectedSearchId(); + if (!currentSearchId) + return; + + const state = searchState.get(currentSearchId); + if (!state) + return; + + state.filterPattern = searchText.filterPattern; + state.seedsFilter = { + min: searchSeedsFilter.min, + max: searchSeedsFilter.max, + }; + state.sizeFilter = { + min: searchSizeFilter.min, + minUnit: searchSizeFilter.minUnit, + max: searchSizeFilter.max, + maxUnit: searchSizeFilter.maxUnit, + }; + state.searchIn = getSearchInTorrentName(); + + state.sort = { + column: searchResultsTable.sortedColumn, + reverse: searchResultsTable.reverseSort, + }; + + // we must copy the array to avoid taking a reference to it + state.selectedRowIds = [...searchResultsTable.selectedRows]; + }; + + const setActiveTab = function(tab) { + const searchId = getSearchIdFromTab(tab); + if (searchId === getSelectedSearchId()) + return; + + saveCurrentTabState(); + + MochaUI.selected(tab, 'searchTabs'); + + const state = searchState.get(searchId); + let rowsToSelect = []; + + // restore table rows + searchResultsTable.clear(); + if (state) { + for (const row of state.rows) { + searchResultsTable.updateRowData(row); + } + + rowsToSelect = state.selectedRowIds; + + // restore filters + searchText.pattern = state.searchPattern; + searchText.filterPattern = state.filterPattern; + $('searchInNameFilter').set("value", state.filterPattern); + + searchSeedsFilter.min = state.seedsFilter.min; + searchSeedsFilter.max = state.seedsFilter.max; + $('searchMinSeedsFilter').set('value', state.seedsFilter.min); + $('searchMaxSeedsFilter').set('value', state.seedsFilter.max); + + searchSizeFilter.min = state.sizeFilter.min; + searchSizeFilter.minUnit = state.sizeFilter.minUnit; + searchSizeFilter.max = state.sizeFilter.max; + searchSizeFilter.maxUnit = state.sizeFilter.maxUnit; + $('searchMinSizeFilter').set('value', state.sizeFilter.min); + $('searchMinSizePrefix').set('value', state.sizeFilter.minUnit); + $('searchMaxSizeFilter').set('value', state.sizeFilter.max); + $('searchMaxSizePrefix').set('value', state.sizeFilter.maxUnit); + + const currentSearchPattern = $('searchPattern').getProperty('value').trim(); + if (state.running && (state.searchPattern === currentSearchPattern)) { + // allow search to be stopped + $('startSearchButton').set('text', 'QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]'); + searchPatternChanged = false; + } + + searchResultsTable.setSortedColumn(state.sort.column, state.sort.reverse); + + $('searchInTorrentName').set('value', state.searchIn); + } + + // must restore all filters before calling updateTable + searchResultsTable.updateTable(); + searchResultsTable.altRow(); + + // must reselect rows after calling updateTable + if (rowsToSelect.length > 0) { + searchResultsTable.reselectRows(rowsToSelect); + } + + $('numSearchResultsVisible').set('html', searchResultsTable.getFilteredAndSortedRows().length); + $('numSearchResultsTotal').set('html', searchResultsTable.getRowIds().length); + + setupSearchTableEvents(true); + }; + + const getStatusIconElement = function(text, image) { + return new Element('img', { + alt: text, + title: text, + src: image, + class: 'statusIcon', + width: '10', + height: '10', + style: 'margin-bottom: -2px; margin-left: 7px', + }); + }; + + const updateStatusIconElement = function(searchId, text, image) { + const searchTab = $(`${searchTabIdPrefix}${searchId}`); + if (searchTab) { + const statusIcon = searchTab.getElement('.statusIcon'); + statusIcon.set('alt', text); + statusIcon.set('title', text); + statusIcon.set('src', image); + } + }; + + const startSearch = function(pattern, category, plugins) { + searchPatternChanged = false; + + const url = new URI('api/v2/search/start'); + new Request.JSON({ + url: url, + method: 'post', + data: { + pattern: pattern, + category: category, + plugins: plugins + }, + onSuccess: function(response) { + $('startSearchButton').set('text', 'QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]'); + const searchId = response.id; + createSearchTab(searchId, pattern); + + const searchJobs = JSON.parse(LocalPreferences.get('search_jobs', '[]')); + searchJobs.push({ id: searchId, pattern: pattern }); + LocalPreferences.set('search_jobs', JSON.stringify(searchJobs)); + } + }).send(); + }; + + const stopSearch = function(searchId) { + const url = new URI('api/v2/search/stop'); + new Request({ + url: url, + method: 'post', + data: { + id: searchId + }, + onSuccess: function(response) { + resetSearchState(searchId); + // not strictly necessary to do this when the tab is being closed, but there's no harm in it + updateStatusIconElement(searchId, 'QBT_TR(Search aborted)QBT_TR[CONTEXT=SearchJobWidget]', 'images/task-reject.svg'); + } + }).send(); + }; + + const getSelectedSearchId = function() { + const selectedTab = $('searchTabs').getElement('li.selected'); + return selectedTab ? getSearchIdFromTab(selectedTab) : null; + }; + + const startStopSearch = function() { + const currentSearchId = getSelectedSearchId(); + const state = searchState.get(currentSearchId); + const isSearchRunning = state && state.running; + if (!isSearchRunning || searchPatternChanged) { + const pattern = $('searchPattern').getProperty('value').trim(); + let category = $('categorySelect').getProperty('value'); + const plugins = $('pluginsSelect').getProperty('value'); + + if (!pattern || !category || !plugins) + return; + + searchText.pattern = pattern; + startSearch(pattern, category, plugins); + } + else { + stopSearch(currentSearchId); + } + }; + + const openSearchTorrentDescriptionUrl = function() { + searchResultsTable.selectedRowsIds().each(function(rowId) { + window.open(searchResultsTable.rows.get(rowId).full_data.descrLink, "_blank"); + }); + }; + + const copySearchTorrentName = function() { + const names = []; + searchResultsTable.selectedRowsIds().each(function(rowId) { + names.push(searchResultsTable.rows.get(rowId).full_data.fileName); + }); + return names.join("\n"); + }; + + const copySearchTorrentDownloadLink = function() { + const urls = []; + searchResultsTable.selectedRowsIds().each(function(rowId) { + urls.push(searchResultsTable.rows.get(rowId).full_data.fileUrl); + }); + return urls.join("\n"); + }; + + const copySearchTorrentDescriptionUrl = function() { + const urls = []; + searchResultsTable.selectedRowsIds().each(function(rowId) { + urls.push(searchResultsTable.rows.get(rowId).full_data.descrLink); + }); + return urls.join("\n"); + }; + + const downloadSearchTorrent = function() { + const urls = []; + searchResultsTable.selectedRowsIds().each(function(rowId) { + urls.push(searchResultsTable.rows.get(rowId).full_data.fileUrl); + }); + + // only proceed if at least 1 row was selected + if (!urls.length) + return; + + showDownloadPage(urls); + }; + + const manageSearchPlugins = function() { + const id = 'searchPlugins'; + if (!$(id)) + new MochaUI.Window({ + id: id, + title: "QBT_TR(Search plugins)QBT_TR[CONTEXT=PluginSelectDlg]", + loadMethod: 'xhr', + contentURL: 'views/searchplugins.html', + scrollbars: false, + maximizable: false, + paddingVertical: 0, + paddingHorizontal: 0, + width: loadWindowWidth(id, 600), + height: loadWindowHeight(id, 360), + onResize: function() { + saveWindowSize(id); + }, + onBeforeBuild: function() { + loadSearchPlugins(); + }, + onClose: function() { + clearTimeout(loadSearchPluginsTimer); + } + }); + }; + + const loadSearchPlugins = function() { + getPlugins(); + loadSearchPluginsTimer = loadSearchPlugins.delay(2000); + }; + + const onSearchPatternChanged = function() { + const currentSearchId = getSelectedSearchId(); + const state = searchState.get(currentSearchId); + const currentSearchPattern = $('searchPattern').getProperty('value').trim(); + // start a new search if pattern has changed, otherwise allow the search to be stopped + if (state && (state.searchPattern === currentSearchPattern)) { + searchPatternChanged = false; + $('startSearchButton').set('text', 'QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]'); + } + else { + searchPatternChanged = true; + $('startSearchButton').set('text', 'QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]'); + } + }; + + const categorySelected = function() { + selectedCategory = $("categorySelect").get("value"); + }; + + const pluginSelected = function() { + selectedPlugin = $("pluginsSelect").get("value"); + + if (selectedPlugin !== prevSelectedPlugin) { + prevSelectedPlugin = selectedPlugin; + getSearchCategories(); + } + }; + + const reselectCategory = function() { + for (let i = 0; i < $("categorySelect").options.length; ++i) + if ($("categorySelect").options[i].get("value") === selectedCategory) + $("categorySelect").options[i].selected = true; + + categorySelected(); + }; + + const reselectPlugin = function() { + for (let i = 0; i < $("pluginsSelect").options.length; ++i) + if ($("pluginsSelect").options[i].get("value") === selectedPlugin) + $("pluginsSelect").options[i].selected = true; + + pluginSelected(); + }; + + const resetSearchState = function(searchId) { + $('startSearchButton').set('text', 'QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]'); + const state = searchState.get(searchId); + if (state) { + state.running = false; + clearTimeout(state.loadResultsTimer); + } + }; + + const getSearchCategories = function() { + const populateCategorySelect = function(categories) { + const categoryHtml = []; + categories.each(function(category) { + const option = new Element("option"); + option.set("value", category.id); + option.set("html", category.name); + categoryHtml.push(option.outerHTML); + }); + + // first category is "All Categories" + if (categoryHtml.length > 1) { + // add separator + const option = new Element("option"); + option.set("disabled", true); + option.set("html", "──────────"); + categoryHtml.splice(1, 0, option.outerHTML); + } + + $('categorySelect').set('html', categoryHtml.join("")); + }; + + const selectedPlugin = $('pluginsSelect').get("value"); + + if ((selectedPlugin === "all") || (selectedPlugin === "enabled")) { + const uniqueCategories = {}; + for (const plugin of searchPlugins) { + if ((selectedPlugin === "enabled") && !plugin.enabled) + continue; + for (const category of plugin.supportedCategories) { + if (uniqueCategories[category.id] === undefined) { + uniqueCategories[category.id] = category; + } + } + } + // we must sort the ids to maintain consistent order. + const categories = Object.keys(uniqueCategories).sort().map(id => uniqueCategories[id]); + populateCategorySelect(categories); + } + else { + const plugin = getPlugin(selectedPlugin); + const plugins = (plugin === null) ? [] : plugin.supportedCategories; + populateCategorySelect(plugins); + } + + reselectCategory(); + }; + + const getPlugins = function() { + new Request.JSON({ + url: new URI('api/v2/search/plugins'), + method: 'get', + noCache: true, + onSuccess: function(response) { + if (response !== prevSearchPluginsResponse) { + prevSearchPluginsResponse = response; + searchPlugins.length = 0; + response.forEach(function(plugin) { + searchPlugins.push(plugin); + }); + + const pluginsHtml = []; + pluginsHtml.push(''); + pluginsHtml.push(''); + + const searchPluginsEmpty = (searchPlugins.length === 0); + if (!searchPluginsEmpty) { + $('searchResultsNoPlugins').style.display = "none"; + if (numSearchTabs() === 0) { + $('searchResultsNoSearches').style.display = "block"; + } + + // sort plugins alphabetically + const allPlugins = searchPlugins.sort((left, right) => { + const leftName = left.fullName; + const rightName = right.fullName; + return window.qBittorrent.Misc.naturalSortCollator.compare(leftName, rightName); + }); + + allPlugins.each(function(plugin) { + if (plugin.enabled === true) + pluginsHtml.push(""); + }); + + if (pluginsHtml.length > 2) + pluginsHtml.splice(2, 0, ""); + } + + $('pluginsSelect').set('html', pluginsHtml.join("")); + + $('searchPattern').setProperty('disabled', searchPluginsEmpty); + $('categorySelect').setProperty('disabled', searchPluginsEmpty); + $('pluginsSelect').setProperty('disabled', searchPluginsEmpty); + $('startSearchButton').setProperty('disabled', searchPluginsEmpty); + + if (window.qBittorrent.SearchPlugins !== undefined) + window.qBittorrent.SearchPlugins.updateTable(); + + reselectPlugin(); + } + } + }).send(); + }; + + const getPlugin = function(name) { + for (let i = 0; i < searchPlugins.length; ++i) + if (searchPlugins[i].name === name) + return searchPlugins[i]; + + return null; + }; + + const resetFilters = function() { + searchText.filterPattern = ''; + $('searchInNameFilter').set('value', ''); + + searchSeedsFilter.min = 0; + searchSeedsFilter.max = 0; + $('searchMinSeedsFilter').set('value', searchSeedsFilter.min); + $('searchMaxSeedsFilter').set('value', searchSeedsFilter.max); + + searchSizeFilter.min = 0.00; + searchSizeFilter.minUnit = 2; // B = 0, KiB = 1, MiB = 2, GiB = 3, TiB = 4, PiB = 5, EiB = 6 + searchSizeFilter.max = 0.00; + searchSizeFilter.maxUnit = 3; + $('searchMinSizeFilter').set('value', searchSizeFilter.min); + $('searchMinSizePrefix').set('value', searchSizeFilter.minUnit); + $('searchMaxSizeFilter').set('value', searchSizeFilter.max); + $('searchMaxSizePrefix').set('value', searchSizeFilter.maxUnit); + }; + + const getSearchInTorrentName = function() { + return $('searchInTorrentName').get('value') === "names" ? "names" : "everywhere"; + }; + + const searchInTorrentName = function() { + LocalPreferences.set('search_in_filter', getSearchInTorrentName()); + searchFilterChanged(); + }; + + const searchSeedsFilterChanged = function() { + searchSeedsFilter.min = $('searchMinSeedsFilter').get('value'); + searchSeedsFilter.max = $('searchMaxSeedsFilter').get('value'); + + searchFilterChanged(); + }; + + const searchSizeFilterChanged = function() { + searchSizeFilter.min = $('searchMinSizeFilter').get('value'); + searchSizeFilter.minUnit = $('searchMinSizePrefix').get('value'); + searchSizeFilter.max = $('searchMaxSizeFilter').get('value'); + searchSizeFilter.maxUnit = $('searchMaxSizePrefix').get('value'); + + searchFilterChanged(); + }; + + const searchSizeFilterPrefixChanged = function() { + if ((Number($('searchMinSizeFilter').get('value')) !== 0) || (Number($('searchMaxSizeFilter').get('value')) !== 0)) + searchSizeFilterChanged(); + }; + + const searchFilterChanged = function() { + searchResultsTable.updateTable(); + $('numSearchResultsVisible').set('html', searchResultsTable.getFilteredAndSortedRows().length); + }; + + const setupSearchTableEvents = function(enable) { + if (enable) + $$(".searchTableRow").each(function(target) { + target.addEventListener('dblclick', downloadSearchTorrent, false); + }); + else + $$(".searchTableRow").each(function(target) { + target.removeEventListener('dblclick', downloadSearchTorrent, false); + }); + }; + + const loadSearchResultsData = function(searchId) { + const state = searchState.get(searchId); + + const maxResults = 500; + const url = new URI('api/v2/search/results'); + new Request.JSON({ + url: url, + method: 'get', + noCache: true, + data: { + id: searchId, + limit: maxResults, + offset: state.rowId + }, + onFailure: function(response) { + if ((response.status === 400) || (response.status === 404)) { + // bad params. search id is invalid + resetSearchState(searchId); + updateStatusIconElement(searchId, 'QBT_TR(An error occurred during search...)QBT_TR[CONTEXT=SearchJobWidget]', 'images/error.svg'); + } + else { + clearTimeout(state.loadResultsTimer); + state.loadResultsTimer = loadSearchResultsData.delay(3000, this, searchId); + } + }, + onSuccess: function(response) { + $('error_div').set('html', ''); + + const state = searchState.get(searchId); + // check if user stopped the search prior to receiving the response + if (!state.running) { + clearTimeout(state.loadResultsTimer); + updateStatusIconElement(searchId, 'QBT_TR(Search aborted)QBT_TR[CONTEXT=SearchJobWidget]', 'images/task-reject.svg'); + return; + } + + if (response) { + setupSearchTableEvents(false); + + const state = searchState.get(searchId); + const newRows = []; + + if (response.results) { + const results = response.results; + for (let i = 0; i < results.length; ++i) { + const result = results[i]; + const row = { + rowId: state.rowId, + descrLink: result.descrLink, + fileName: result.fileName, + fileSize: result.fileSize, + fileUrl: result.fileUrl, + nbLeechers: result.nbLeechers, + nbSeeders: result.nbSeeders, + siteUrl: result.siteUrl, + pubDate: result.pubDate, + }; + + newRows.push(row); + state.rows.push(row); + state.rowId += 1; + } + } + + // only update table if this search is currently being displayed + if (searchId === getSelectedSearchId()) { + for (const row of newRows) { + searchResultsTable.updateRowData(row); + } + + $('numSearchResultsVisible').set('html', searchResultsTable.getFilteredAndSortedRows().length); + $('numSearchResultsTotal').set('html', searchResultsTable.getRowIds().length); + + searchResultsTable.updateTable(); + searchResultsTable.altRow(); + } + + setupSearchTableEvents(true); + + if ((response.status === "Stopped") && (state.rowId >= response.total)) { + resetSearchState(searchId); + updateStatusIconElement(searchId, 'QBT_TR(Search has finished)QBT_TR[CONTEXT=SearchJobWidget]', 'images/task-complete.svg'); + return; + } + } + + clearTimeout(state.loadResultsTimer); + state.loadResultsTimer = loadSearchResultsData.delay(2000, this, searchId); + } + }).send(); + }; + + const updateSearchResultsData = function(searchId) { + const state = searchState.get(searchId); + clearTimeout(state.loadResultsTimer); + state.loadResultsTimer = loadSearchResultsData.delay(500, this, searchId); + }; + + new ClipboardJS('.copySearchDataToClipboard', { + text: function(trigger) { + switch (trigger.id) { + case "copySearchTorrentName": + return copySearchTorrentName(); + case "copySearchTorrentDownloadLink": + return copySearchTorrentDownloadLink(); + case "copySearchTorrentDescriptionUrl": + return copySearchTorrentDescriptionUrl(); + default: + return ""; + } + } + }); + + return exports(); +})(); + +Object.freeze(window.qBittorrent.Search); diff --git a/src/webui/www/private/views/search.html b/src/webui/www/private/views/search.html index cad0a8d0a..74085eae0 100644 --- a/src/webui/www/private/views/search.html +++ b/src/webui/www/private/views/search.html @@ -199,855 +199,3 @@ - - diff --git a/src/webui/www/webui.qrc b/src/webui/www/webui.qrc index 346f02fd5..0382ed08a 100644 --- a/src/webui/www/webui.qrc +++ b/src/webui/www/webui.qrc @@ -400,6 +400,7 @@ private/scripts/prop-trackers.js private/scripts/prop-webseeds.js private/scripts/rename-files.js + private/scripts/search.js private/scripts/speedslider.js private/setlocation.html private/shareratio.html From 4687b4e8e43de285ef9006b8c68d90415f078557 Mon Sep 17 00:00:00 2001 From: Chocobo1 Date: Mon, 27 May 2024 22:50:17 +0800 Subject: [PATCH 02/44] WebUI: enforce string quotes coding style --- src/webui/www/eslint.config.mjs | 18 +- src/webui/www/package.json | 4 +- src/webui/www/private/addpeers.html | 24 +- src/webui/www/private/addtrackers.html | 22 +- src/webui/www/private/confirmdeletion.html | 54 +- .../www/private/confirmfeeddeletion.html | 16 +- src/webui/www/private/confirmruleclear.html | 18 +- .../www/private/confirmruledeletion.html | 20 +- src/webui/www/private/download.html | 20 +- src/webui/www/private/downloadlimit.html | 30 +- src/webui/www/private/edittracker.html | 30 +- src/webui/www/private/newcategory.html | 58 +- src/webui/www/private/newfeed.html | 36 +- src/webui/www/private/newfolder.html | 36 +- src/webui/www/private/newrule.html | 38 +- src/webui/www/private/newtag.html | 36 +- src/webui/www/private/rename.html | 30 +- src/webui/www/private/rename_feed.html | 40 +- src/webui/www/private/rename_file.html | 46 +- src/webui/www/private/rename_files.html | 214 +-- src/webui/www/private/rename_rule.html | 44 +- src/webui/www/private/scripts/cache.js | 40 +- src/webui/www/private/scripts/client.js | 724 ++++---- src/webui/www/private/scripts/contextmenu.js | 332 ++-- src/webui/www/private/scripts/download.js | 56 +- src/webui/www/private/scripts/dynamicTable.js | 1462 ++++++++--------- src/webui/www/private/scripts/file-tree.js | 2 +- src/webui/www/private/scripts/filesystem.js | 10 +- .../www/private/scripts/localpreferences.js | 2 +- src/webui/www/private/scripts/misc.js | 14 +- src/webui/www/private/scripts/mocha-init.js | 318 ++-- src/webui/www/private/scripts/piecesbar.js | 44 +- src/webui/www/private/scripts/progressbar.js | 110 +- src/webui/www/private/scripts/prop-files.js | 142 +- src/webui/www/private/scripts/prop-general.js | 130 +- src/webui/www/private/scripts/prop-peers.js | 74 +- .../www/private/scripts/prop-trackers.js | 46 +- .../www/private/scripts/prop-webseeds.js | 26 +- src/webui/www/private/scripts/rename-files.js | 26 +- src/webui/www/private/scripts/search.js | 250 +-- src/webui/www/private/scripts/speedslider.js | 102 +- src/webui/www/private/setlocation.html | 34 +- src/webui/www/private/shareratio.html | 84 +- src/webui/www/private/upload.html | 18 +- src/webui/www/private/uploadlimit.html | 30 +- src/webui/www/private/views/about.html | 16 +- src/webui/www/private/views/aboutToolbar.html | 40 +- src/webui/www/private/views/filters.html | 30 +- .../private/views/installsearchplugin.html | 16 +- src/webui/www/private/views/log.html | 88 +- src/webui/www/private/views/preferences.html | 1238 +++++++------- .../www/private/views/preferencesToolbar.html | 52 +- src/webui/www/private/views/properties.html | 4 +- src/webui/www/private/views/rss.html | 200 +-- .../www/private/views/rssDownloader.html | 332 ++-- .../www/private/views/searchplugins.html | 38 +- src/webui/www/private/views/transferlist.html | 18 +- src/webui/www/public/scripts/login.js | 30 +- 58 files changed, 3510 insertions(+), 3502 deletions(-) diff --git a/src/webui/www/eslint.config.mjs b/src/webui/www/eslint.config.mjs index ba6f91a0d..121f8acb2 100644 --- a/src/webui/www/eslint.config.mjs +++ b/src/webui/www/eslint.config.mjs @@ -1,8 +1,8 @@ -import Globals from 'globals'; -import Html from 'eslint-plugin-html'; -import Js from '@eslint/js'; -import Stylistic from '@stylistic/eslint-plugin'; -import * as RegexpPlugin from 'eslint-plugin-regexp'; +import Globals from "globals"; +import Html from "eslint-plugin-html"; +import Js from "@eslint/js"; +import Stylistic from "@stylistic/eslint-plugin"; +import * as RegexpPlugin from "eslint-plugin-regexp"; export default [ Js.configs.recommended, @@ -38,6 +38,14 @@ export default [ } ], "Stylistic/nonblock-statement-body-position": ["error", "below"], + "Stylistic/quotes": [ + "error", + "double", + { + "avoidEscape": true, + "allowTemplateLiterals": true + } + ], "Stylistic/semi": "error" } } diff --git a/src/webui/www/package.json b/src/webui/www/package.json index e5f2acd14..bad08c881 100644 --- a/src/webui/www/package.json +++ b/src/webui/www/package.json @@ -6,8 +6,8 @@ "url": "https://github.com/qbittorrent/qBittorrent.git" }, "scripts": { - "format": "js-beautify -r private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js && prettier --write **.css", - "lint": "eslint private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js && stylelint **/*.css && html-validate private public" + "format": "js-beautify -r *.mjs private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js && prettier --write **.css", + "lint": "eslint *.mjs private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js && stylelint **/*.css && html-validate private public" }, "devDependencies": { "@stylistic/eslint-plugin": "*", diff --git a/src/webui/www/private/addpeers.html b/src/webui/www/private/addpeers.html index a86673d14..80801c8de 100644 --- a/src/webui/www/private/addpeers.html +++ b/src/webui/www/private/addpeers.html @@ -8,42 +8,42 @@ diff --git a/src/webui/www/private/edittracker.html b/src/webui/www/private/edittracker.html index feb5f5766..4fc76efc9 100644 --- a/src/webui/www/private/edittracker.html +++ b/src/webui/www/private/edittracker.html @@ -8,44 +8,44 @@ diff --git a/src/webui/www/private/upload.html b/src/webui/www/private/upload.html index ccdbc1124..72303d9ed 100644 --- a/src/webui/www/private/upload.html +++ b/src/webui/www/private/upload.html @@ -150,27 +150,27 @@
diff --git a/src/webui/www/private/uploadlimit.html b/src/webui/www/private/uploadlimit.html index 87d0db7b6..ef3b2731b 100644 --- a/src/webui/www/private/uploadlimit.html +++ b/src/webui/www/private/uploadlimit.html @@ -25,17 +25,17 @@ diff --git a/src/webui/www/private/views/about.html b/src/webui/www/private/views/about.html index 450a0a46e..e566106a8 100644 --- a/src/webui/www/private/views/about.html +++ b/src/webui/www/private/views/about.html @@ -841,18 +841,18 @@ diff --git a/src/webui/www/private/views/aboutToolbar.html b/src/webui/www/private/views/aboutToolbar.html index 3d9afd0e6..002b001f8 100644 --- a/src/webui/www/private/views/aboutToolbar.html +++ b/src/webui/www/private/views/aboutToolbar.html @@ -11,39 +11,39 @@ diff --git a/src/webui/www/private/views/filters.html b/src/webui/www/private/views/filters.html index 9b65dfa40..f0ad436e5 100644 --- a/src/webui/www/private/views/filters.html +++ b/src/webui/www/private/views/filters.html @@ -42,7 +42,7 @@ diff --git a/src/webui/www/private/views/properties.html b/src/webui/www/private/views/properties.html index 473221ed1..350f569ad 100644 --- a/src/webui/www/private/views/properties.html +++ b/src/webui/www/private/views/properties.html @@ -168,10 +168,10 @@ diff --git a/src/webui/www/private/views/filters.html b/src/webui/www/private/views/filters.html index f0ad436e5..cf73c2ba3 100644 --- a/src/webui/www/private/views/filters.html +++ b/src/webui/www/private/views/filters.html @@ -44,9 +44,8 @@