From e126ea20b089fc90a1e2d8b1f845568f13b0a751 Mon Sep 17 00:00:00 2001 From: RG Date: Tue, 10 Oct 2023 21:04:27 +0530 Subject: [PATCH 01/45] [ADD] event listener for FS API --- www/js/app.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/www/js/app.js b/www/js/app.js index d59e3c855..f0a914811 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -1270,7 +1270,18 @@ function displayFileSelect () { globalDropZone.addEventListener('drop', handleFileDrop); } // This handles use of the file picker - document.getElementById('archiveFiles').addEventListener('change', setLocalArchiveFromFileSelect); + if (typeof window.showOpenFilePicker === 'function') { + document.getElementById('archiveFiles').addEventListener('click', function (e) { + e.preventDefault(); + window.showOpenFilePicker({ multiple: true }).then(function (fileHandle) { + fileHandle[0].getFile().then(function (file) { + setLocalArchiveFromFileList([file]); + }); + }); + }) + } else { + document.getElementById('archiveFiles').addEventListener('change', setLocalArchiveFromFileSelect); + } } function handleGlobalDragover (e) { From cae1b9bd849d48dbad4abb0142871a782656214b Mon Sep 17 00:00:00 2001 From: RG Date: Tue, 10 Oct 2023 21:11:41 +0530 Subject: [PATCH 02/45] [ADD] cache API forked from `kiwix-js-windows` --- .gitignore | 1 + www/js/lib/cache.js | 975 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 976 insertions(+) create mode 100644 www/js/lib/cache.js diff --git a/.gitignore b/.gitignore index 69d180f3b..7db72b2ca 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,4 @@ www-ghdeploy /.vs/ /.vscode/ scripts/github_token +.prettierrc diff --git a/www/js/lib/cache.js b/www/js/lib/cache.js new file mode 100644 index 000000000..33654ce84 --- /dev/null +++ b/www/js/lib/cache.js @@ -0,0 +1,975 @@ +/** + * cache.js : Provide a cache for assets from the ZIM archive using indexedDB, localStorage or memory cache + * + * Copyright 2018 Mossroy, Jaifroid and contributors + * License GPL v3: + * + * This file is part of Kiwix. + * + * Kiwix is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Kiwix is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Kiwix (file LICENSE-GPLv3.txt). If not, see + */ + +/* globals params, appstate, caches, assetsCache */ + +'use strict'; +import settingsStore from './settingsStore.js'; +import uiUtil from './uiUtil.js'; + +const CACHEAPI = params.cacheAPI; // Set the database or cache name here, and synchronize with Service Worker +const CACHEIDB = params.cacheIDB; // Slightly different name to disambiguate +var objStore = 'kiwix-assets'; // Name of the object store +const APPCACHE = 'kiwix-appCache-' + params.appVersion; // Ensure this is the same as in Service Worker + +// DEV: Regex below defines the permitted MIME types for the cache; add further types as needed +var regexpMimeTypes = /\b(?:javascript|css|ico|html)\b/; + +/** + * Tests the enviornment's caching capabilities and sets assetsCache.capability to the supported level + * + * @param {Function} callback Function to indicate that the capability level has been set + */ +function test (callback) { + // Test for indexedDB capability + if (typeof assetsCache.capability !== 'undefined') { + callback(true); + return; + } + // Set baseline capability + assetsCache.capability = 'memory'; + idxDB('count', function (result) { + if (result !== false) { + assetsCache.capability = 'indexedDB|' + assetsCache.capability; + } else { + console.log('inexedDB is not supported'); + } + // Test for Cache API + if ('caches' in window && /https?:/i.test(window.location.protocol)) { + assetsCache.capability = 'cacheAPI|' + assetsCache.capability; + } else { + console.log('CacheAPI is not supported' + (/https?:/i.test(window.location.protocol) ? '' + : ' with the ' + window.location.protocol + ' protocol')); + } + // Test for localCache capability (this is a fallback, indexedDB is preferred because it permits more storage) + if (typeof Storage !== 'undefined') { + try { + // If localStorage is really supported, this won't produce an error + var item = window.localStorage.length; + assetsCache.capability = assetsCache.capability + '|localStorage'; + } catch (err) { + console.log('localStorage is not supported'); + } + } + console.log('Setting storage type to ' + assetsCache.capability.match(/^[^|]+/)[0]); + if (/localStorage/.test(assetsCache.capability)) { + console.debug("DEV: 'UnknownError' may be produced as part of localStorage capability detection"); + } + callback(result); + }); +} + +/** + * Counts the numnber of cached assets + * + * @param {Function} callback which will receive an array containing [cacheType, cacheCount] + */ +function count (callback) { + test(function (result) { + var type = null; + var description = null; + var cacheCount = null; + + switch (assetsCache.capability.match(/^[^|]+/)[0]) { + case 'memory': + type = 'memory'; + description = 'Memory'; + cacheCount = assetsCache.size; + break; + case 'localStorage': + type = 'localStorage'; + description = 'LocalStorage'; + cacheCount = localStorage.length; + break; + case 'indexedDB': + type = 'indexedDB'; + description = 'IndexedDB'; + // Sometimes we already have the count as a result of test, so no need to look again + if (typeof result !== 'boolean' && (result === 0 || result > 0)) { + cacheCount = result; + } else { + idxDB('count', function (cacheCount) { + callback({ type: type, description: description, count: cacheCount }); + }); + } + break; + case 'cacheAPI': + type = 'cacheAPI'; + description = 'CacheAPI'; + caches.open(CACHEAPI).then(function (cache) { + cache.keys().then(function (keys) { + callback({ type: type, description: description, count: keys.length }); + }); + }); + break; + default: + // User has turned off caching + type = 'none'; + description = 'None'; + cacheCount = 'null'; + } + + if (cacheCount || cacheCount === 0) { + callback({ type: type, description: description, count: cacheCount }); + } + }); + // Refresh instructions to Service Worker + if (navigator.serviceWorker && navigator.serviceWorker.controller) { + // Create a Message Channel + var channel = new MessageChannel(); + navigator.serviceWorker.controller.postMessage({ + action: { + assetsCache: params.assetsCache ? 'enable' : 'disable', + appCache: params.appCache ? 'enable' : 'disable', + checkCache: window.location.href + } + }, [channel.port2]); + } +} + +/** + * Opens an IndexedDB database and adds or retrieves a key-value pair to it, or performs utility commands + * on the database + * + * @param {String} keyOrCommand The key of the value to be written or read, or commands 'clear' (clears objStore), + * 'count' (counts number of objects in objStore), 'delete' (deletes a record with key passed in valueOrCallback), + * 'deleteNonCurrent' (deletes all databases that do not match CACHEIDB - but only works in Chromium currently) + * @param {Variable} valueOrCallback The value to write, or a callback function for read and command transactions + * @param {Function} callback Callback for write transactions only - mandatory for delete and write transactions + */ +function idxDB (keyOrCommand, valueOrCallback, callback) { + var value = callback ? valueOrCallback : null; + var rtnFn = callback || valueOrCallback; + if (typeof window.indexedDB === 'undefined') { + rtnFn(false); + return; + } + + // Delete all non-curren IdxDB databases (only works in Chromium currently) + if (keyOrCommand === 'deleteNonCurrent') { + if (indexedDB.databases) { + var result = 0; + indexedDB.databases().then(function (dbs) { + dbs.forEach(function (db) { + if (db.name !== CACHEIDB) { + result++; + indexedDB.deleteDatabase(db.name); + } + }); + }).then(function () { + rtnFn(result); + }); + } else { + rtnFn(false); + } + return; + } + + // Open (or create) the database + var open = indexedDB.open(CACHEIDB, 1); + + open.onerror = function (e) { + // Suppress error reporting if testing (older versions of Firefox support indexedDB but cannot use it with + // the file:// protocol, so will report an error) + if (assetsCache.capability !== 'test') { + console.error('IndexedDB failed to open: ' + open.error.message); + } + rtnFn(false); + }; + + // Create the schema + open.onupgradeneeded = function () { + var db = open.result; + var store = db.createObjectStore(objStore); + }; + + open.onsuccess = function () { + // Start a new transaction + var db = open.result; + + // Set the store to readwrite or read only according to presence or not of value variable + var tx = value !== null || /clear|delete/.test(keyOrCommand) ? db.transaction(objStore, 'readwrite') : db.transaction(objStore); + var store = tx.objectStore(objStore); + + var processData; + // Process commands + if (keyOrCommand === 'clear') { + // Delete all keys and values in the store + processData = store.clear(); + } else if (keyOrCommand === 'count') { + // Count the objects in the store + processData = store.count(); + } else if (keyOrCommand === 'delete') { + // Delete the record with key set to value + processData = store.delete(value); + } else { + // Request addition or retrieval of data + processData = value !== null ? store.put(value, keyOrCommand) : store.get(keyOrCommand); + } + // Call the callback with the result + processData.onsuccess = function (e) { + if (keyOrCommand === 'delete') { + rtnFn(true); + } else { + rtnFn(processData.result); + } + }; + processData.onerror = function (e) { + console.error('IndexedDB command failed: ' + processData.error); + rtnFn(false); + }; + + // Close the db when the transaction is done + tx.oncomplete = function () { + db.close(); + }; + }; +} + +/** + * Opens a CacheAPI cache and adds or retrieves a key-value pair to it, or performs utility commands + * on the cache. This interface also allows the use of callbacks inside the Cache Promise API for ease of + * interoperability with the interface for idxDB code above. + * + * @param {String} keyOrCommand The key of the value to be written or read, or commands 'clear' (clears cache), + * 'delete' (deletes a record with key passed in valueOrCallback) + * @param {Variable} valueOrCallback The value to write, or a callback function for read and command transactions + * @param {Function} callback Callback for write transactions only + * @param {String} mimetype The MIME type of any content to be stored + */ +function cacheAPI (keyOrCommand, valueOrCallback, callback, mimetype) { + var value = callback ? valueOrCallback : null; + var rtnFn = callback || valueOrCallback; + // Process commands + if (keyOrCommand === 'clear') { + caches.delete(CACHEAPI).then(rtnFn); + } else if (keyOrCommand === 'delete') { + caches.open(CACHEAPI).then(function (cache) { + cache.delete(value).then(rtnFn); + }); + } else if (value === null) { + // Request retrieval of data + caches.open(CACHEAPI).then(function (cache) { + cache.match('../' + keyOrCommand).then(function (response) { + if (!response) { + rtnFn(null); + } else { + response.text().then(function (data) { + rtnFn(data); + }); + } + }).catch(function (err) { + console.error('Unable to match assets from Cache API!', err); + rtnFn(null); + }); + }); + } else { + // Request storing of data in cache + caches.open(CACHEAPI).then(function (cache) { + var contentLength; + if (typeof value === 'string') { + var m = encodeURIComponent(value).match(/%[89ABab]/g); + contentLength = value.length + (m ? m.length : 0); + } else { + contentLength = value.byteLength || value.length; + } + var headers = new Headers(); + if (contentLength) headers.set('Content-Length', contentLength); + // Prevent CORS issues in PWAs + // if (contentLength) headers.set('Access-Control-Allow-Origin', '*'); + headers.set('Content-Security-Policy', 'sandbox allow-scripts allow-same-origin allow-modals allow-popups allow-forms'); + if (mimetype) headers.set('Content-Type', mimetype); + var responseInit = { + status: 200, + statusText: 'OK', + headers: headers + }; + var httpResponse = new Response(value, responseInit); + cache.put('../' + keyOrCommand, httpResponse).then(function () { + rtnFn(true); + }).catch(function (err) { + console.error('Unable to store assets in Cache API!', err); + rtnFn(null); + }); + }); + } +} + +/** + * Stores information about the last visited page in a cookie and, if available, in localStorage or indexedDB + * + * @param {String} zimFile The filename (or name of first file in set) of the ZIM archive + * @param {String} article The URL of the article (including namespace) + * @param {String} content The content of the page to be stored + * @param {Function} callback Callback function to report the outcome of the operation + */ +function setArticle (zimFile, article, content, callback) { + // Prevent storage if user has deselected the option in Configuration + if (!params.rememberLastPage) { + callback(-1); + return; + } + settingsStore.setItem(zimFile, article, Infinity); + setItem(zimFile, content, 'text/html', function (response) { + callback(response); + }); +} + +/** + * Retrieves article contents from cache only if the article's key has been stored in settings store + * (since checking the store is synchronous, it prevents unnecessary async cache lookups) + * + * @param {String} zimFile The filename (or name of first file in set) of the ZIM archive + * @param {String} article The URL of the article to be retrieved (including namespace) + * @param {Function} callback The function to call with the result + */ +function getArticle (zimFile, article, callback) { + if (settingsStore.getItem(zimFile) === article) { + getItem(zimFile, callback); + } else { + callback(false); + } +} + +/** + * Caches the contents of an asset in memory or local storage + * + * @param {String} key The database key of the asset to cache + * @param {String} contents The file contents to be stored in the cache + * @param {String} mimetype The MIME type of the contents + * @param {Function} callback Callback function to report outcome of operation + * @param {Boolean} isAsset Optional indicator that a file is an asset + */ +function setItem (key, contents, mimetype, callback, isAsset) { + // Prevent use of storage if user has deselected the option in Configuration + // or if the asset is of the wrong type + if (params.assetsCache === false || !regexpMimeTypes.test(mimetype)) { + callback(-1); + return; + } + // Check if we're actually setting an article + var keyArticle = key.match(/([^/]+)\/([AC]\/.+$)/); + if (keyArticle && !isAsset && /\bx?html\b/i.test(mimetype) && !/\.(png|gif|jpe?g|css|js|mpe?g|webp|webm|woff2?|eot|mp[43])(\?|$)/i.test(key)) { // We're setting an article, so go to setArticle function + setArticle(keyArticle[1], keyArticle[2], contents, callback); + return; + } + if (/^localStorage/.test(assetsCache.capability)) { + localStorage.setItem(key, contents); + } else { + assetsCache.set(key, contents); + } + if (/^indexedDB/.test(assetsCache.capability)) { + idxDB(key, contents, function (result) { + callback(result); + }); + } else if (/^cacheAPI/.test(assetsCache.capability)) { + cacheAPI(key, contents, function (result) { + callback(result); + }, mimetype); + } else { + callback(key); + } +} + +/** + * Retrieves a ZIM file asset that has been cached with the addItem function + * either from the memory cache or local storage + * + * @param {String} key The database key of the asset to retrieve + * @param {Function} callback The function to call with the result + */ +function getItem (key, callback) { + // Only look up assets of the type stored in the cache + if (params.assetsCache === false) { + callback(false); + return; + } + // Check if we're actually calling an article + // DEV: With new ZIM types, we can't know we're retrieving an article... + // var keyArticle = key.match(/([^/]+)\/(A\/.+$)/); + // if (keyArticle) { // We're retrieving an article, so go to getArticle function + // getArticle(keyArticle[1], keyArticle[2], callback); + // return; + // } + var contents = null; + if (assetsCache.has(key)) { + contents = assetsCache.get(key); + callback(contents); + } else if (/^localStorage/.test(assetsCache.capability)) { + contents = localStorage.getItem(key); + callback(contents); + } else if (/^cacheAPI/.test(assetsCache.capability)) { + cacheAPI(key, function (contents) { + callback(contents); + }); + } else if (/^indexedDB/.test(assetsCache.capability)) { + idxDB(key, function (contents) { + if (typeof contents !== 'undefined') { + // Also store in fast memory cache to prevent repaints + assetsCache.set(key, contents); + } + callback(contents); + }); + } else { + callback(contents); + } +} + +/** + * Gets an item from the cache, or extracts it from the ZIM if it is not cached. After extracting + * an item from the ZIM, it is added to the cache if it is of the type specified in regexpKeyTypes. + * + * @param {Object} selectedArchive The ZIM archive picked by the user + * @param {String} key The cache key of the item to retrieve + * @param {Object} dirEntry If the item's dirEntry has already been looked up, it can optionally be + * supplied here (saves a redundant dirEntry lookup) + * @returns {Promise} A Promise for the content + */ +function getItemFromCacheOrZIM (selectedArchive, key, dirEntry) { + return new Promise(function (resolve, reject) { + // First check if the item is already in the cache + var title = key.replace(/^[^/]+\//, ''); + getItem(key, function (result) { + if (result !== null && result !== false && typeof result !== 'undefined') { + // console.debug("Cache supplied " + title); + if (/\.css$/.test(title)) { + assetsCache.cssLoading--; + if (assetsCache.cssLoading <= 0) { + document.getElementById('articleContent').style.display = 'block'; + } + } + resolve(result); + return; + } + // Bypass getting dirEntry if we already have it + var getDirEntry = dirEntry ? Promise.resolve() + : selectedArchive.getDirEntryByPath(title); + // Read data from ZIM + getDirEntry.then(function (resolvedDirEntry) { + if (dirEntry) resolvedDirEntry = dirEntry; + if (resolvedDirEntry === null) { + console.log('Error: asset file not found: ' + title); + resolve(null); + } else { + var mimetype = resolvedDirEntry.getMimetype(); + if (resolvedDirEntry.nullify) { + console.debug('Zimit filter prevented access to ' + resolvedDirEntry.url + '. Storing empty contents in cache.'); + setItem(key, '', mimetype, function () { }); + resolve(''); + return; + } + var shortTitle = key.replace(/[^/]+\//g, '').substring(0, 18); + // Since there was no result, post UI messages and look up asset in ZIM + if (/\bx?html\b/.test(mimetype) && !resolvedDirEntry.isAsset && + !/\.(png|gif|jpe?g|svg|css|js|mpe?g|webp|webm|woff2?|eot|mp[43])(\?|$)/i.test(resolvedDirEntry.url)) { + uiUtil.pollSpinner('Loading ' + shortTitle + '...'); + } else if (/(css|javascript|video|vtt)/i.test(mimetype)) { + uiUtil.pollSpinner('Getting ' + shortTitle + '...'); + } + // Set the read function to use according to filetype + var readFile = /\b(?:x?html|css|javascript)\b/i.test(mimetype) + ? selectedArchive.readUtf8File : selectedArchive.readBinaryFile; + readFile(resolvedDirEntry, function (fileDirEntry, content) { + if (regexpMimeTypes.test(mimetype)) { + console.debug('Cache retrieved ' + title + ' from ZIM'); + // Process any pre-cache transforms + content = transform(content, title.replace(/^.*\.([^.]+)$/, '$1')); + } + // Hide article while it is rendering + if (!fileDirEntry.isAsset && /\bx?html\b/i.test(mimetype) && !/\.(png|gif|jpe?g|svg|css|js|mpe?g|webp|webm|woff2?|eot|mp[34])(\?|$)/i.test(key)) { + // Count CSS so we can attempt to show article before JS/images are fully loaded + var cssCount = content.match(/<(?:link)[^>]+?href=["']([^"']+)[^>]+>/ig); + assetsCache.cssLoading = cssCount ? cssCount.length : 0; + if (assetsCache.cssLoading) document.getElementById('articleContent').style.display = 'none'; + } + if (/\bcss\b/i.test(mimetype)) { + assetsCache.cssLoading--; + if (assetsCache.cssLoading <= 0) { + document.getElementById('articleContent').style.display = 'block'; + } + } + setItem(key, content, mimetype, function (result) { + if (result === -1) { + // Cache rejected item due to user settings + } else if (result) { + console.log('Cache: stored asset ' + title); + } else { + console.error('Cache: failed to store asset ' + title); + } + }, fileDirEntry.isAsset); + resolve(content); + }); + } + }).catch(function (e) { + reject(new Error('Could not find DirEntry for asset ' + title, e)); + }); + }); + }); +} + +/** + * Clears caches (including cookie) according to the scope represented by the 'items' variable + * + * @param {String} items 'lastpages' (last visited pages of various archives), 'all' or 'reset' + * @param {Function} callback Callback function to report the number of items cleared + */ +function clear (items, callback) { + if (!/lastpages|all|reset/.test(items)) { + if (callback) callback(false); + return; + } + // Delete cookie entries with a key containing '.zim' or '.zimaa' etc. followed by article namespace + var itemsCount = 0; + var key; + var capability = assetsCache.capability; + var zimRegExp = /(?:^|;)\s*([^=]+)=([^;]*)/ig; + var currentCookies = document.cookie; + var cookieCrumb = zimRegExp.exec(currentCookies); + while (cookieCrumb !== null) { + if (/\.zim\w{0,2}=/i.test(decodeURIComponent(cookieCrumb[0]))) { + key = cookieCrumb[1]; + // This expiry date will cause the browser to delete the cookie on next page refresh + document.cookie = key + '=;expires=Thu, 21 Sep 1979 00:00:01 UTC;'; + if (items === 'lastpages') { + assetsCache.delete(key); + // See note on loose test below + if (/localStorage/.test(capability)) { + localStorage.removeItem(key); + } + if (/indexedDB/.test(capability)) { + idxDB('delete', key, function () { }); + } + if (/cacheAPI/.test(capability)) { + cacheAPI('delete', key, function () { }); + } + itemsCount++; + } + } + cookieCrumb = zimRegExp.exec(currentCookies); + } + if (items === 'all' || items === 'reset') { + var result; + if (/^(memory|indexedDB|cacheAPI)/.test(capability)) { + itemsCount += assetsCache.size; + result = 'assetsCache'; + } + // Delete and reinitialize assetsCache + assetsCache = new Map(); + assetsCache.capability = capability; + // Loose test here ensures we clear localStorage even if it wasn't being used in this session + if (/localStorage/.test(capability)) { + if (items === 'reset') { + itemsCount += localStorage.length; + localStorage.clear(); + } else { + for (var i = localStorage.length; i--;) { + key = localStorage.key(i); + if (/\.zim\w{0,2}/i.test(key)) { + localStorage.removeItem(key); + itemsCount++; + } + } + } + result = result ? result + ' and localStorage' : 'localStorage'; + } + // Loose test here ensures we clear indexedDB even if it wasn't being used in this session + if (/indexedDB/.test(capability)) { + result = result ? result + ' and indexedDB' : 'indexedDB'; + idxDB('count', function (number) { + itemsCount += number; + idxDB('clear', function () { + result = result ? result + ' (' + itemsCount + ' items deleted)' : 'no assets to delete'; + console.log('cache.clear: ' + result); + if (!/^cacheAPI/.test(capability) && callback) callback(itemsCount); + }); + }); + } + // No need to use loose test here because cacheAPI trumps the others + if (/^cacheAPI/.test(capability)) { + result = result ? result + ' and cacheAPI' : 'cacheAPI'; + count(function (number) { + itemsCount += number[1]; + cacheAPI('clear', function () { + result = result ? result + ' (' + itemsCount + ' items deleted)' : 'no assets to delete'; + console.log('cache.clear: ' + result); + if (callback) callback(itemsCount); + }); + }); + } + } + if (!/^cacheAPI|indexedDB/.test(capability)) { + result = result ? result + ' (' + itemsCount + ' items deleted)' : 'no assets to delete'; + console.log('cache.clear: ' + result); + if (callback) callback(itemsCount); + } +} + +/** + * Replaces all assets that have the given attribute in the html string with inline tags containing content + * from the cache entries corresponding to the given zimFile + * Function is intended for link or script tags, but could be extended + * Returns the substituted html in the callback function (even if no substitutions were made) + * + * @param {String} html The html string to process + * @param {String} tags The html tag or tags ('link|script') containing the asset to replace; + * multiple tags must be separated with a pipe + * @param {String} attribute The attribute that stores the URL to be substituted + * @param {String} zimFile The name of the ZIM file (or first file in the file set) + * @param {Object} selectedArchive The archive selected by the user in app.js + * @param {Function} callback The function to call with the substituted html + */ +function replaceAssetRefsWithUri (html, tags, attribute, zimFile, selectedArchive, callback) { + // Creates an array of all link tags that have the given attribute + var regexpTagsWithAttribute = new RegExp('<(?:' + tags + ')[^>]+?' + attribute + '=["\']([^"\']+)[^>]+>', 'ig'); + var titles = []; + var tagArray = regexpTagsWithAttribute.exec(html); + while (tagArray !== null) { + titles.push([tagArray[0], + decodeURIComponent(tagArray[1])]); + tagArray = regexpTagsWithAttribute.exec(html); + } + if (!titles.length) { + callback(html); + } + // Iterate through the erray of titles, populating the HTML string with substituted tags containing + // a reference to the content from the Cache or from the ZIM + assetsCache.busy = titles.length; + titles.forEach(function (title) { + getItemFromCacheOrZIM(selectedArchive, zimFile + '/' + title[1], function (assetContent) { + assetsCache.busy--; + if (assetContent || assetContent === '') { + var newAssetTag = uiUtil.createNewAssetElement(title[0], attribute, assetContent); + html = html.replace(title[0], newAssetTag); + } + if (!assetsCache.busy) callback(html); + }); + }); +} + +/** + * Provides "Server Side" transformation of textual content "served" to app.js + * For performance reasons, this is only hooked into content extracted from the ZIM: the transformed + * content will then be cached in its transformed state + * + * @param {String} string The string to transform + * @param {String} filter An optional filter: only transforms which match the filter will be executed + * @returns {String} The tranformed content + */ +function transform (string, filter) { + switch (filter) { + case 'html': + // Filter to remove any BOM (causes quirks mode in browser) + string = string.replace(/^[^<]*/, ''); + + // Filter to open all heading sections + string = string.replace(/(class=["'][^"']*?collapsible-(?:heading|block)(?!\s+open-block))/g, + '$1 open-block'); + + break; + } + return string; +} + +/** + * Provide method to verify File System Access API permissions + * + * @param {Object} fileHandle The file handle that we wish to verify with the Native Filesystem API + * @param {Boolean} withWrite Indicates read only or read/write persmissions + * @returns {Promise} A Promise for a Boolean value indicating whether permission has been granted or not + */ +function verifyPermission (fileHandle, withWrite) { + if (params.useOPFS) return Promise.resolve(true); // No permission prompt required for OPFS + var opts = withWrite ? { mode: 'readwrite' } : {}; + return fileHandle.queryPermission(opts).then(function (permission) { + if (permission === 'granted') return true; + return fileHandle.requestPermission(opts).then(function (permission) { + if (permission === 'granted') return true; + console.error('Permission for ' + fileHandle.name + ' was not granted: ' + permission); + return false; + }).catch(function (error) { + console.warn('Cannot use previously picked file handle programmatically (this is normal) ' + fileHandle.name, error); + }); + }); +} + +/** + * Download an archive directly into the picked folder (primarily for use with the Origin Private File System) + * + * @param {String} archiveName The name of the archive to download (will be used as the filename) + * @param {String} archiveUrl An optional URL to download the archive from (if not supplied, will use params.kiwixDownloadLink) + * @param {Function} callback Callback function to report the progress of the download + * @returns {Promise} A Promise for a FileHandle object representing the downloaded file + */ +function downloadArchiveToPickedFolder (archiveName, archiveUrl, callback) { + archiveUrl = archiveUrl || params.kiwixDownloadLink + archiveName; + if (params.pickedFolder && params.pickedFolder.getFileHandle) { + return verifyPermission(params.pickedFolder, true).then(function (permission) { + if (permission) { + return params.pickedFolder.getFileHandle(archiveName, { create: true }).then(function (fileHandle) { + return fileHandle.createWritable().then(function (writer) { + return fetch(archiveUrl).then(function (response) { + if (!response.ok) { + return writer.close().then(function () { + // Delete the file + params.pickedFolder.removeEntry(archiveName).then(function () { + throw new Error('HTTP error, status = ' + response.status); + }); + }); + } + var loaded = 0; + var reported = 0; + return new Response( + new ReadableStream({ + start: function (controller) { + var reader = response.body.getReader(); + var processResult = function (result) { + if (result.done) { + return controller.close(); + } + loaded += result.value.byteLength; + if (loaded - reported >= 1048576) { // 1024 * 1024 + reported = loaded; + if (callback) { + callback(reported); + } else console.debug('Downloaded ' + reported + ' bytes so far...'); + } + controller.enqueue(result.value); + return reader.read().then(processResult); + }; + return reader.read().then(processResult); + } + }) + ).body.pipeTo(writer).then(function () { + if (callback) callback('completed'); + return true; + }).catch(function (err) { + console.error('Error downloading archive', err); + if (callback) callback('error'); + writer.close().then(function () { + // Delete the file + params.pickedFolder.removeEntry(archiveName).then(function () { + throw err; + }); + }); + }); + }); + }); + }); + } else { + throw (new Error('Write permission not granted!')); + } + }).catch(function (err) { + console.error('Error downloading archive', err); + throw err; + }); + } else { + return Promise.reject(new Error('No picked folder available!')); + } +} + +/** + * Imports the picked files into the OPFS file system + * + * @param {Array} files An array of File objects to import + */ +function importOPFSEntries (files) { + return Promise.all(files.map(function (file) { + return params.pickedFolder.getFileHandle(file.name, { create: true }).then(function (fileHandle) { + return fileHandle.createWritable().then(function (writer) { + uiUtil.pollOpsPanel('Please wait: Importing ' + file.name + '...', true); + return writer.write(file).then(function () { + uiUtil.pollOpsPanel('Please wait: Imported ' + file.name + '...', true); + return writer.close(); + }); + }); + }); + })); +} + +/** + * Exports an entry from the OPFS file system to the user-visible file system + * + * @param {String} name The filename of the entry to export + * @returns {Promise} A Promise for a Boolean value indicating whether the export was successful + */ +function exportOPFSEntry (name) { + if (navigator && navigator.storage && 'getDirectory' in navigator.storage) { + return navigator.storage.getDirectory().then(function (dir) { + return dir.getFileHandle(name).then(function (fileHandle) { + try { + // Obtain a file handle to a new file in the user-visible file system + // with the same name as the file in the origin private file system. + return window.showSaveFilePicker({ + suggestedName: fileHandle.name || '' + }).then(function (saveHandle) { + return saveHandle.createWritable().then(function (writable) { + return fileHandle.getFile().then(function (file) { + return writable.write(file).then(function () { + writable.close(); + return true; + }); + }); + }); + }); + } catch (err) { + console.error(err.name, err.message); + return false; + } + }).catch(function (err) { + console.error('Unable to get file handle from OPFS', err); + return false; + }); + }).catch(function (err) { + console.error('Unable to get directory from OPFS', err); + return false; + }); + } +} + +/** + * Deletes an entry from the OPFS file system + * + * @param {String} name The filename of the entry to delete + */ +function deleteOPFSEntry (name) { + if (navigator && navigator.storage && 'getDirectory' in navigator.storage) { + return navigator.storage.getDirectory().then(function (dirHandle) { + return iterateOPFSEntries().then(function (entries) { + var baseName = name.replace(/\.zim[^.]*$/i, ''); + entries.forEach(function (entry) { + if (~entry.name.indexOf(baseName)) { + return dirHandle.removeEntry(entry.name).then(function () { + console.log('Deleted ' + entry.name + ' from OPFS'); + populateOPFSStorageQuota(); + }).catch(function (err) { + console.error('Unable to delete ' + entry.name + ' from OPFS', err); + }); + } + }); + }).catch(function (err) { + console.error('Unable to get directory from OPFS', err); + }); + }).catch(function (err) { + console.error('Unable to get directory from OPFS', err); + }); + } +} + +/** + * Iterates the OPFS file system and returns an array of entries found + * + * @returns {Promise} A Promise for an array of entries in the OPFS file system + */ +function iterateOPFSEntries () { + if (navigator && navigator.storage && 'getDirectory' in navigator.storage) { + return navigator.storage.getDirectory().then(function (dirHandle) { + var archiveEntries = []; + var entries = dirHandle.entries(); + var promisesForEntries = []; + // Push the pormise for each entry to the promises array + var pushPromises = new Promise(function (resolve) { + (function iterate () { + return entries.next().then(function (result) { + if (!result.done) { + // Process the entry, then continue iterating + var entry = result.value[1]; + archiveEntries.push(entry); + promisesForEntries.push(result); + iterate(); + } else { + return resolve(true); + } + }); + })(); + }); + return pushPromises.then(function () { + return Promise.all(promisesForEntries).then(function () { + return archiveEntries; + }).catch(function (err) { + console.error('Unable to iterate OPFS entries', err); + }); + }); + }); + } +} + +/** + * Gets the OPFS storage quota and populates the OPFSQuota panel + * + * @returns {Promise} A Promise that populates the OPFSQuota panel + */ +function populateOPFSStorageQuota () { + if (navigator && navigator.storage && ('estimate' in navigator.storage)) { + return navigator.storage.estimate().then(function (estimate) { + var percent = ((estimate.usage / estimate.quota) * 100).toFixed(2); + appstate.OPFSQuota = estimate.quota - estimate.usage; + document.getElementById('OPFSQuota').innerHTML = + 'OPFS storage quota:
Used: ' + percent + '%; Remaining: ' + + (appstate.OPFSQuota / 1024 / 1024 / 1024).toFixed(2) + ' GB'; + }); + } +} + +/** + * Wraps a semaphor in a Promise. A function can signal that it is done by setting a sempahor to true, + * if it has first set it to false at the outset of the procedure. Ensure no other functions use the same + * sempahor. The semaphor must be an object key of the app-wide assetsCache object. + * + * @param {String} semaphor The name of a semaphor key in the assetsCache object + * @param {String|Object} value An optional value or object to pass in the resolved promise + * @returns {Promise} A promise that resolves when assetsCache[semaphor] is true + */ +function wait (semaphor, value) { + var p = new Promise(function (resolve) { + setTimeout(function awaitCache () { + if (assetsCache[semaphor]) { + return resolve(value); + } + setTimeout(awaitCache, 300); + }, 0); + }); + return p; +} + +export default { + APPCACHE: APPCACHE, + CACHEAPI: CACHEAPI, + test: test, + count: count, + idxDB: idxDB, + cacheAPI: cacheAPI, + setArticle: setArticle, + getArticle: getArticle, + setItem: setItem, + getItem: getItem, + clear: clear, + wait: wait, + getItemFromCacheOrZIM: getItemFromCacheOrZIM, + replaceAssetRefsWithUri: replaceAssetRefsWithUri, + verifyPermission: verifyPermission, + downloadArchiveToPickedFolder: downloadArchiveToPickedFolder, + importOPFSEntries: importOPFSEntries, + exportOPFSEntry: exportOPFSEntry, + deleteOPFSEntry: deleteOPFSEntry, + iterateOPFSEntries: iterateOPFSEntries, + populateOPFSStorageQuota: populateOPFSStorageQuota +}; From 050815bf484f54177d7071a11e6aba1f8761407f Mon Sep 17 00:00:00 2001 From: RG Date: Wed, 11 Oct 2023 18:40:10 +0530 Subject: [PATCH 03/45] [ADD] Zim File handler saving in indexDB --- www/js/app.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/www/js/app.js b/www/js/app.js index f0a914811..dc427fad4 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -35,6 +35,7 @@ import uiUtil from './lib/uiUtil.js'; import settingsStore from './lib/settingsStore.js'; import abstractFilesystemAccess from './lib/abstractFilesystemAccess.js'; import translateUI from './lib/translateUI.js'; +import cache from './lib/cache.js'; if (params.abort) { // If the app was loaded only to pass a message from the remote code, then we exit immediately @@ -166,6 +167,7 @@ function resizeIFrame () { document.addEventListener('DOMContentLoaded', function () { getDefaultLanguageAndTranslateApp(); resizeIFrame(); + loadPreviousZimFile(); }); window.addEventListener('resize', resizeIFrame); @@ -1250,6 +1252,26 @@ function resetCssCache () { } } +async function loadPreviousZimFile () { + const openFile = await uiUtil.systemAlert('Do you want to load the previously selected zim file?', 'Load previous zim file', true, 'No', 'Yes', 'No') + // .then(function (response) { + // }) + if (!openFile) return + // If a old zim file is already selected, we set it as the localArchive + cache.idxDB('files', async function (filesHandlers) { + // console.log("FILE HANDLE", a); + // refer to this article ; https://developer.chrome.com/articles/file-system-access/ + const files = []; + for (let index = 0; index < filesHandlers.length; index++) { + const fileHandler = filesHandlers[index]; + await fileHandler.requestPermission(); + files.push(await fileHandler.getFile()) + } + console.log(files); + setLocalArchiveFromFileList(files); + }) +} + /** * Displays the zone to select files from the archive */ @@ -1276,6 +1298,10 @@ function displayFileSelect () { window.showOpenFilePicker({ multiple: true }).then(function (fileHandle) { fileHandle[0].getFile().then(function (file) { setLocalArchiveFromFileList([file]); + + cache.idxDB('files', fileHandle, function (a) { + // console.log(a); + }) }); }); }) From e3963751e52a90760395fe0433382db668dda9a3 Mon Sep 17 00:00:00 2001 From: RG Date: Thu, 12 Oct 2023 16:43:34 +0530 Subject: [PATCH 04/45] [FINALIZE] File system api picker and saving handles in indexDB --- www/js/app.js | 67 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 24 deletions(-) diff --git a/www/js/app.js b/www/js/app.js index dc427fad4..a7cd95e12 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -1252,24 +1252,28 @@ function resetCssCache () { } } -async function loadPreviousZimFile () { - const openFile = await uiUtil.systemAlert('Do you want to load the previously selected zim file?', 'Load previous zim file', true, 'No', 'Yes', 'No') - // .then(function (response) { - // }) - if (!openFile) return - // If a old zim file is already selected, we set it as the localArchive - cache.idxDB('files', async function (filesHandlers) { - // console.log("FILE HANDLE", a); - // refer to this article ; https://developer.chrome.com/articles/file-system-access/ - const files = []; - for (let index = 0; index < filesHandlers.length; index++) { - const fileHandler = filesHandlers[index]; - await fileHandler.requestPermission(); - files.push(await fileHandler.getFile()) - } - console.log(files); - setLocalArchiveFromFileList(files); - }) +/** + * Loads the Previously selected zim file via IndexedDB + */ +function loadPreviousZimFile () { + if (typeof window.showOpenFilePicker === 'function') { + cache.idxDB('zimFile', async function (fileHandler) { + // console.log(fileHandler); + if (!fileHandler) return console.info('There is no previous zim file in DB') + + const openFile = await uiUtil.systemAlert('Do you want to load the previously selected zim file?', 'Load previous zim file', true, 'No', 'Yes', 'No') + if (!openFile) { + cache.idxDB('zimFile', undefined, function () { + // reset all zim files in DB + }) + return console.log('User Dont want to load previous zim file') + } + + // refer to this article for easy explanation https://developer.chrome.com/articles/file-system-access/ + const isGranted = await fileHandler.requestPermission(); + if (isGranted === 'granted') setLocalArchiveFromFileList([await fileHandler.getFile()]); + }) + } } /** @@ -1295,12 +1299,12 @@ function displayFileSelect () { if (typeof window.showOpenFilePicker === 'function') { document.getElementById('archiveFiles').addEventListener('click', function (e) { e.preventDefault(); - window.showOpenFilePicker({ multiple: true }).then(function (fileHandle) { - fileHandle[0].getFile().then(function (file) { + window.showOpenFilePicker({ multiple: false }).then(function (fileHandle) { + const selectedFile = fileHandle[0] + selectedFile.getFile().then(function (file) { setLocalArchiveFromFileList([file]); - - cache.idxDB('files', fileHandle, function (a) { - // console.log(a); + cache.idxDB('zimFile', selectedFile, function () { + // file saved in DB }) }); }); @@ -1327,7 +1331,7 @@ function handleIframeDrop (e) { e.preventDefault(); } -function handleFileDrop (packet) { +async function handleFileDrop (packet) { packet.stopPropagation(); packet.preventDefault(); configDropZone.style.border = ''; @@ -1338,6 +1342,21 @@ function handleFileDrop (packet) { setLocalArchiveFromFileList(files); // This clears the display of any previously picked archive in the file selector document.getElementById('archiveFiles').value = null; + + if (typeof window.showOpenFilePicker === 'function') { + // Only runs when browser support File System API + const fileInfo = await packet.dataTransfer.items[0] + if (fileInfo.kind === 'file') { + const fileHandle = await fileInfo.getAsFileSystemHandle(); + cache.idxDB('zimFile', fileHandle, function () { + // save file in DB + }); + } + } + // will be later on used + // if (fileInfo.kind === 'directory'){ + // const dirHandle = fileInfo.getAsFileSystemHandle(); + // } } document.getElementById('libraryBtn').addEventListener('click', function (e) { From decafcd22a7b0af04e21e15c927acbb4a43e506f Mon Sep 17 00:00:00 2001 From: RG Date: Fri, 13 Oct 2023 00:36:29 +0530 Subject: [PATCH 05/45] [ADD] Select for zim files --- www/index.html | 3 +++ www/js/app.js | 47 ++++++++++++++++++++++++++++++++++------------- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/www/index.html b/www/index.html index e1dc67f55..ee800ef9f 100644 --- a/www/index.html +++ b/www/index.html @@ -467,6 +467,9 @@

