diff --git a/www/js/app.js b/www/js/app.js index afe6ab89..6621e9c1 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -224,8 +224,6 @@ function onPointerUp (e) { if (/UWP/.test(params.appType)) document.body.addEventListener('pointerup', onPointerUp); -var searchArticlesFocused = false; - document.getElementById('searchArticles').addEventListener('click', function () { var val = prefix.value; // Do not initiate the same search if it is already in progress @@ -243,8 +241,6 @@ document.getElementById('searchArticles').addEventListener('click', function () var headerHeight = document.getElementById('top').getBoundingClientRect().height; var footerHeight = document.getElementById('footer').getBoundingClientRect().height; scrollbox.style.height = window.innerHeight - headerHeight - footerHeight + 'px'; - // This flag is set to true in the mousedown event below - searchArticlesFocused = false; }); document.getElementById('formArticleSearch').addEventListener('submit', function () { document.getElementById('searchArticles').click(); @@ -342,19 +338,16 @@ prefix.addEventListener('focus', function () { }); // Hide the search results if user moves out of prefix field prefix.addEventListener('blur', function () { - if (!searchArticlesFocused) { - appstate.search.status = 'cancelled'; - } // We need to wait one tick for the activeElement to receive focus - setTimeout(function () { - if (!(/^articleList|searchSyntaxLink/.test(document.activeElement.id) || - /^list-group/.test(document.activeElement.className))) { - scrollbox.style.height = 0; - document.getElementById('articleListWithHeader').style.display = 'none'; - appstate.tempPrefix = ''; - uiUtil.clearSpinner(); - } - }, 1); + setTimeout(function () { + if (!(/^articleList|searchSyntaxLink/.test(document.activeElement.id) || + /^list-group/.test(document.activeElement.className))) { + scrollbox.style.height = 0; + document.getElementById('articleListWithHeader').style.display = 'none'; + appstate.tempPrefix = ''; + uiUtil.clearSpinner(); + } + }, 1); }); // Add keyboard shortcuts @@ -685,7 +678,7 @@ document.getElementById('btnRescanDeviceStorage').addEventListener('click', func displayFileSelect(); } // Check if we are in an Android app, and if so, auto-select use of OPFS if there is no set value in settingsStore for useOPFS - if ((/Android/.test(params.appType) || /Firefox/.test(navigator.userAgent)) && !params.useOPFS && !settingsStore.getItem('useOPFS')) { + if ((/Android/.test(params.appType) || /Firefox/.test(navigator.userAgent)) && !params.useOPFS && !settingsStore.hasItem('useOPFS')) { // This will only run first time app is run on Android setTimeout(function () { uiUtil.systemAlert('

We are switching to the Private File System (OPFS).

' + @@ -701,11 +694,13 @@ document.getElementById('btnRescanDeviceStorage').addEventListener('click', func } }); }, 2000); - } else if (!settingsStore.getItem('useOPFS')) { - // This esnures that there is an explicit setting for useOPFS, which in turn allows us to tell if the + } else if (!settingsStore.hasItem('useOPFS')) { + // This ensures that there is an explicit setting for useOPFS, which in turn allows us to tell if the // app is running for the first time (so we don't keep prompting the user to use the OPFS) settingsStore.setItem('useOPFS', false, Infinity); } + // Since we may have changed the storage type, we should recalculate the max search size + uiUtil.dynamicallySetMaxSearchResults(); }); // Bottom bar : // @TODO Since bottom bar now hidden in Settings and About the returntoArticle code cannot be accessed; @@ -1675,6 +1670,8 @@ function setOPFSUI () { btnDeleteOPFSEntry.style.display = 'none'; btnExportOPFSEntry.style.display = 'none'; } + // Enabling or disabling the OPFS affects the maximum number of search results we should return + uiUtil.dynamicallySetMaxSearchResults(); } // Set the OPFS UI on app launch @@ -1940,6 +1937,7 @@ if (window.electronAPI) { document.getElementById('libzimSearchType').addEventListener('change', function (e) { params.libzimSearchType = e.target.checked ? 'searchWithSnippets' : 'search'; settingsStore.setItem('libzimSearchType', params.libzimSearchType, Infinity); + uiUtil.dynamicallySetMaxSearchResults(); }); document.getElementById('disableDragAndDropCheck').addEventListener('change', function () { @@ -4792,12 +4790,12 @@ function listenForSearchKeys () { * with a binary search inside the index file) * @param {String} prefix The string that must appear at the start of any title searched for */ -function searchDirEntriesFromPrefix (prefix) { +function searchDirEntriesFromPrefix (prefix, size) { if (appstate.selectedArchive !== null && appstate.selectedArchive.isReady()) { // Cancel the old search (zimArchive search object will receive this change) appstate.search.status = 'cancelled'; // Initiate a new search object and point appstate.search to it (the zimAcrhive search object will continue to point to the old object) - appstate.search = { prefix: prefix, status: 'init', type: '', size: params.maxSearchResultsSize }; + appstate.search = { prefix: prefix, status: 'init', type: '', size: size || params.maxSearchResultsSize }; uiUtil.hideActiveContentWarning(); if (!prefix || /^\s/.test(prefix)) { var sel = prefix ? prefix.replace(/^\s(.*)/, '$1') : ''; @@ -4975,23 +4973,49 @@ function populateListOfArticles (dirEntryArray, reportingSearch) { var message; if (stillSearching) { message = 'Searching [' + appstate.search.type + ']... found: ' + nbDirEntry + '...' + - (reportingSearch.scanCount ? ' [scanning ' + reportingSearch.scanCount + ' titles] stop' : ''); - } else if (nbDirEntry >= params.maxSearchResultsSize) { - message = 'First ' + params.maxSearchResultsSize + (reportingSearch.searchUrlIndex ? ' assets' : ' articles') + ' found: refine your search.'; + (reportingSearch.scanCount ? ' [scanning ' + reportingSearch.scanCount + ' titles] stop' : ''); + } else if (nbDirEntry >= reportingSearch.size) { + message = 'First ' + reportingSearch.size + (reportingSearch.searchUrlIndex ? ' assets' : ' articles') + + ' found: refine your search.'; } else if (reportingSearch.status === 'error') { message = 'Incorrect search syntax! See Search syntax in About!'; } else { message = 'Finished. ' + (nbDirEntry || 'No') + ' articles found' + (appstate.search.type === 'basic' ? ': try fewer words for full search.' : '.'); } - if (!stillSearching && reportingSearch.scanCount) message += ' [scanned ' + reportingSearch.scanCount + ' titles]'; + if (!stillSearching && reportingSearch.scanCount) { + message += ' [scanned ' + reportingSearch.scanCount + ' titles] ' + + 'Get more'; + } articleListHeaderMessageDiv.innerHTML = message; - if (stillSearching && reportingSearch.countReport) return; + // Add event listener for stopScan link + var stopScanElement = document.getElementById('stopScan'); + if (stopScanElement && !stopScanElement.hasAttribute('data-listener-added')) { + stopScanElement.addEventListener('mousedown', function (e) { + e.preventDefault(); + appstate.search.status = 'cancelled'; + }); + stopScanElement.setAttribute('data-listener-added', 'true'); + } + // Add event listener for getMoreResults link + var getMoreResultsElement = document.getElementById('getMoreResults'); + if (getMoreResultsElement && !getMoreResultsElement.hasAttribute('data-listener-added')) { + getMoreResultsElement.addEventListener('mousedown', function (e) { + e.preventDefault(); + // Temporarily increase the search window by params.maxSearchResultsSize + var temporarySearchSize = appstate.search.size + params.maxSearchResultsSize; + // Rerun the search with the current prefix + searchDirEntriesFromPrefix(appstate.search.prefix, temporarySearchSize); + }); + getMoreResultsElement.setAttribute('data-listener-added', 'true'); + } + + if (stillSearching && reportingSearch.countReport) return; var articleListDiv = document.getElementById('articleList'); var articleListDivHtml = ''; - var listLength = dirEntryArray.length < params.maxSearchResultsSize ? dirEntryArray.length : params.maxSearchResultsSize; + var listLength = dirEntryArray.length < reportingSearch.size ? dirEntryArray.length : reportingSearch.size; for (var i = 0; i < listLength; i++) { var dirEntry = dirEntryArray[i]; // NB We use encodeURIComponent rather than encodeURI here because we know that any question marks in the title are not querystrings, diff --git a/www/js/init.js b/www/js/init.js index c0c998fe..6c79587c 100644 --- a/www/js/init.js +++ b/www/js/init.js @@ -96,8 +96,8 @@ params['PWAServer'] = 'https://pwa.kiwix.org/'; // Production server params['storeType'] = getBestAvailableStorageAPI(); params['appType'] = getAppType(); params['keyPrefix'] = 'kiwixjs-'; // Prefix to use for localStorage keys -// Maximum number of article titles to return (range is 5 - 100, default 20), but see intelligent search-size calculation below -params['maxSearchResultsSize'] = ~~(getSetting('maxSearchResultsSize') || 20); +// Maximum number of article titles to return (range is 5 - 100, default 15), but see intelligent search-size calculation below +params['maxSearchResultsSize'] = ~~(getSetting('maxSearchResultsSize') || 15); params['relativeFontSize'] = ~~(getSetting('relativeFontSize') || 100); // Sets the initial font size for articles (as a percentage) - user can adjust using zoom buttons params['relativeUIFontSize'] = ~~(getSetting('relativeUIFontSize') || 100); // Sets the initial font size for UI (as a percentage) - user can adjust using slider in Config params['cssSource'] = getSetting('cssSource') || 'auto'; // Set default to "auto", "desktop" or "mobile" @@ -244,25 +244,6 @@ if (getSetting('lastPageLoad') === 'failed') { setSetting('lastPageLoad', 'failed'); } -// Use intelligent search-size calculation based on app type and environment -// This is because fulltext search with snippets is slower than basic fulltext search, and excruciatingly slow on Android if not using the OPFS -if (!getSetting('maxSearchResultsSize')) { - if (params.libzimSearchType === 'search') { - // If the user has set the search type to basic search, we can use a larger number of results - params.maxSearchResultsSize = 30; - } - if (/Android/.test(params.appType)) { - if (params.useOPFS) { - // Android with OPFS can handle more results: 15 with snippets, 20 with basic search - params.maxSearchResultsSize = params.libzimSearchType === 'search' ? 20 : 15; - } else { - // Android without OPFS needs restricted results and basic search - params.maxSearchResultsSize = 10; - params.libzimSearchType = getSetting('libzimSearchType') || 'search'; - } - } -} - // Initialize checkbox, radio and other values document.getElementById('cssCacheModeCheck').checked = params.cssCache; document.getElementById('navButtonsPosCheck').checked = params.navButtonsPos === 'top'; diff --git a/www/js/lib/uiUtil.js b/www/js/lib/uiUtil.js index d7900d7c..74e30fc7 100644 --- a/www/js/lib/uiUtil.js +++ b/www/js/lib/uiUtil.js @@ -26,6 +26,7 @@ /* global webpMachine, params, appstate, Windows */ import util from './util.js'; +import settingsStore from './settingsStore.js'; /** * Global variables @@ -1646,8 +1647,7 @@ function attachArticleListEventListeners (findDirEntryCallback, appstate) { hoverTimeout = null; } // Safety check: ensure the element still has the expected children - if (element.children.length < 2) return; - + // if (element.children.length < 2) return; // Always collapse on mouse leave // var header = element.children[0]; // var content = element.children[1]; @@ -1658,6 +1658,29 @@ function attachArticleListEventListeners (findDirEntryCallback, appstate) { }); } +/** + * Use intelligent search-size calculation based on app type and environment. This is because fulltext search with snippets + * is slower than basic fulltext search, and excruciatingly slow on Android if not using the OPFS + */ +function dynamicallySetMaxSearchResults () { + if (!settingsStore.hasItem('maxSearchResultsSize')) { + if (params.libzimSearchType === 'search') { + // If the user has set the search type to basic search, we can use a larger number of results + params.maxSearchResultsSize = 25; + } + if (/Android/.test(params.appType)) { + if (params.useOPFS) { + // Android with OPFS can handle more results: 15 with snippets, 20 with basic search + params.maxSearchResultsSize = params.libzimSearchType === 'search' ? 15 : 12; + } else { + // Android without OPFS needs restricted results and basic search + params.maxSearchResultsSize = 10; + params.libzimSearchType = settingsStore.getItem('libzimSearchType') || 'search'; + } + } + } +} + /** * Functions and classes exposed by this module */ @@ -1699,5 +1722,6 @@ export default { handleTitleClick: handleTitleClick, createSnippetElements: createSnippetElements, toggleSnippet: toggleSnippet, - attachArticleListEventListeners: attachArticleListEventListeners + attachArticleListEventListeners: attachArticleListEventListeners, + dynamicallySetMaxSearchResults: dynamicallySetMaxSearchResults }; diff --git a/www/js/lib/zimArchive.js b/www/js/lib/zimArchive.js index c31c0e4c..571ee0e3 100644 --- a/www/js/lib/zimArchive.js +++ b/www/js/lib/zimArchive.js @@ -575,7 +575,7 @@ ZIMArchive.prototype.findDirEntriesWithPrefixCaseSensitive = function (prefix, s * * @param {Object} search The appstate.search object * @param {Array} dirEntries The array of already found Directory Entries - * @param {Integer} number Optional positive number of search results requested (otherwise params.maxSearchResults will be used) + * @param {Integer} number Override number of results requested in search object (used to get remaining results) * @returns {Promise} The augmented array of Directory Entries with titles that correspond to search */ ZIMArchive.prototype.findDirEntriesFromFullTextSearch = function (search, dirEntries, number) { @@ -583,7 +583,7 @@ ZIMArchive.prototype.findDirEntriesFromFullTextSearch = function (search, dirEnt var that = this; // We give ourselves an overhead in caclulating the results needed, because full-text search will return some results already found // var resultsNeeded = Math.floor(params.maxSearchResultsSize - dirEntries.length / 2); - var resultsNeeded = number || params.maxSearchResultsSize; + var resultsNeeded = number || search.size; var searchType = params.libzimSearchType || 'search'; return this.callLibzimWorker({ action: searchType, text: search.prefix, numResults: resultsNeeded }).then(function (returned) { if (returned) {