Allow user to search for more results from UI

This commit is contained in:
Jaifroid 2025-06-12 07:23:42 +01:00
parent 54640dc6a7
commit cc77c59e47
4 changed files with 82 additions and 53 deletions

View File

@ -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('<p>We are switching to the Private File System (OPFS).</p>' +
@ -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] <a href="#">stop</a>' : '');
} else if (nbDirEntry >= params.maxSearchResultsSize) {
message = 'First ' + params.maxSearchResultsSize + (reportingSearch.searchUrlIndex ? ' assets' : ' articles') + ' found: refine your search.';
(reportingSearch.scanCount ? ' [scanning ' + reportingSearch.scanCount + ' titles] <a href="#" id="stopScan">stop</a>' : '');
} 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 <a href="#searchSyntaxError" id="searchSyntaxLink">Search syntax</a> 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] ' +
'<a href="#" id="getMoreResults">Get more</a>';
}
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,

View File

@ -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';

View File

@ -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
};

View File

@ -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<callbackDirEntry>} 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) {