Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reopen last selected archive with File System Access API #1221

Merged
merged 6 commits into from
Mar 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions i18n/en.jsonp.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions i18n/es.jsonp.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions i18n/fr.jsonp.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 10 additions & 5 deletions www/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -393,7 +393,7 @@ <h3 id="credits" data-i18n="about-credits">Credits</h3>

<h3 id="licence" data-i18n="about-licence">Licence information</h3>
<div data-i18n="about-licence-text">
<p>Copyright 2013-2023 Mossroy, Peter-x, Jaifroid and other contributors.</p>
<p>Copyright 2013-2024 Mossroy, Jaifroid, Peter-x and other contributors.</p>
<p>This application is licensed under the GPL v3 Licence:</p>
<em>
<p>Kiwix is free software: you can redistribute it and/or modify
Expand Down Expand Up @@ -541,9 +541,14 @@ <h3 data-i18n="configure-display-settings-title">Display settings</h3>
</div>
<div class="checkbox">
<label data-i18n-tip="configure-display-openexternallinks-tip" title="Opens the external links outside kiwix-js (avoids some side-effects affecting kiwix-js UI).">
<input type="checkbox" name="openExternalLinksInNewTabs"
id="openExternalLinksInNewTabsCheck" checked>
<span data-i18n="configure-display-openexternallinks"><strong>Open external links in new tabs</strong>. Disabling this might break kiwix-js UI in some specific cases
<input type="checkbox" name="openExternalLinksInNewTabs" id="openExternalLinksInNewTabsCheck" checked>
<span data-i18n="configure-display-openexternallinks"><strong>Open external links in new tabs</strong>. Disabling this might break kiwix-js UI in some specific cases
</label>
</div>
<div id="reopenLastArchiveDiv" class="checkbox" style="display: none;">
<label data-i18n-tip="configure-display-reopenlastarchive-tip" title="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.">
<input type="checkbox" name="reopenLastArchive" id="reopenLastArchiveCheck" checked>
<span data-i18n="configure-display-reopenlastarchive"><strong>Automatically re-open last selected archive</strong> (only works if you grant permanent permission when prompted)</span>
</label>
</div>
<div class="form-group">
Expand Down
29 changes: 23 additions & 6 deletions www/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
Expand All @@ -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);
}
});
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions www/js/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down
14 changes: 9 additions & 5 deletions www/js/lib/abstractFilesystemAccess.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
Loading