Configuration

display file selectors.

-
diff --git a/www/js/app.js b/www/js/app.js index 5e5f1df6c..d9b3f0d7d 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -1161,6 +1161,7 @@ window.onpopstate = function (event) { function populateDropDownListOfArchives (archiveDirectories) { document.getElementById('scanningForArchives').style.display = 'none'; document.getElementById('chooseArchiveFromLocalStorage').style.display = ''; + document.getElementById('rescanButtonAndText').style.display = ''; var comboArchiveList = document.getElementById('archiveList'); comboArchiveList.options.length = 0; for (var i = 0; i < archiveDirectories.length; i++) { @@ -1266,7 +1267,7 @@ function displayFileSelect () { document.getElementById('openLocalFiles').style.display = 'block'; if (isFileSystemAPISupported || isWebkitSupported) { - document.getElementById('zimSelectDropdown').style.display = ''; + document.getElementById('chooseArchiveFromLocalStorage').style.display = ''; document.getElementById('folderSelect').style.display = ''; } @@ -1285,7 +1286,7 @@ function displayFileSelect () { globalDropZone.addEventListener('drop', handleFileDrop); } - document.getElementById('zimSelectDropdown').addEventListener('change', async function (e) { + document.getElementById('archiveList').addEventListener('change', async function (e) { // handle zim selection from dropdown if multiple files are loaded via webkitdirectory or filesystem api if (isFileSystemAPISupported) { const files = await fileSystem.getSelectedZimFromCache(e.target.value) @@ -1312,6 +1313,7 @@ function displayFileSelect () { filenames.push(file.name); } webKitFileList = e.target.files; + // populateDropDownListOfArchives(filenames); await fileSystem.updateZimDropdownOptions({ fileOrDirHandle: null, files: filenames }, ''); }) } diff --git a/www/js/lib/fileSystem.js b/www/js/lib/fileSystem.js index 79233b177..f7dbaf761 100644 --- a/www/js/lib/fileSystem.js +++ b/www/js/lib/fileSystem.js @@ -14,7 +14,7 @@ import cache from './cache.js'; * @returns {Promise>} Array of unique filenames (if a split zim is considered a single file) */ async function updateZimDropdownOptions (fileSystemHandler, selectedFile) { - const select = document.getElementById('zimSelectDropdown'); + const select = document.getElementById('archiveList'); let options = ''; let count = 0; if (fileSystemHandler.files.length !== 0) options += ''; @@ -26,8 +26,9 @@ async function updateZimDropdownOptions (fileSystemHandler, selectedFile) { } }); select.innerHTML = options; - document.getElementById('zimSelectDropdown').value = selectedFile; + document.getElementById('archiveList').value = selectedFile; document.getElementById('numberOfFilesDisplay').innerText = count; + document.getElementById('fileCountDisplay').style.display = ''; } /** From b01f962fd394dbdf496f0e9a4a53b188fa2afa4b Mon Sep 17 00:00:00 2001 From: RG Date: Sun, 22 Oct 2023 19:14:15 +0530 Subject: [PATCH 23/45] [REFACTOR] saving and getting filenames from localstorage --- www/index.html | 7 ++-- www/js/app.js | 4 +-- www/js/lib/fileSystem.js | 70 +++++++++++++--------------------------- 3 files changed, 27 insertions(+), 54 deletions(-) diff --git a/www/index.html b/www/index.html index 90361f226..96c057d44 100644 --- a/www/index.html +++ b/www/index.html @@ -493,11 +493,10 @@

Configuration

diff --git a/www/js/app.js b/www/js/app.js index 337926c4d..e70e45984 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -1342,10 +1342,11 @@ let webKitFileList = null function displayFileSelect () { const isFireFoxOsNativeFileApiAvailable = typeof navigator.getDeviceStorages === 'function'; - console.debug('File system api supported', params.isFileSystemApiSupported); - console.debug('Webkit supported', params.isWebkitDirApiSupported); - console.debug('Firefox os native file support api', isFireFoxOsNativeFileApiAvailable) + console.debug(`File system api is ${params.isFileSystemApiSupported ? '' : 'not '}supported`); + console.debug(`Webkit directory api ${params.isWebkitDirApiSupported ? '' : 'not '}supported`); + console.debug(`Firefox os native file ${isFireFoxOsNativeFileApiAvailable ? '' : 'not '}support api`) + document.getElementById('fileCountDisplay').innerText = translateUI.t('configure-select-file-numbers').replace('{{numberOfFiles}}', '0'); document.getElementById('openLocalFiles').style.display = 'block'; if (params.isFileSystemApiSupported || params.isWebkitDirApiSupported) { document.getElementById('chooseArchiveFromLocalStorage').style.display = ''; diff --git a/www/js/init.js b/www/js/init.js index 6c51b28bb..d670f89c7 100644 --- a/www/js/init.js +++ b/www/js/init.js @@ -51,8 +51,8 @@ * @property {boolean} useCanvasElementsForWebpTranscoding - A parameter to circumvent anti-fingerprinting technology in browsers that do not support WebP natively by substituting images directly with the canvas elements produced by the WebP polyfill. * @property {string} libraryUrl - The URL of the Kiwix library. * @property {string} altLibraryUrl - The alternative URL of the Kiwix library in non-supported browsers. - * @property {string} cacheAPI - Database name for the IndexedDB cache - * @property {string} cacheIDB - Not sure what this does + * @property {string} cacheAPI - Name of the prefix used to identify the cache in Cache API + * @property {string} cacheIDB - Name of the Indexed DB database * @property {boolean} isFileSystemApiSupported - A boolean indicating whether the FileSystem API is supported. * @property {boolean} isWebkitDirApiSupported - A boolean indicating whether the Webkit Directory API is supported. * @property {DecompressorAPI} decompressorAPI @@ -119,10 +119,10 @@ params['contentInjectionMode'] = getSetting('contentInjectionMode') || params['useCanvasElementsForWebpTranscoding'] = null; // Value is determined in uiUtil.determineCanvasElementsWorkaround(), called when setting the content injection mode params['libraryUrl'] = 'https://library.kiwix.org/'; // Url for iframe that will be loaded to download new zim files params['altLibraryUrl'] = 'https://download.kiwix.org/zim/'; // Alternative Url for iframe (for use with unsupported browsers) that will be loaded to download new zim files -params['cacheAPI'] = 'kiwix-js'; // Sets the database name for the IndexedDB cache -params['cacheIDB'] = 'kiwix-zim'; // Not sure what this does -params['isFileSystemApiSupported'] = typeof window.showOpenFilePicker === 'function'; // Not sure what this does -params['isWebkitDirApiSupported'] = 'webkitdirectory' in document.createElement('input'); // Not sure what this does +params['cacheAPI'] = 'kiwix-js'; // Sets name of the prefix used to identify the cache in Cache API +params['cacheIDB'] = 'kiwix-zim'; // Sets name of the Indexed DB database +params['isFileSystemApiSupported'] = typeof window.showOpenFilePicker === 'function'; // Sets a boolean indicating whether the FileSystem API is supported +params['isWebkitDirApiSupported'] = 'webkitdirectory' in document.createElement('input'); // Sets a boolean indicating whether the Webkit Directory API is supported /** * Apply any override parameters that might be in the querystring. diff --git a/www/js/lib/abstractFilesystemAccess.js b/www/js/lib/abstractFilesystemAccess.js index 76ab17180..0366f1e32 100644 --- a/www/js/lib/abstractFilesystemAccess.js +++ b/www/js/lib/abstractFilesystemAccess.js @@ -301,12 +301,12 @@ async function getFilesFromReader (reader) { export default { StorageFirefoxOS: StorageFirefoxOS, - updateZimDropdownOptions, - selectDirectoryFromPickerViaFileSystemApi, - selectFileFromPickerViaFileSystemApi, - getSelectedZimFromCache, - loadPreviousZimFile, - handleFolderDropViaWebkit, - handleFolderDropViaFileSystemAPI, - getSelectedZimFromWebkitList + updateZimDropdownOptions: updateZimDropdownOptions, + selectDirectoryFromPickerViaFileSystemApi: selectDirectoryFromPickerViaFileSystemApi, + selectFileFromPickerViaFileSystemApi: selectFileFromPickerViaFileSystemApi, + getSelectedZimFromCache: getSelectedZimFromCache, + loadPreviousZimFile: loadPreviousZimFile, + handleFolderDropViaWebkit: handleFolderDropViaWebkit, + handleFolderDropViaFileSystemAPI: handleFolderDropViaFileSystemAPI, + getSelectedZimFromWebkitList: getSelectedZimFromWebkitList }; From 6ededf1ae04b64488dab9cf1e837b06dc89e9f77 Mon Sep 17 00:00:00 2001 From: RG Date: Tue, 31 Oct 2023 23:36:05 +0530 Subject: [PATCH 35/45] [REFACTOR] translations updated --- i18n/en.jsonp.js | 2 +- i18n/es.jsonp.js | 2 +- i18n/fr.jsonp.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/i18n/en.jsonp.js b/i18n/en.jsonp.js index 7969f4790..4e8b76d47 100644 --- a/i18n/en.jsonp.js +++ b/i18n/en.jsonp.js @@ -22,7 +22,7 @@ document.localeJson = { "configure-btn-folderselect": "Select Folder", "configure-btn-rescan": "Rescan", "configure-about-rescan-tip": "Rescans your SD Cards and internal memory", - "configure-select-file-numbers": "{{numberOfFiles}} archives found in selected location. ", + "configure-select-file-numbers": "{{numberOfFiles}} archive(s) found in selected location. ", "configure-download-instructions": "This application needs a ZIM archive to work.
For full instructions, please see the section", "configure-select-instructions": "Please select or drag and drop a .zim file (or all the .zimaa, .zimab etc in case of a split ZIM file):", "configure-select-file-instructions": "Please select the archive you want to use :", diff --git a/i18n/es.jsonp.js b/i18n/es.jsonp.js index d981bdbf5..2bb96b3d9 100644 --- a/i18n/es.jsonp.js +++ b/i18n/es.jsonp.js @@ -22,7 +22,7 @@ document.localeJson = { "configure-btn-folderselect": "Seleccione carpeta", "configure-btn-rescan": "Volver a escanear", "configure-about-rescan-tip": "Vuelve a escanear las tarjetas SD y la memoria interna", - "configure-select-file-numbers": "{{numberOfFiles}} archivos encontrados en la ubicación seleccionada. ", + "configure-select-file-numbers": "{{numberOfFiles}} archivo(s) encontrados en la ubicación seleccionada. ", "configure-download-instructions": "Esta aplicación necesita un archivo ZIM para funcionar.
Para instrucciones completas, vea la sección", "configure-select-instructions": "Seleccione o arrastre y suelte un archivo .zim (o todos los .zimaa, .zimab etc en caso de un archivo dividido):", "configure-select-file-instructions": "Seleccione el archivo que desea utilizar:", diff --git a/i18n/fr.jsonp.js b/i18n/fr.jsonp.js index 2d5e135ac..b04c62d67 100644 --- a/i18n/fr.jsonp.js +++ b/i18n/fr.jsonp.js @@ -22,7 +22,7 @@ document.localeJson = { "configure-btn-folderselect": "Sélectionner un dossier", "configure-btn-rescan": "Rechercher", "configure-about-rescan-tip": "Réanalyser la carte SD et la mérmoire interne", - "configure-select-file-numbers": "{{numberOfFiles}} archives trouvées dans le lieu sélectionné. ", + "configure-select-file-numbers": "{{numberOfFiles}} archive(s) trouvées dans le lieu sélectionné. ", "configure-download-instructions": "Cette application a besoin d'un fichier ZIM pour fonctionner.
Pour des instructions complètes, veuillez consulter la section", "configure-select-instructions": "Veuillez sélectionner ou glisser-déposer un fichier .zim (ou tous les .zimaa, .zimab etc. dans le cas d'un fichier ZIM découpé) :", "configure-select-file-instructions": "Veuillez sélectionner l'archive que vous souhaitez utiliser :", From 600e83109d6fb8be34297e5c6d2fd33e397655eb Mon Sep 17 00:00:00 2001 From: RG Date: Wed, 1 Nov 2023 00:56:08 +0530 Subject: [PATCH 36/45] [FIX] edge 18 file picker [FIX] firefox filedrop [FIX] number of file counter display --- i18n/en.jsonp.js | 2 +- i18n/es.jsonp.js | 2 +- i18n/fr.jsonp.js | 2 +- www/index.html | 3 ++- www/js/app.js | 1 - www/js/lib/abstractFilesystemAccess.js | 15 ++++++++++----- 6 files changed, 15 insertions(+), 10 deletions(-) diff --git a/i18n/en.jsonp.js b/i18n/en.jsonp.js index 4e8b76d47..55348999c 100644 --- a/i18n/en.jsonp.js +++ b/i18n/en.jsonp.js @@ -22,7 +22,7 @@ document.localeJson = { "configure-btn-folderselect": "Select Folder", "configure-btn-rescan": "Rescan", "configure-about-rescan-tip": "Rescans your SD Cards and internal memory", - "configure-select-file-numbers": "{{numberOfFiles}} archive(s) found in selected location. ", + "configure-select-file-numbers": "archive(s) found in selected location. ", "configure-download-instructions": "This application needs a ZIM archive to work.
For full instructions, please see the section", "configure-select-instructions": "Please select or drag and drop a .zim file (or all the .zimaa, .zimab etc in case of a split ZIM file):", "configure-select-file-instructions": "Please select the archive you want to use :", diff --git a/i18n/es.jsonp.js b/i18n/es.jsonp.js index 2bb96b3d9..f623d84db 100644 --- a/i18n/es.jsonp.js +++ b/i18n/es.jsonp.js @@ -22,7 +22,7 @@ document.localeJson = { "configure-btn-folderselect": "Seleccione carpeta", "configure-btn-rescan": "Volver a escanear", "configure-about-rescan-tip": "Vuelve a escanear las tarjetas SD y la memoria interna", - "configure-select-file-numbers": "{{numberOfFiles}} archivo(s) encontrados en la ubicación seleccionada. ", + "configure-select-file-numbers": "archivo(s) encontrados en la ubicación seleccionada. ", "configure-download-instructions": "Esta aplicación necesita un archivo ZIM para funcionar.
Para instrucciones completas, vea la sección", "configure-select-instructions": "Seleccione o arrastre y suelte un archivo .zim (o todos los .zimaa, .zimab etc en caso de un archivo dividido):", "configure-select-file-instructions": "Seleccione el archivo que desea utilizar:", diff --git a/i18n/fr.jsonp.js b/i18n/fr.jsonp.js index b04c62d67..b8d71836e 100644 --- a/i18n/fr.jsonp.js +++ b/i18n/fr.jsonp.js @@ -22,7 +22,7 @@ document.localeJson = { "configure-btn-folderselect": "Sélectionner un dossier", "configure-btn-rescan": "Rechercher", "configure-about-rescan-tip": "Réanalyser la carte SD et la mérmoire interne", - "configure-select-file-numbers": "{{numberOfFiles}} archive(s) trouvées dans le lieu sélectionné. ", + "configure-select-file-numbers": "archive(s) trouvées dans le lieu sélectionné. ", "configure-download-instructions": "Cette application a besoin d'un fichier ZIM pour fonctionner.
Pour des instructions complètes, veuillez consulter la section", "configure-select-instructions": "Veuillez sélectionner ou glisser-déposer un fichier .zim (ou tous les .zimaa, .zimab etc. dans le cas d'un fichier ZIM découpé) :", "configure-select-file-instructions": "Veuillez sélectionner l'archive que vous souhaitez utiliser :", diff --git a/www/index.html b/www/index.html index 11b1ca8c6..b7de52d46 100644 --- a/www/index.html +++ b/www/index.html @@ -492,8 +492,9 @@

Configuration

-
-

Display settings

From c74e04d504a2a4ed91c33902e05e06db0c083b8f Mon Sep 17 00:00:00 2001 From: RG Date: Sun, 5 Nov 2023 02:52:30 +0530 Subject: [PATCH 40/45] [ADD] file list save for webkit dir --- www/js/app.js | 26 +++++++++++++++++++------- www/js/lib/abstractFilesystemAccess.js | 9 +++++---- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/www/js/app.js b/www/js/app.js index 8f49f41c8..02062e139 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -1370,10 +1370,15 @@ function displayFileSelect () { if (!isFireFoxOsNativeFileApiAvailable) { document.getElementById('archiveList').addEventListener('change', async function (e) { // handle zim selection from dropdown if multiple files are loaded via webkitdirectory or filesystem api + localStorage.setItem('previousZimFileName', e.target.value); if (params.isFileSystemApiSupported) { const files = await abstractFilesystemAccess.getSelectedZimFromCache(e.target.value) setLocalArchiveFromFileList(files); } else { + if (webKitFileList === null) { + document.getElementById('folderSelect').click(); + return; + } const files = abstractFilesystemAccess.getSelectedZimFromWebkitList(webKitFileList, e.target.value) setLocalArchiveFromFileList(files); } @@ -1391,12 +1396,19 @@ function displayFileSelect () { document.getElementById('folderSelect').addEventListener('change', async function (e) { e.preventDefault(); const filenames = []; + const previousZimFile = [] + const lastFilename = localStorage.getItem('previousZimFileName'); + const filenameWithoutExtension = lastFilename.replace(/\.zim\w\w$/i, ''); + const regex = new RegExp(`\\${filenameWithoutExtension}.zim\\w\\w$`, 'i'); for (const file of e.target.files) { filenames.push(file.name); + if (regex.test(file.name) || file.name === lastFilename) previousZimFile.push(file); } webKitFileList = e.target.files; - // populateDropDownListOfArchives(filenames); - await abstractFilesystemAccess.updateZimDropdownOptions(filenames, ''); + localStorage.setItem('zimFilenames', filenames.join('|')); + // will load the old file if the selected folder contains the same file + if (previousZimFile.length !== 0) setLocalArchiveFromFileList(previousZimFile); + await abstractFilesystemAccess.updateZimDropdownOptions(filenames, previousZimFile.length !== 0 ? lastFilename : ''); }) } if (params.isFileSystemApiSupported && !isFireFoxOsNativeFileApiAvailable) { @@ -1451,13 +1463,13 @@ async function handleFileDrop (packet) { // call the `setLocalArchiveFromFileList` let loadZim = true; - if (params.isFileSystemApiSupported) loadZim = await abstractFilesystemAccess.handleFolderOrFileDropViaFileSystemAPI(packet) + // no previous file will be loaded in case of FileSystemApi + if (params.isFileSystemApiSupported) loadZim = await abstractFilesystemAccess.handleFolderOrFileDropViaFileSystemAPI(packet); else if (params.isWebkitDirApiSupported) { - const ret = await abstractFilesystemAccess.handleFolderOrFileDropViaWebkit(packet) - loadZim = ret.loadZim - webKitFileList = ret.files + const ret = await abstractFilesystemAccess.handleFolderOrFileDropViaWebkit(packet); + loadZim = ret.loadZim; + webKitFileList = ret.files; } - if (loadZim) setLocalArchiveFromFileList(files); } diff --git a/www/js/lib/abstractFilesystemAccess.js b/www/js/lib/abstractFilesystemAccess.js index a7b0dd44e..aff6524a4 100644 --- a/www/js/lib/abstractFilesystemAccess.js +++ b/www/js/lib/abstractFilesystemAccess.js @@ -106,7 +106,7 @@ async function updateZimDropdownOptions (files, selectedFile) { const select = document.getElementById('archiveList'); let options = ''; let count = 0; - if (files.length !== 0) options += ``; + if (files.length !== 0) options += ``; files.forEach((fileName) => { if (fileName.endsWith('.zim') || fileName.endsWith('.zimaa')) { @@ -166,7 +166,7 @@ function getSelectedZimFromCache (selectedFilename) { return new Promise((resolve, _reject) => { cache.idxDB('zimFiles', async function (fileOrDirHandle) { // Left it here for debugging purposes as its sometimes asking for permission even when its granted - console.debug('FileHandle and Permission', fileOrDirHandle, fileOrDirHandle.queryPermission()) + console.debug('FileHandle and Permission', fileOrDirHandle, await fileOrDirHandle.queryPermission()) if ((await fileOrDirHandle.queryPermission()) !== 'granted') await fileOrDirHandle.requestPermission(); if (fileOrDirHandle.kind === 'directory') { @@ -217,7 +217,7 @@ function loadPreviousZimFile () { // If we call `updateZimDropdownOptions` without any delay it will run before the internationalization is initialized // It's a bit hacky but it works and I am not sure if there is any other way ATM setTimeout(() => { - if (window.params.isFileSystemApiSupported) { + if (window.params.isFileSystemApiSupported || window.params.isWebkitDirApiSupported) { const filenames = localStorage.getItem('zimFilenames'); if (filenames) updateZimDropdownOptions(filenames.split('|'), ''); } @@ -268,7 +268,7 @@ async function handleFolderOrFileDropViaWebkit (event) { var entry = dt.items[0].webkitGetAsEntry(); if (entry.isFile) { - console.log(entry.file); + localStorage.setItem('zimFilenames', [entry.name].join('|')); await updateZimDropdownOptions([entry.name], entry.name); return { loadZim: true, files: [entry.file] }; } else if (entry.isDirectory) { @@ -276,6 +276,7 @@ async function handleFolderOrFileDropViaWebkit (event) { const files = await getFilesFromReader(reader); const fileNames = []; files.forEach((file) => fileNames.push(file.name)); + localStorage.setItem('zimFilenames', fileNames.join('|')); await updateZimDropdownOptions(fileNames, ''); return { loadZim: false, files: files }; } From 5f8ce320cde4dd964a715440d677c393a43d4b12 Mon Sep 17 00:00:00 2001 From: RG Date: Sun, 5 Nov 2023 03:20:09 +0530 Subject: [PATCH 41/45] [FIX] single file picker for webkit --- www/js/app.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/www/js/app.js b/www/js/app.js index 02062e139..1cb5b3f91 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -1376,7 +1376,9 @@ function displayFileSelect () { setLocalArchiveFromFileList(files); } else { if (webKitFileList === null) { - document.getElementById('folderSelect').click(); + const element = localStorage.getItem('zimFilenames').split('|').length === 1 ? 'archiveFiles' : 'folderSelect'; + // console.log(localStorage.getItem('zimFilenames').split('|')); + document.getElementById(element).click(); return; } const files = abstractFilesystemAccess.getSelectedZimFromWebkitList(webKitFileList, e.target.value) @@ -1423,6 +1425,7 @@ function displayFileSelect () { document.getElementById('archiveFiles').addEventListener('change', async function (e) { if (params.isWebkitDirApiSupported || params.isFileSystemApiSupported) { const activeFilename = e.target.files[0].name; + localStorage.setItem('zimFilenames', [activeFilename].join('|')); await abstractFilesystemAccess.updateZimDropdownOptions([activeFilename], activeFilename); } From a55d9d29977a0b2737b2543f60d9ea3617f35819 Mon Sep 17 00:00:00 2001 From: RG Date: Sun, 5 Nov 2023 23:21:09 +0530 Subject: [PATCH 42/45] [FIX] XSS and mobile phone folder picker disabled --- www/js/app.js | 11 ++++++++--- www/js/lib/abstractFilesystemAccess.js | 13 +++++++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/www/js/app.js b/www/js/app.js index 1cb5b3f91..7ccb946de 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -1341,13 +1341,16 @@ let webKitFileList = null */ function displayFileSelect () { const isFireFoxOsNativeFileApiAvailable = typeof navigator.getDeviceStorages === 'function'; + let isPlatformMobilePhone = false; + if (/Android/i.test(navigator.userAgent)) isPlatformMobilePhone = true; + if (/iphone|ipad|ipod/i.test(navigator.userAgent) || navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) isPlatformMobilePhone = true; console.debug(`File system api is ${params.isFileSystemApiSupported ? '' : 'not '}supported`); console.debug(`Webkit directory api ${params.isWebkitDirApiSupported ? '' : 'not '}supported`); console.debug(`Firefox os native file ${isFireFoxOsNativeFileApiAvailable ? '' : 'not '}support api`) - + console.log('ASSSSS'); document.getElementById('openLocalFiles').style.display = 'block'; - if (params.isFileSystemApiSupported || params.isWebkitDirApiSupported) { + if ((params.isFileSystemApiSupported || params.isWebkitDirApiSupported) && !isPlatformMobilePhone) { document.getElementById('chooseArchiveFromLocalStorage').style.display = ''; document.getElementById('folderSelect').style.display = ''; } @@ -1398,10 +1401,12 @@ function displayFileSelect () { document.getElementById('folderSelect').addEventListener('change', async function (e) { e.preventDefault(); const filenames = []; + const previousZimFile = [] - const lastFilename = localStorage.getItem('previousZimFileName'); + const lastFilename = localStorage.getItem('previousZimFileName') ?? ''; const filenameWithoutExtension = lastFilename.replace(/\.zim\w\w$/i, ''); const regex = new RegExp(`\\${filenameWithoutExtension}.zim\\w\\w$`, 'i'); + for (const file of e.target.files) { filenames.push(file.name); if (regex.test(file.name) || file.name === lastFilename) previousZimFile.push(file); diff --git a/www/js/lib/abstractFilesystemAccess.js b/www/js/lib/abstractFilesystemAccess.js index aff6524a4..b3c180ca5 100644 --- a/www/js/lib/abstractFilesystemAccess.js +++ b/www/js/lib/abstractFilesystemAccess.js @@ -104,17 +104,22 @@ async function updateZimDropdownOptions (files, selectedFile) { if (isFireFoxOsNativeFileApiAvailable) return // do nothing let other function handle it const select = document.getElementById('archiveList'); - let options = ''; + const options = []; let count = 0; - if (files.length !== 0) options += ``; + select.innerHTML = ''; + if (files.length !== 0) { + const placeholderOption = new Option(translateUI.t('configure-select-file-first-option'), ''); + placeholderOption.disabled = true; + select.appendChild(placeholderOption); + }; files.forEach((fileName) => { if (fileName.endsWith('.zim') || fileName.endsWith('.zimaa')) { - options += ``; + options.push(new Option(fileName, fileName)); + select.appendChild(new Option(fileName, fileName)); count++; } }); - select.innerHTML = options; document.getElementById('archiveList').value = selectedFile; document.getElementById('numberOfFilesCount').style.display = ''; document.getElementById('fileCountDisplay').style.display = ''; From bb1d61b1a5b2a476b2f615e480151aa27996479d Mon Sep 17 00:00:00 2001 From: RG Date: Sun, 5 Nov 2023 23:44:42 +0530 Subject: [PATCH 43/45] [FIX] auto load zim on Folder select --- www/js/app.js | 7 ++++--- www/js/lib/abstractFilesystemAccess.js | 10 +++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/www/js/app.js b/www/js/app.js index 7ccb946de..c6e7a8e41 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -214,7 +214,7 @@ function resizeIFrame () { if (iframe.style.display === 'none') { // We are in About or Configuration, so we only set the region height region.style.height = window.innerHeight + 'px'; - nestedFrame.style.height = window.innerHeight - 110 + 'px'; + if (nestedFrame) nestedFrame.style.height = window.innerHeight - 110 + 'px'; } else { // IE cannot retrieve computed headerStyles till the next paint, so we wait a few ticks setTimeout(function () { @@ -1348,7 +1348,7 @@ function displayFileSelect () { console.debug(`File system api is ${params.isFileSystemApiSupported ? '' : 'not '}supported`); console.debug(`Webkit directory api ${params.isWebkitDirApiSupported ? '' : 'not '}supported`); console.debug(`Firefox os native file ${isFireFoxOsNativeFileApiAvailable ? '' : 'not '}support api`) - console.log('ASSSSS'); + document.getElementById('openLocalFiles').style.display = 'block'; if ((params.isFileSystemApiSupported || params.isWebkitDirApiSupported) && !isPlatformMobilePhone) { document.getElementById('chooseArchiveFromLocalStorage').style.display = ''; @@ -1393,7 +1393,8 @@ function displayFileSelect () { // Handles Folder selection when showDirectoryPicker is supported document.getElementById('folderSelect').addEventListener('click', async function (e) { e.preventDefault(); - await abstractFilesystemAccess.selectDirectoryFromPickerViaFileSystemApi() + const previousZimFiles = await abstractFilesystemAccess.selectDirectoryFromPickerViaFileSystemApi() + if (previousZimFiles.length !== 0) setLocalArchiveFromFileList(previousZimFiles); }) } if (params.isWebkitDirApiSupported && !params.isFileSystemApiSupported && !isFireFoxOsNativeFileApiAvailable) { diff --git a/www/js/lib/abstractFilesystemAccess.js b/www/js/lib/abstractFilesystemAccess.js index b3c180ca5..63d35752a 100644 --- a/www/js/lib/abstractFilesystemAccess.js +++ b/www/js/lib/abstractFilesystemAccess.js @@ -130,19 +130,27 @@ async function updateZimDropdownOptions (files, selectedFile) { /** * Opens the File System API to select a directory + * @returns {Promise>} Previously selected file if available in selected folder */ async function selectDirectoryFromPickerViaFileSystemApi () { const handle = await window.showDirectoryPicker(); const fileNames = []; + const previousZimFile = [] + + const lastZimNameWithoutExtension = (localStorage.getItem('previousZimFileName') ?? '').replace(/\.zim\w\w$/i, ''); + const regex = new RegExp(`\\${lastZimNameWithoutExtension}.zim\\w\\w$`, 'i'); + for await (const entry of handle.values()) { fileNames.push(entry.name); + if (regex.test(entry.name) || entry.name === (localStorage.getItem('previousZimFileName') ?? '')) previousZimFile.push(await entry.getFile()); } localStorage.setItem('zimFilenames', fileNames.join('|')); - updateZimDropdownOptions(fileNames, ''); + updateZimDropdownOptions(fileNames, previousZimFile.length !== 0 ? localStorage.getItem('previousZimFileName') : ''); cache.idxDB('zimFiles', handle, function () { // save file in DB }); + return previousZimFile; } /** From b18fe52edb71bba683d2becde162f193b529c1eb Mon Sep 17 00:00:00 2001 From: RG Date: Tue, 7 Nov 2023 23:10:34 +0530 Subject: [PATCH 44/45] Refactor file selection logic for better compatibility --- www/js/app.js | 71 +++++++++++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/www/js/app.js b/www/js/app.js index 3e3bb2b38..d44413639 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -1318,26 +1318,30 @@ function displayFileSelect () { globalDropZone.addEventListener('drop', handleFileDrop); } - if (!isFireFoxOsNativeFileApiAvailable) { - document.getElementById('archiveList').addEventListener('change', async function (e) { - // handle zim selection from dropdown if multiple files are loaded via webkitdirectory or filesystem api - localStorage.setItem('previousZimFileName', e.target.value); - if (params.isFileSystemApiSupported) { - const files = await abstractFilesystemAccess.getSelectedZimFromCache(e.target.value) - setLocalArchiveFromFileList(files); - } else { - if (webKitFileList === null) { - const element = localStorage.getItem('zimFilenames').split('|').length === 1 ? 'archiveFiles' : 'folderSelect'; - // console.log(localStorage.getItem('zimFilenames').split('|')); - document.getElementById(element).click(); - return; - } - const files = abstractFilesystemAccess.getSelectedZimFromWebkitList(webKitFileList, e.target.value) - setLocalArchiveFromFileList(files); - } - }); + if (isFireFoxOsNativeFileApiAvailable) { + useLegacyFilePicker(); + return; } - if (params.isFileSystemApiSupported && !isFireFoxOsNativeFileApiAvailable) { + + document.getElementById('archiveList').addEventListener('change', async function (e) { + // handle zim selection from dropdown if multiple files are loaded via webkitdirectory or filesystem api + localStorage.setItem('previousZimFileName', e.target.value); + if (params.isFileSystemApiSupported) { + const files = await abstractFilesystemAccess.getSelectedZimFromCache(e.target.value) + setLocalArchiveFromFileList(files); + } else { + if (webKitFileList === null) { + const element = localStorage.getItem('zimFilenames').split('|').length === 1 ? 'archiveFiles' : 'folderSelect'; + // console.log(localStorage.getItem('zimFilenames').split('|')); + document.getElementById(element).click(); + return; + } + const files = abstractFilesystemAccess.getSelectedZimFromWebkitList(webKitFileList, e.target.value) + setLocalArchiveFromFileList(files); + } + }); + + if (params.isFileSystemApiSupported) { // Handles Folder selection when showDirectoryPicker is supported document.getElementById('folderSelect').addEventListener('click', async function (e) { e.preventDefault(); @@ -1345,7 +1349,7 @@ function displayFileSelect () { if (previousZimFiles.length !== 0) setLocalArchiveFromFileList(previousZimFiles); }) } - if (params.isWebkitDirApiSupported && !params.isFileSystemApiSupported && !isFireFoxOsNativeFileApiAvailable) { + if (params.isWebkitDirApiSupported) { // Handles Folder selection when webkitdirectory is supported but showDirectoryPicker is not document.getElementById('folderSelect').addEventListener('change', async function (e) { e.preventDefault(); @@ -1367,7 +1371,7 @@ function displayFileSelect () { await abstractFilesystemAccess.updateZimDropdownOptions(filenames, previousZimFile.length !== 0 ? lastFilename : ''); }) } - if (params.isFileSystemApiSupported && !isFireFoxOsNativeFileApiAvailable) { + if (params.isFileSystemApiSupported) { // Handles File selection when showOpenFilePicker is supported and uses the filesystem api document.getElementById('archiveFiles').addEventListener('click', async function (e) { e.preventDefault(); @@ -1376,18 +1380,25 @@ function displayFileSelect () { }); } else { // Fallbacks to simple file input with multi file selection - document.getElementById('archiveFiles').addEventListener('change', async function (e) { - if (params.isWebkitDirApiSupported || params.isFileSystemApiSupported) { - const activeFilename = e.target.files[0].name; - localStorage.setItem('zimFilenames', [activeFilename].join('|')); - await abstractFilesystemAccess.updateZimDropdownOptions([activeFilename], activeFilename); - } - - setLocalArchiveFromFileSelect(); - }); + useLegacyFilePicker(); } } +/** + * Adds a event listener to the file input to handle file selection (if no other file picker is supported) + */ +function useLegacyFilePicker () { + // Fallbacks to simple file input with multi file selection + document.getElementById('archiveFiles').addEventListener('change', async function (e) { + if (params.isWebkitDirApiSupported || params.isFileSystemApiSupported) { + const activeFilename = e.target.files[0].name; + localStorage.setItem('zimFilenames', [activeFilename].join('|')); + await abstractFilesystemAccess.updateZimDropdownOptions([activeFilename], activeFilename); + } + setLocalArchiveFromFileSelect(); + }); +} + function handleGlobalDragover (e) { e.preventDefault(); e.dataTransfer.dropEffect = 'link'; From b23f46dec71d5183cedf2f6c45aaf22726ae0c3d Mon Sep 17 00:00:00 2001 From: RG Date: Wed, 8 Nov 2023 01:38:33 +0530 Subject: [PATCH 45/45] [FIX] safari file picker not showing --- www/js/app.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/www/js/app.js b/www/js/app.js index d44413639..9ed4aee50 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -1331,9 +1331,12 @@ function displayFileSelect () { setLocalArchiveFromFileList(files); } else { if (webKitFileList === null) { - const element = localStorage.getItem('zimFilenames').split('|').length === 1 ? 'archiveFiles' : 'folderSelect'; - // console.log(localStorage.getItem('zimFilenames').split('|')); - document.getElementById(element).click(); + const element = localStorage.getItem('zimFilenames').split('|').length === 1 ? 'archiveFiles' : 'archiveFolders'; + if ('showPicker' in HTMLInputElement.prototype) { + document.getElementById(element).showPicker(); + return; + } + document.getElementById(element).click() return; } const files = abstractFilesystemAccess.getSelectedZimFromWebkitList(webKitFileList, e.target.value)