From 0e5db5ba8ea48d73525eaf7a364ea9320746117d Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Sat, 2 Mar 2024 18:07:35 +0000 Subject: [PATCH] Reopen last selected archive with File System Access API #1218 (#1221) --- README.md | 16 +++++++++----- i18n/en.jsonp.js | 2 ++ i18n/es.jsonp.js | 2 ++ i18n/fr.jsonp.js | 2 ++ www/index.html | 15 ++++++++----- www/js/app.js | 29 ++++++++++++++++++++------ www/js/init.js | 6 ++++++ www/js/lib/abstractFilesystemAccess.js | 14 ++++++++----- 8 files changed, 65 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index f585085d..c3a4916e 100644 --- a/README.md +++ b/README.md @@ -124,12 +124,18 @@ best to display static content, but much functionality is likely to be broken. H You can switch between these content injection modes in Configuration, but if your browser supports ServiceWorker mode as an offline-first PWA, you are strongly advised to remain in this mode. -### Limitations +### File access and other limitations -It is not yet technically possible automatically re-open a selected ZIM file between sessions. However, browsers that support the File System Access API -or the `webkitdirectory` property of the File API, allow you to re-open a folder or directory of ZIMs with a quick permission prompt. Another alternative -is to drag-and-drop a ZIM file into the app. There are [versions of this app](https://kiwix.github.io/kiwix-js-pwa/app) that have experimental support for -the Origin Private File System, or that use frameworks like Electron, which do have the capability of remembering the chosen archive between app launches. +You can only re-open an archive automatically if your browser supports the File System Access API and allows you to grant permanent access permission. +In practice, this currently means Chromium browsers (Chrome, Edge, etc.) with a version number of 122 or higher. If that is the case, you will see a +popup asking you whether you wish to grant access "on every visit" (this will appear only after the second or third time that you have picked an archive +or folder). If you grant this permanent permission, then the browser will (optionally) re-open the last-visited archive when you open the app. + +In other cases, your browser may fall back to using the `webkitdirectory` property of the File API, which allows you to re-open a folder or directory of +ZIMs with a quick permission prompt. Another alternative is to drag-and-drop a ZIM file into the app. + +There are [versions of this app](https://kiwix.github.io/kiwix-js-pwa/app) that have experimental support for the Origin Private File System, or that use +frameworks like Electron, which do have the capability of remembering the chosen archive between app launches. The app has fast title search, and slower full-text search for ZIM archives that have a full-text index, thanks to the [openzim/javascript-libzim](https://github.com/openzim/javascript-libzim) project. Currently, full-text searching only works in browsers diff --git a/i18n/en.jsonp.js b/i18n/en.jsonp.js index 4946b26c..d6d7388d 100644 --- a/i18n/en.jsonp.js +++ b/i18n/en.jsonp.js @@ -49,6 +49,8 @@ document.localeJson = { "configure-display-homekeyfocus-tip": "Auto-focuses the search bar when you press the Home key, and also when you click or tap on the Home tab. Enables quick access to search, no matter where you are in an article.", "configure-display-openexternallinks": "Open external links in new tabs. Disabling this might break kiwix-js UI in some specific cases", "configure-display-openexternallinks-tip": "Opens the external links outside kiwix-js (avoids some side-effects affecting kiwix-js UI).", + "configure-display-reopenlastarchive": "Automatically re-open last selected archive (only works if you grant permanent permission when prompted)", + "configure-display-reopenlastarchive-tip": "If your browser supports the permanent permissions feature of the File System Access API, you can automatically re-open archives when you restart the app. To enable this functionality, you need to give permission to access files 'on every visit' when prompted by your browser.", "configure-display-selectapptheme": "Select app theme (content inversion is experimental):", "configure-display-selectapptheme-tip": "Allows selection of themes either for the app only, or for the app and the loaded content.", "configure-display-themeoption-light": "Light", diff --git a/i18n/es.jsonp.js b/i18n/es.jsonp.js index 5aacd911..c07eaa2f 100644 --- a/i18n/es.jsonp.js +++ b/i18n/es.jsonp.js @@ -49,6 +49,8 @@ document.localeJson = { "configure-display-homekeyfocus-tip": "Enfoca automáticamente la barra de búsqueda al pulsar la tecla Inicio, y también al hacer clic o tocar en la pestaña Inicio. Habilita acceso rápido a la búsqueda con el teclado, sin importar en qué parte del artículo esté.", "configure-display-openexternallinks": "Abrir enlaces externos en nuevas pestañas. Si desactiva esto, se puede romper la interfaz de kiwix-js en algunos casos específicos", "configure-display-openexternallinks-tip": "Abre los enlaces externos fuera de kiwix-js (evita algunos efectos secundarios que afectan la interfaz de kiwix-js).", + "configure-display-reopenlastarchive": "Reabrir automáticamente el último archivo seleccionado (sólo funciona si concede permiso permanente cuando se le solicite)", + "configure-display-reopenlastarchive-tip": "Si su navegador soporta la característica de permisos permanentes de la API de Acceso al Sistema de Archivos, puede reabrir automáticamente los archivos cuando reinicie la aplicación. Para habilitar esta funcionalidad, debe dar permiso para acceder a los archivos 'en cada visita' cuando su navegador se lo solicite.", "configure-display-selectapptheme": "Seleccionar tema de la aplicación (la inversión de contenido es experimental):", "configure-display-selectapptheme-tip": "Permite la selección de temas para la aplicación o para la aplicación y el contenido cargado.", "configure-display-themeoption-light": "Claro", diff --git a/i18n/fr.jsonp.js b/i18n/fr.jsonp.js index 558913e4..5608a229 100644 --- a/i18n/fr.jsonp.js +++ b/i18n/fr.jsonp.js @@ -49,6 +49,8 @@ document.localeJson = { "configure-display-homekeyfocus-tip": "La barre de recherche se focalise automatiquement lorsque vous appuyez sur la touche Accueil, ainsi que lorsque vous cliquez ou tapez sur l'onglet Accueil. Active l'accès rapide à la recherche par clavier, quel que soit l'endroit où vous vous trouvez dans l'article.", "configure-display-openexternallinks": "Ouvrir les liens externes dans de nouveaux onglets. Si vous désactivez cette option, l'interface de kiwix-js peut être cassée dans certains cas spécifiques", "configure-display-openexternallinks-tip": "Ouvre les liens externes en dehors de kiwix-js (évite certains effets secondaires affectant l'interface de kiwix-js).", + "configure-display-reopenlastarchive": "Rouvrir automatiquement la dernière archive sélectionnée (fonctionne uniquement si vous accordez la permission permanente lorsque vous y êtes invité(e))", + "configure-display-reopenlastarchive-tip": "Si votre navigateur prend en charge la fonctionnalité de permissions permanentes de l'API d'Accès au Système de Fichiers, vous pouvez rouvrir automatiquement les archives lorsque vous redémarrez l'application. Pour activer cette fonctionnalité, vous devez donner la permission d'accéder aux fichiers 'à chaque visite' lorsque votre navigateur vous le demande.", "configure-display-selectapptheme": "Sélectionner le thème de l'application (le mode inversé est expérimentale) :", "configure-display-selectapptheme-tip": "Permet de sélectionner un thème pour l'application ou pour l'application et le contenu chargé.", "configure-display-themeoption-light": "Clair", diff --git a/www/index.html b/www/index.html index 5c538d4c..52aaabf7 100644 --- a/www/index.html +++ b/www/index.html @@ -17,7 +17,7 @@ Peter-x - https://github.com/peter-x Jaifroid - https://github.com/Jaifroid - Copyright 2013-2023 Mossroy, Peter-x, Jaifroid, sharun-s and contributors + Copyright 2013-2024 Mossroy, Peter-x, Jaifroid, sharun-s and contributors Licence GPL v3: This file is part of Kiwix. @@ -393,7 +393,7 @@

Licence information

-

Copyright 2013-2023 Mossroy, Peter-x, Jaifroid and other contributors.

+

Copyright 2013-2024 Mossroy, Jaifroid, Peter-x and other contributors.

This application is licensed under the GPL v3 Licence:

Kiwix is free software: you can redistribute it and/or modify @@ -541,9 +541,14 @@

+
+
diff --git a/www/js/app.js b/www/js/app.js index 4fc39068..3a8d2fdf 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -499,7 +499,7 @@ document.getElementById('disableDragAndDropCheck').addEventListener('change', fu // Handle switching from jQuery to serviceWorker modes. document.getElementById('serviceworkerModeRadio').addEventListener('click', async function () { document.getElementById('enableSourceVerificationCheckBox').style.display = ''; - if (selectedArchive.isReady() && !(settingsStore.getItem("trustedZimFiles").includes(selectedArchive.file.name)) && params.sourceVerification) { + if (selectedArchive.isReady() && !(settingsStore.getItem('trustedZimFiles').includes(selectedArchive.file.name)) && params.sourceVerification) { await verifyLoadedArchive(selectedArchive); } }); @@ -511,7 +511,7 @@ document.getElementById('jqueryModeRadio').addEventListener('click', function () // Handle switching to serviceWorkerLocal mode for chrome-extension document.getElementById('serviceworkerLocalModeRadio').addEventListener('click', async function () { document.getElementById('enableSourceVerificationCheckBox').style.display = ''; - if (selectedArchive.isReady() && !(settingsStore.getItem("trustedZimFiles").includes(selectedArchive.file.name)) && params.sourceVerification) { + if (selectedArchive.isReady() && !(settingsStore.getItem('trustedZimFiles').includes(selectedArchive.file.name)) && params.sourceVerification) { await verifyLoadedArchive(selectedArchive); } }); @@ -561,6 +561,10 @@ document.querySelectorAll('input[type="checkbox"][name=openExternalLinksInNewTab settingsStore.setItem('openExternalLinksInNewTabs', params.openExternalLinksInNewTabs, Infinity); }) }); +document.getElementById('reopenLastArchiveCheck').addEventListener('change', function (e) { + params.reopenLastArchive = e.target.checked; + settingsStore.setItem('reopenLastArchive', params.reopenLastArchive, Infinity); +}); document.getElementById('appThemeSelect').addEventListener('change', function (e) { params.appTheme = e.target.value; settingsStore.setItem('appTheme', params.appTheme, Infinity); @@ -633,7 +637,7 @@ function focusPrefixOnHomeKey (event) { * @param {archive} the archive that needs verification * */ async function verifyLoadedArchive (archive) { - const response = await uiUtil.systemAlert(translateUI.t('dialog-sourceverification-alert') || "Is this ZIM archive from a trusted source?\n If not, you can still read the ZIM file in Safe Mode (aka JQuery mode). Closing this window also opens the file in Safe Mode. This option can be disabled in Expert Settings", translateUI.t('dialog-sourceverification-title') || "Security alert!", true, translateUI.t('dialog-sourceverification-safe-mode-button') || 'Open in Safe Mode', translateUI.t('dialog-sourceverification-trust-button')|| 'Trust Source'); + const response = await uiUtil.systemAlert(translateUI.t('dialog-sourceverification-alert') || 'Is this ZIM archive from a trusted source?\n If not, you can still read the ZIM file in Safe Mode (aka JQuery mode). Closing this window also opens the file in Safe Mode. This option can be disabled in Expert Settings', translateUI.t('dialog-sourceverification-title') || 'Security alert!', true, translateUI.t('dialog-sourceverification-safe-mode-button') || 'Open in Safe Mode', translateUI.t('dialog-sourceverification-trust-button') || 'Trust Source'); if (response) { params.contentInjectionMode = 'serviceworker'; var trustedZimFiles = settingsStore.getItem('trustedZimFiles'); @@ -1311,14 +1315,27 @@ if ($.isFunction(navigator.getDeviceStorages)) { }); } +// @AUTOLOAD of archives starts here for frameworks or APIs that allow it + +// If DeviceStorage is available (Firefox OS), we look for archives in it if (storages !== null && storages.length > 0) { // Make a fake first access to device storage, in order to ask the user for confirmation if necessary. // This way, it is only done once at this moment, instead of being done several times in callbacks // After that, we can start looking for archives storages[0].get('fake-file-to-read').then(searchForArchivesInPreferencesOrStorage, searchForArchivesInPreferencesOrStorage); +// If the File System Access API is available, we may be able to autoload the last selected archive in Chromium > 122 +// which has persistent permissions +} else if (params.reopenLastArchive && window.showOpenFilePicker && params.previousZimFileName) { + displayFileSelect(); + abstractFilesystemAccess.getSelectedZimFromCache(params.previousZimFileName).then(function (files) { + setLocalArchiveFromFileList(files); + }).catch(function (err) { + console.warn(err); + document.getElementById('btnConfigure').click(); + }); +// If no autoload API is available, we display the file select dialog } else { - // If DeviceStorage is not available, we display the file select components displayFileSelect(); if (archiveFiles.files && archiveFiles.files.length > 0) { // Archive files are already selected, @@ -1714,9 +1731,9 @@ async function archiveReadyCallback (archive) { if (params.sourceVerification && (params.contentInjectionMode === 'serviceworker' || params.contentInjectionMode === 'serviceworkerlocal')) { // Check if source of the zim file can be trusted. if (!(settingsStore.getItem('trustedZimFiles').includes(archive.file.name))) { - await verifyLoadedArchive(archive); + await verifyLoadedArchive(archive); + } } -} // When a new ZIM is loaded, we turn this flag to null, so that we don't get false positive attempts to use the Worker // It will be defined as false or true when the first article is loaded appstate.isReplayWorkerAvailable = null; diff --git a/www/js/init.js b/www/js/init.js index e538e30b..faaaa380 100644 --- a/www/js/init.js +++ b/www/js/init.js @@ -128,6 +128,8 @@ params['isWebkitDirApiSupported'] = 'webkitdirectory' in document.createElement( params['sourceVerification'] = params.contentInjectionMode === 'serviceworker' ? (getSetting('sourceVerification') === null ? true : getSetting('sourceVerification')) : false; // Sets a boolean indicating weather a user trusts the source of zim files params['libzimMode'] = getSetting('libzimMode') || 'wasm'; // Sets a value indicating which libzim mode is selected params['useLibzim'] = !!getSetting('useLibzim'); // Sets a value indicating which libzim mode is selected +params['previousZimFileName'] = getSetting('previousZimFileName') || ''; // Sets the name of the last opened zim file +params['reopenLastArchive'] = getSetting('reopenLastArchive') !== false; // Sets a Boolean defaulting to true indicating whether to reopen the last opened zim file if possible /** * Apply any override parameters that might be in the querystring. @@ -194,6 +196,10 @@ document.getElementById('libzimModeSelect').value = params.libzimMode; document.getElementById('useLibzim').checked = params.useLibzim; document.getElementById('appVersion').textContent = 'Kiwix ' + params.appVersion; document.getElementById('enableSourceVerification').checked = getSetting('sourceVerification') === null ? true : getSetting('sourceVerification'); +document.getElementById('reopenLastArchiveCheck').checked = params.reopenLastArchive; +// If the File System Access API is supported, unhide the reopenLastArchiveDiv +if (params.isFileSystemApiSupported) document.getElementById('reopenLastArchiveDiv').style.display = ''; + // This is a simplified version of code in settingsStore, because that module is not available in init.js function getSetting (name) { var result; diff --git a/www/js/lib/abstractFilesystemAccess.js b/www/js/lib/abstractFilesystemAccess.js index 69e20b31..37d60443 100644 --- a/www/js/lib/abstractFilesystemAccess.js +++ b/www/js/lib/abstractFilesystemAccess.js @@ -181,12 +181,16 @@ function getSelectedZimFromCache (selectedFilename) { return new Promise((resolve, reject) => { cache.idxDB('zimFiles', async function (fileOrDirHandle) { if (!fileOrDirHandle) { - reject(new Error('No file or directory selected')); + return reject(new Error('No file or directory selected')); + } + // Request permission if not already granted + if ((await fileOrDirHandle.queryPermission()) !== 'granted') { + try { + await fileOrDirHandle.requestPermission(); + } catch (error) { + return reject(new Error('Permission denied', error)); + } } - // Left it here for debugging purposes as its sometimes asking for permission even when its granted - // console.debug('FileHandle and Permission', fileOrDirHandle, await fileOrDirHandle.queryPermission()) - if ((await fileOrDirHandle.queryPermission()) !== 'granted') await fileOrDirHandle.requestPermission(); - if (fileOrDirHandle.kind === 'directory') { const files = []; for await (const entry of fileOrDirHandle.values()) {