Support the HTMLInputElement:webkitdirectory API #421 (#422)

This commit is contained in:
Jaifroid 2023-07-14 11:25:47 +01:00 committed by GitHub
parent 54cb4aa8c9
commit 759d59b04b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 568 additions and 369 deletions

View File

@ -19,6 +19,7 @@ module.exports = {
'no-unused-vars': 1,
'n/no-callback-literal': 0,
'object-shorthand': 0,
'one-var': 0,
'multiline-ternary': 0,
'no-extend-native': 0,
'no-global-assign': 0

View File

@ -53,7 +53,7 @@ jobs:
echo "Changing to the dist directory"
cd dist && pwd
# Get archive name
packagedFile="$(grep -m1 'params\[.packagedFile' www/js/init.js | sed -E 's/^[^"]+"([^"]+\.zim)".+/\1/')"
packagedFile=$(grep -m1 'params\[.packagedFile' www/js/init.js | sed -E "s/^.+'([^']+\.zim)'.+/\1/")
# If file doesn't exist in FS, download it
if [ ! -f "archives/$packagedFile" ]; then
# Generalize the name if cron_launched and download it
@ -144,7 +144,7 @@ jobs:
run: |
echo "Changing to the dist directory"
cd dist && pwd
$packagedFile = (Select-String 'packagedFile' "www\js\init.js" -List) -ireplace '^[^"]+"([^"]+\.zim)".+', '$1'
$packagedFile = (Select-String 'packagedFile' "www\js\init.js" -List) -ireplace "^.+'([^']+\.zim)'.+", '$1'
if ($packagedFile -and ! (Test-Path "archives\$packagedFile" -PathType Leaf)) {
# File not in archives, so generalize the name (if nightly) and download it
Write-Host "`nDownloading https://download.kiwix.org/zim/$packagedFile"
@ -190,7 +190,7 @@ jobs:
}
# To ensure there is enough disk space, we can delete the archive that is no longer needed
rm -r dist/archives
./scripts/Create-DraftRelease -buildonly -tag_name $INPUT_VERSION_E -portableonly -nobundle -wingetprompt N
./scripts/Create-DraftRelease -buildonly -tag_name $INPUT_VERSION_E -portableonly -nobundle -wingetprompt N -nobranchcheck
}
- name: Publish packages
if: github.event.inputs.target != 'artefacts'
@ -241,7 +241,7 @@ jobs:
run: |
echo "Changing to the dist directory"
cd dist && pwd
$packagedFile = (Select-String 'packagedFile' "www\js\init.js" -List) -ireplace '^[^"]+"([^"]+\.zim)".+', '$1'
$packagedFile = (Select-String 'packagedFile' "www\js\init.js" -List) -ireplace "^.+'([^']+\.zim)'.+", '$1'
if ($packagedFile -and ! (Test-Path "archives\$packagedFile" -PathType Leaf)) {
# File not in archives, so generalize the name (if nightly) and download it
Write-Host "`nDownloading https://download.kiwix.org/zim/$packagedFile"

View File

@ -1,7 +1,7 @@
{
"name": "kiwix-js-electron",
"productName": "Kiwix JS Electron",
"version": "2.5.4-E",
"version": "2.5.4-N",
"description": "Kiwix JS packaged for the Electron framework",
"main": "main.cjs",
"type": "module",
@ -88,6 +88,7 @@
"index.html",
"CHANGELOG.md",
"LICENCE",
"manifest.json",
"www/**",
"preload.cjs",
"main.cjs",
@ -97,10 +98,13 @@
"scripts": {
"serve": "vite",
"preview": "del-cli dist && npm run build-src && vite preview",
"build": "del-cli dist && npm run build-src && npm run build-min",
"prebuild": "del-cli dist",
"build": "rollup --config --file dist/www/js/bundle.js && rollup --config --file dist/www/js/bundle.min.js --environment BUILD:production",
"prebuild-min": "del-cli dist",
"build-min": "rollup --config --file dist/www/js/bundle.min.js --environment BUILD:production",
"prebuild-src": "del-cli dist",
"build-src": "rollup --config --file dist/www/js/bundle.js",
"del-dist": "del-cli dist",
"del": "del-cli dist",
"start": "electron .",
"dist-win": "electron-builder build --win --projectDir dist",
"dist-win-nsis": "electron-builder build --win NSIS:x64 NSIS:ia32 --projectDir dist",
@ -154,3 +158,5 @@
"electron-updater": "^6.1.0"
}
}

View File

@ -16,18 +16,20 @@
"start": "run --x86 --mirror https://dl.nwjs.io/ .",
"dist-win-x86": "build --tasks win-x86 --mirror https://dl.nwjs.io/ .",
"dist-win-x64": "build --tasks win-x64 --mirror https://dl.nwjs.io/ .",
"dist": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./scripts/Build_NWJS.ps1"
"dist": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./scripts/Build-NWJS.ps1"
},
"build-xp": {
"nwVersion": "0.14.7",
"output": "bld/nwjs/win-x86-xp",
"files": [
"archives/**",
"service-worker.js",
"index.html",
"CHANGELOG.md",
"LICENCE",
"manifest.json",
"www/**",
"archives/README.md",
"archives/wikip*.*",
"!**/*.dev.{js,wasm}"
],
"win": {
@ -38,12 +40,14 @@
"nwVersion": "0.72.0",
"output": "bld/nwjs/win-x64",
"files": [
"archives/**",
"service-worker.js",
"index.html",
"CHANGELOG.md",
"LICENCE",
"manifest.json",
"www/**",
"archives/README.md",
"archives/wikip*.*",
"!**/*.dev.{js,wasm}"
],
"win": {
@ -58,6 +62,7 @@
"index.html",
"CHANGELOG.md",
"LICENCE",
"manifest.json",
"www/**",
"archives/README.md",
"archives/wikip*.*",

View File

@ -1,6 +1,7 @@
[CmdletBinding()]
param (
[switch]$only32bit = $false
[switch]$only32bit = $false,
[switch]$usesdk = $false
)
$builds = @("win-ia32", "win-xp")
if (-Not $only32bit) {
@ -41,17 +42,21 @@ foreach ($build in $builds) {
$folderTarget = "$PSScriptRoot\..\dist\bld\nwjs\$build-$version"
$target = "$folderTarget\kiwix_js_windows$sep$appBuild"
$fullTarget = "$target-$build"
$sdk = ""
if ($usesdk) {
$sdk = "-sdk"
}
$ZipFolder = "$PSScriptRoot\..\dist\node_modules\nwjs-builder-phoenix\caches\"
$ZipLocation = $ZipFolder + "nwjs-v$version-$build.zip"
$ZipLocation = $ZipFolder + "nwjs$sdk-v$version-$build.zip"
$UnzipLocation = "$ZipLocation-extracted\"
$buildLocation = "$ZipLocation-extracted\nwjs-v$version-$build\"
$buildLocation = "$ZipLocation-extracted\nwjs$sdk-v$version-$build\"
if (-Not (Test-Path $ZipFolder -PathType Container)) {
mkdir $ZipFolder
}
if (-Not (Test-Path $buildLocation -PathType Container)) {
# We need to download and/or unzip the release, as it is not available
if (-Not (Test-Path $ZipLocation -PathType Leaf)) {
$serverFile = "https://dl.nwjs.io/v$version/nwjs-v$version-$build.zip"
$serverFile = "https://dl.nwjs.io/v$version/nwjs$sdk-v$version-$build.zip"
"Downloading $serverFile"
Invoke-WebRequest -Uri $serverFile -OutFile $ZipLocation
}
@ -72,7 +77,7 @@ foreach ($build in $builds) {
# Copy latest binary x64
cp $buildLocation\* $fullTarget -Recurse
$root = $PSScriptRoot -replace 'scripts.*$', ''
cp $root\dist\package.json, $root\dist\service-worker.js, $root\dist\index.html, $root\CHANGELOG.md, $root\LICENSE, $root\dist\www $fullTarget -Recurse
cp $root\dist\package.json, $root\dist\service-worker.js, $root\dist\index.html, $root\CHANGELOG.md, $root\LICENSE, $root\manifest.json, $root\dist\www $fullTarget -Recurse
# Remove unwanted files
# del $fullTarget\www\js\lib\libzim-*.dev.*
"Copying archive..."
@ -101,3 +106,4 @@ foreach ($build in $builds) {
Compress-Archive "$PSScriptRoot\..\dist\bld\nwjs\$build-$version\*" "$PSScriptRoot\..\dist\bld\nwjs\$foldername.zip" -Force
"Build $OBuild finished.`n"
}

View File

@ -11,7 +11,8 @@ param (
[string]$electronbuild = "", # 'local' or 'cloud'
[switch]$portableonly = $false, # If set, only the portable electron build will be built. Implies local electron build.
[switch]$updatewinget = $false,
[string]$wingetprompt = "" # Provide an override response (Y/N) to the winget prompt at the end of the script - for automation
[string]$wingetprompt = "", # Provide an override response (Y/N) to the winget prompt at the end of the script - for automation
[switch]$nobranchcheck = $false # If set, will not check that the current branch is correct for the type of app to build
)
# DEV: To build Electron packages for all platforms and NWJS for XP and Vista in a single release, use, e.g., "v1.3.0E+N" (Electron + NWJS)
# DEV: To build UWP + Electron in a single release (for WikiMed or Wikivoyage), use "v1.3.0+E" (plus Electron)
@ -185,8 +186,12 @@ if ($numeric_tag -ne $file_tag_numeric) {
$actual_branch = git rev-parse --abbrev-ref HEAD
if ($branch -ne $actual_branch) {
Write-Host "`nError! The branch you are on [$actual_branch] does not match the type of app you wish to build [$branch]!" -ForegroundColor Red
Write-Host "Please switch to the correct branch and try again.`n" -ForegroundColor Red
exit 1
if (-not $nobranchcheck) {
Write-Host "Please switch to the correct branch and try again.`n" -ForegroundColor Red
exit 1
} else {
Write-Host "Continuing anyway as you have specified the -nobranchcheck option.`n" -ForegroundColor Yellow
}
}
# Determine type of Electron build if any
@ -290,7 +295,7 @@ if (-Not ($dryrun -or $buildonly)) {
if (-Not $nobundle) {
"`nBuilding production bundle with rollup..."
if (-Not $dryrun) {
& npm run del-dist && npm run build-min
& npm run build-min
} else {
"[DRYRUN] & npm run build"
}
@ -428,7 +433,7 @@ if ($dryrun -or $buildonly -or $release.assets_url -imatch '^https:') {
}
}
# Check for the existence of the requested packaged archive
$packagedFile = (Select-String 'packagedFile' "dist\www\js\init.js" -List) -ireplace '^[^"]+"([^"]+\.zim)".+', '$1'
$packagedFile = (Select-String 'packagedFile' "dist\www\js\init.js" -List) -ireplace "^.+['`"]([^'`"]+\.zim)['`"].+", '$1'
if ($packagedFile -and ! (Test-Path "dist\archives\$packagedFile" -PathType Leaf)) {
# File not in archives
$downloadArchiveChk = Read-Host "`nWe could not find the packaged archive, do you wish to download it? Y/N"

View File

@ -60,7 +60,7 @@ var useAssetsCache = true;
* This is an expert setting in Configuration
* @type {Boolean}
*/
var useAppCache = true;
var useAppCache = true;
/**
* A Boolean that governs whether images are displayed

View File

@ -742,6 +742,7 @@
<div class="row" style="padding-bottom: 0.5em;">
<input type="file" accept=".zim,.zimaa,.zimab,.zimac,.zimad,.zimae,.zimaf,.zimag,.zimah,.zimai,.zimaj,.zimak,.zimal,.zimam,.ziman,.zimao,.zimap,.zimaq,.zimar,.zimas,.zimat,.zimau,.zimav,.zimaw,.zimax,.zimay,.zimaz,.zimba,.zimbb,.zimbc,.zimbd,.zimbe,.zimbf,.zimbg,.zimbh,.zimbi,.zimbj,.zimbk,.zimbl,.zimbm,.zimbn,.zimbo,.zimbp,.zimbq,.zimbr,.zimbs,.zimbt,.zimbu,.zimbv,.zimbw,.zimbx,.zimby,.zimbz"
class="btn btn-primary btn-inline" value="Select folder with ZIM files" id="archiveFilesLegacy" multiple />
<input type="file" class="btn btn-primary btn-inline" value="Select folder" id="archiveDirLegacy" webkitdirectory />
</div>
<div id="instructions" class="row">
<p>

View File

@ -55,7 +55,7 @@ const DELAY_BETWEEN_KEEPALIVE_SERVICEWORKER = 30000;
*/
// The global parameter and app state objects are defined in init.js
/* global params, appstate, nw, electronAPI, Windows, webpMachine */
/* global params, appstate, nw, electronAPI, Windows, webpMachine, dialog */
// Placeholders for the article container, the article window and the article DOM
var articleContainer = document.getElementById('articleContent');
@ -80,6 +80,9 @@ appstate['search'] = {
params['storeType'] = null;
params['storeType'] = settingsStore.getBestAvailableStorageAPI();
// A parameter to determine whether the webkitdirectory API is available
params['webkitdirectory'] = util.webkitdirectorySupported();
// Placeholder for the alert box header element, so it can be displayed and hidden easily
const alertBoxHeader = document.getElementById('alertBoxHeader');
@ -754,6 +757,11 @@ document.getElementById('btnHome').addEventListener('click', function () {
var currentArchive = document.getElementById('currentArchive');
var currentArchiveLink = document.getElementById('currentArchiveLink');
var openCurrentArchive = document.getElementById('openCurrentArchive');
var archiveFilesLegacy = document.getElementById('archiveFilesLegacy');
var archiveDirLegacy = document.getElementById('archiveDirLegacy');
if (!params.webkitdirectory) {
document.getElementById('archiveDirLegacy').style.display = 'none';
}
function setTab (activeBtn) {
// Highlight the selected section in the navbar
@ -776,8 +784,8 @@ function setTab (activeBtn) {
} else {
cssUIThemeGetOrSet(determinedTheme);
}
if (typeof Windows === 'undefined' && typeof window.showOpenFilePicker !== 'function' && !window.dialog) {
// If not UWP, File System Access API, or Electron methods, display legacy File Select
if (typeof Windows === 'undefined' && typeof window.showOpenFilePicker !== 'function' && !window.dialog && !params.webkitdirectory) {
// If not UWP, File System Access API, webkitdirectory API or Electron methods, display legacy File Select
document.getElementById('archiveFilesDiv').style.display = 'none';
document.getElementById('archivesFound').style.display = 'none';
document.getElementById('instructions').style.display = appstate.selectedArchive ? 'none' : 'block';
@ -1063,8 +1071,11 @@ function getNativeFSHandle (callback) {
} else {
// We have failed to load a picked archive via the File System API, but if params.storedFilePath exists, then the archive
// was launched with Electron APIs, so we can get the folder that way
if (params.storedFilePath) params.pickedFolder = params.storedFilePath.replace(/[\\/]+[^\\/]+$/, '');
searchForArchivesInPreferencesOrStorage();
if (params.storedFile && params.storedFilePath) params.pickedFolder = params.pickedFolder = params.storedFilePath.replace(/[^\\/]+$/, '');
scanNodeFolderforArchives(params.pickedFolder, function () {
// We now have the list of archives in the dropdown, so we try to select the storedFile
setLocalArchiveFromArchiveList(params.storedFile);
});
}
}
});
@ -1081,7 +1092,7 @@ document.getElementById('btnAbout').addEventListener('click', function () {
}
// Check if we're 'unclicking' the button
var searchDiv = document.getElementById('about');
if (searchDiv.style.display != 'none') {
if (searchDiv.style.display !== 'none') {
setTab();
return;
}
@ -1122,10 +1133,17 @@ function selectArchive (list) {
// Void any previous picked file to prevent it launching
if (params.pickedFile && params.pickedFile.name !== selected) {
params.pickedFile = '';
params.storedFile = '';
}
if (!window.fs && window.showOpenFilePicker) {
if (window.showOpenFilePicker) {
getNativeFSHandle(function (handle) {
if (!handle) {
if (window.fs && params.storedFilePath) {
// Fall back to using the Electron APIs
params.pickedFolder = params.storedFilePath.replace(/[^\\/]+$/, '');
setLocalArchiveFromArchiveList(selected);
return;
}
console.error('No handle was retrieved');
uiUtil.systemAlert('We could not get a handle to the previously picked file or folder!<br>' +
'This is probably because the contents of the folder have changed. Please try picking it again.');
@ -1149,8 +1167,27 @@ function selectArchive (list) {
});
}
});
} else if (typeof MSApp === 'undefined' && !window.fs && params.webkitdirectory) {
// If we don't have any picked files or directories...
if (!archiveDirLegacy.files.length && !archiveFilesLegacy.files.length) {
appstate.waitForFileSelect = selected;
// No files are set, so we need to ask user to select the file or directory again
if (params.pickedFolder || document.getElementById('archiveList').options.length > 1) {
archiveDirLegacy.click();
} else {
archiveFilesLegacy.click();
}
} else {
console.debug('Files are set, attempting to select ' + selected);
params.pickedFile = selected;
if (archiveDirLegacy.files.length) {
params.pickedFolder = archiveDirLegacy.files[0].webkitRelativePath.replace(/\/[^/]*$/, '');
params.pickedFile = '';
}
setLocalArchiveFromArchiveList(selected);
}
} else {
setLocalArchiveFromArchiveList([selected]);
setLocalArchiveFromArchiveList(selected);
}
setTimeout(function () {
document.getElementById('openLocalFiles').style.display = 'none';
@ -1161,7 +1198,30 @@ function selectArchive (list) {
}
// Legacy file picker is used as a fallback when all other pickers are unavailable
document.getElementById('archiveFilesLegacy').addEventListener('change', setLocalArchiveFromFileSelect);
archiveFilesLegacy.addEventListener('change', function (files) {
var filesArray = Array.from(files.target.files);
params.pickedFolder = null;
params.pickedFile = filesArray[0];
params.storedFile = params.pickedFile.name.replace(/\.zim\w\w$/i, '.zimaa');
if (params.webkitdirectory) {
settingsStore.setItem('pickedFolder', '', Infinity);
processFilesArray(filesArray);
}
var selected = params.storedFile;
if (appstate.waitForFileSelect) {
selected = appstate.waitForFileSelect;
appstate.waitForFileSelect = null;
// Select the selected file in the dropdown list of archives
document.getElementById('archiveList').value = selected;
console.debug('Files are set, attempting to select ' + selected);
}
if (!window.fs && params.webkitdirectory) {
// populateDropDownListOfArchives([params.pickedFile], true);
setLocalArchiveFromArchiveList(selected);
} else {
setLocalArchiveFromFileList(files.target.files);
}
});
// But in preference, use UWP, File System Access API
document.getElementById('archiveFile').addEventListener('click', function () {
if (typeof Windows !== 'undefined' && typeof Windows.Storage !== 'undefined') {
@ -1173,9 +1233,45 @@ document.getElementById('archiveFile').addEventListener('click', function () {
} else if (window.fs && window.dialog) {
// Electron file picker if showOpenFilePicker is not available
dialog.openFile();
} else {
// Legacy file picker
archiveFilesLegacy.click();
}
});
document.getElementById('archiveFiles').addEventListener('click', function () {
// Legacy webkitdirectory file picker is used as a fallback when File System Access API is unavailable
archiveDirLegacy.addEventListener('change', function (files) {
if (files.target.files.length) {
var filesArray = Array.from(files.target.files);
// Supports reading in NWJS/Electron frameworks that have a path property on the File object
var path = filesArray[0] ? filesArray[0].path ? filesArray[0].path : filesArray[0].webkitRelativePath : '';
params.pickedFile = null;
var oldDir = params.pickedFolder;
params.pickedFolder = path.replace(/[^\\/]*$/, '');
// If we're picking a different directroy, don't look for the previously picked file in it
if (params.pickedFolder !== oldDir) {
params.storedFile = null;
params.storedFilePath = null;
}
settingsStore.setItem('pickedFolder', params.pickedFolder, Infinity);
if (document.getElementById('archiveList').options.length === 0) {
params.storedFile = null;
}
processFilesArray(filesArray);
var selected = '';
if (appstate.waitForFileSelect) {
selected = appstate.waitForFileSelect;
appstate.waitForFileSelect = null;
// Select the selected file in the dropdown list of archives
document.getElementById('archiveList').value = selected;
console.debug('Files are set, attempting to select ' + selected);
}
if (selected) setLocalArchiveFromArchiveList(selected);
} else {
appstate.waitForFileSelect = null;
console.log('User cancelled directory picker, or chose a directory with no files');
}
});
document.getElementById('archiveFiles').addEventListener('click', function (e) {
if (typeof Windows !== 'undefined' && typeof Windows.Storage !== 'undefined') {
// UWP FolderPicker
pickFolderUWP();
@ -1185,7 +1281,10 @@ document.getElementById('archiveFiles').addEventListener('click', function () {
} else if (window.fs && window.dialog) {
// Electron fallback
dialog.openDirectory();
}
} else if (params.webkitdirectory) {
// Legacy webkitdirectory file picker
archiveDirLegacy.click();
}
});
document.getElementById('btnRefresh').addEventListener('click', function () {
// Refresh list of archives
@ -1197,6 +1296,8 @@ document.getElementById('btnRefresh').addEventListener('click', function () {
scanUWPFolderforArchives(params.pickedFolder)
} else if (window.fs) {
scanNodeFolderforArchives(params.pickedFolder);
} else if (params.webkitdirectory) {
document.getElementById('archiveFiles').click();
}
} else if (typeof window.showOpenFilePicker === 'function' && !params.pickedFile) {
getNativeFSHandle(function (fsHandle) {
@ -1804,9 +1905,9 @@ function cssUIThemeGetOrSet (value, getOnly) {
var elements;
if (value == 'dark') {
document.getElementsByTagName('body')[0].classList.add('dark');
document.getElementById('archiveFilesLegacy').classList.add('dark');
archiveFilesLegacy.classList.add('dark');
document.getElementById('footer').classList.add('darkfooter');
document.getElementById('archiveFilesLegacy').classList.remove('btn');
archiveFilesLegacy.classList.remove('btn');
document.getElementById('findInArticle').classList.add('dark');
document.getElementById('prefix').classList.add('dark');
elements = document.querySelectorAll('.settings');
@ -1820,8 +1921,8 @@ function cssUIThemeGetOrSet (value, getOnly) {
document.getElementsByTagName('body')[0].classList.remove('dark');
document.getElementById('search-article').classList.remove('dark');
document.getElementById('footer').classList.remove('darkfooter');
document.getElementById('archiveFilesLegacy').classList.remove('dark');
document.getElementById('archiveFilesLegacy').classList.add('btn');
archiveFilesLegacy.classList.remove('dark');
archiveFilesLegacy.classList.add('btn');
document.getElementById('findInArticle').classList.remove('dark');
document.getElementById('prefix').classList.remove('dark');
elements = document.querySelectorAll('.settings');
@ -2535,7 +2636,7 @@ var storages = [];
function searchForArchivesInPreferencesOrStorage () {
// First see if the list of archives is stored in the cookie
var listOfArchivesFromCookie = settingsStore.getItem('listOfArchives');
if (listOfArchivesFromCookie !== null && listOfArchivesFromCookie !== undefined && listOfArchivesFromCookie !== '') {
if (listOfArchivesFromCookie) {
var directories = listOfArchivesFromCookie.split('|');
populateDropDownListOfArchives(directories);
} else {
@ -2591,7 +2692,8 @@ if ($.isFunction(navigator.getDeviceStorages)) {
}
if (storages !== null && storages.length > 0 ||
typeof Windows !== 'undefined' && typeof Windows.Storage !== 'undefined' ||
typeof window.fs !== 'undefined' || typeof window.showOpenFilePicker === 'function') {
typeof window.fs !== 'undefined' || typeof window.showOpenFilePicker === 'function' ||
params.webkitdirectory) {
if (window.fs && !(params.pickedFile || params.pickedFolder)) {
// Below we compare the prefix of the files, i.e. the generic filename without date, so we can smoothly deal with upgrades
if (params.packagedFile && params.storedFile.replace(/(^[^-]+all).+/, '$1') === params.packagedFile.replace(/(^[^-]+all).+/, '$1')) {
@ -2604,12 +2706,12 @@ if (storages !== null && storages.length > 0 ||
} else if (/\/archives\//.test(params.storedFilePath) && ~params.storedFilePath.indexOf(params.storedFile)) {
// We're in an Electron / NWJS app, and there is a stored file in the archive, but it's not the packaged archive!
// Probably there is more than one archive in the archive folder, so we are forced to use .fs code
console.warn('There may be more than one archive in the directory ' + params.storedFilePath.replace(/[^\/]+$/, ''));
console.warn('There may be more than one archive in the directory ' + params.storedFilePath.replace(/[^\\/]+$/, ''));
params.pickedFile = params.storedFile;
}
}
if (!params.pickedFile) {
if (params.storedFile && window.showOpenFilePicker) {
if (params.storedFile && (window.showOpenFilePicker)) {
// We are in an app with support for File System Access API, so we can't auto-load the file, show file pickers
document.getElementById('btnConfigure').click();
} else {
@ -2618,9 +2720,8 @@ if (storages !== null && storages.length > 0 ||
} else if (typeof Windows !== 'undefined' && typeof Windows.Storage !== 'undefined') {
console.log('Loading picked file for UWP app...');
processPickedFileUWP(params.pickedFile);
// } else if (launchArguments && 'launchQueue' in window) {
// // The app was launched with a file
// processNativeFileHandle(params.pickedFile);
} else if (!window.fs && params.webkitdirectory) {
searchForArchivesInPreferencesOrStorage();
} else {
// @AUTOLOAD packaged archive in Electron and NWJS packaged apps
// We need to read the packaged file using the node File System API (so user doesn't need to pick it on startup)
@ -2676,7 +2777,6 @@ if (storages !== null && storages.length > 0 ||
uiUtil.systemAlert(message);
}, 10);
}
});
document.getElementById('hideFileSelectors').style.display = params.showFileSelectors ? 'inline' : 'none';
}
@ -2751,16 +2851,16 @@ function populateDropDownListOfArchives (archiveDirectories, displayOnly) {
comboArchiveList.options[i] = new Option(archiveDirectory, archiveDirectory);
}
}
// Store the list of archives in a cookie, to avoid rescanning at each start
// Store the list of archives in settingsStore, to avoid rescanning at each start
settingsStore.setItem('listOfArchives', archiveDirectories.join('|'), Infinity);
comboArchiveList.size = comboArchiveList.length > 15 ? 15 : comboArchiveList.length;
// Kiwix-Js-Windows #23 - remove dropdown caret if only one archive
if (comboArchiveList.length > 1) comboArchiveList.removeAttribute('multiple');
if (comboArchiveList.length == 1) comboArchiveList.setAttribute('multiple', '1');
if (comboArchiveList.length === 1) comboArchiveList.setAttribute('multiple', '1');
if (comboArchiveList.options.length > 0) {
// If we're doing a rescan, then don't attempt to jump to the last selected archive, but leave selectors open
var lastSelectedArchive = params.rescan ? '' : params.storedFile;
if (lastSelectedArchive !== null && lastSelectedArchive !== undefined && lastSelectedArchive !== '') {
if (lastSelectedArchive) {
// || comboArchiveList.options.length == 1
// Either we have previously chosen a file, or there is only one file
// Attempt to select the corresponding item in the list, if it exists
@ -2777,24 +2877,26 @@ function populateDropDownListOfArchives (archiveDirectories, displayOnly) {
// We can't find lastSelectedArchive in the archive list
// Let's first check if this is a Store UWP/PWA that has a different archive package from that last selected
// (or from that indicated in init.js)
if (typeof Windows !== 'undefined' && typeof Windows.Storage !== 'undefined' &&
params.packagedFile && settingsStore.getItem('lastSelectedArchive') !== params.packagedFile) {
// We didn't pick this file previously, so select first one in list
params.storedFile = archiveDirectories[0];
params.fileVersion = ~params.fileVersion.indexOf(params.storedFile.replace(/\.zim\w?\w?$/i, '')) ? params.fileVersion : params.storedFile;
setLocalArchiveFromArchiveList(params.storedFile);
} else {
// It's genuinely no longer available, so let's ask the user to pick it
var message = '<p>We could not find the archive <b>' + lastSelectedArchive + '</b>!</p><p>Please select its location...</p>';
if (typeof Windows !== 'undefined' && typeof Windows.Storage !== 'undefined')
message += '<p><i>Note:</i> If you drag-drop an archive into this UWP app, then it will have to be dragged again each time you launch the app. Try double-clicking on the archive instead, or select it using the controls on this page.</p>';
if (document.getElementById('configuration').style.display == 'none') {
document.getElementById('btnConfigure').click();
}
uiUtil.systemAlert(message).then(function () {
displayFileSelect();
});
// if (typeof Windows !== 'undefined' && typeof Windows.Storage !== 'undefined' &&
// params.packagedFile && settingsStore.getItem('lastSelectedArchive') !== params.packagedFile) {
// // We didn't pick this file previously, so select first one in list
// params.storedFile = archiveDirectories[0];
// params.fileVersion = ~params.fileVersion.indexOf(params.storedFile.replace(/\.zim\w?\w?$/i, '')) ? params.fileVersion : params.storedFile;
// setLocalArchiveFromArchiveList(params.storedFile);
// }
// Warn user that the file they wanted is no longer available
var message = '<p>We could not find the archive <b>' + lastSelectedArchive + '</b>!</p><p>Please select its location...</p>';
if (params.webkitdirectory && !window.fs || typeof Windows !== 'undefined' && typeof Windows.Storage !== 'undefined') {
message += '<p><i>Note:</i> If you drag-drop ' + (window.showOpenFilePicker ? 'a <b>split</b>' : 'an') + ' archive into this app, then it will have to be dragged again each time you launch the app. Try ';
message += typeof Windows !== 'undefined' ? 'double-clicking on the archive instead, or ' : '';
message += 'selecting it using the controls on this page.</p>';
}
if (document.getElementById('configuration').style.display === 'none') {
document.getElementById('btnConfigure').click();
}
uiUtil.systemAlert(message).then(function () {
displayFileSelect();
});
}
}
usage.style.display = 'none';
@ -2886,64 +2988,14 @@ function setLocalArchiveFromArchiveList (archive) {
openCurrentArchive.style.display = 'inline';
return;
} else if (params.pickedFolder.kind === 'directory') {
return processNativeDirHandle(params.pickedFolder, function (fileHandles) {
var fileHandle;
var fileset = [];
if (fileHandles) {
for (var i = 0; i < fileHandles.length; i++) {
if (fileHandles[i].name == archive) {
fileHandle = fileHandles[i];
break;
}
}
if (fileHandle) {
// Deal with split archives
if (/\.zim\w\w$/i.test(fileHandle.name)) {
var genericFileName = fileHandle.name.replace(/(\.zim)\w\w$/i, '$1');
var testFileName = new RegExp(genericFileName + '\\w\\w$');
for (i = 0; i < fileHandles.length; i++) {
if (testFileName.test(fileHandles[i].name)) {
// This gets a JS File object from a file handle
fileset.push(fileHandles[i].getFile().then(function (file) {
return file;
}));
}
}
} else {
// Deal with single unslpit archive
fileset.push(fileHandle.getFile().then(function (file) {
return file;
}));
}
if (fileset.length) {
// Wait for all getFile Promises to resolve
Promise.all(fileset).then(function (resolvedFiles) {
setLocalArchiveFromFileList(resolvedFiles);
});
} else {
console.error('There was an error reading the picked file(s)!');
}
} else {
console.error('The picked file could not be found in the selected folder!');
var archiveList = [];
for (i = 0; i < fileHandles.length; i++) {
if (/\.zim(aa)?$/i.test(fileHandles[i].name)) {
archiveList.push(fileHandles[i].name);
}
}
populateDropDownListOfArchives(archiveList);
document.getElementById('btnConfigure').click();
}
} else {
console.log('There was an error obtaining the file handle(s).');
}
return processNativeDirHandle(params.pickedFolder, function (files) {
processDirectoryOfFiles(files, archive);
});
}
openCurrentArchive.style.display = 'none';
}).catch(function () {
openCurrentArchive.style.display = 'inline';
});
return;
} else if (window.fs) {
if (params.pickedFile) {
setLocalArchiveFromFileList([params.pickedFile]);
@ -2957,31 +3009,41 @@ function setLocalArchiveFromArchiveList (archive) {
} else {
setLocalArchiveFromFileList(selectedFiles);
}
}).catch(function (err) {
console.error(err);
});
} else {
uiUtil.systemAlert('We could not find the location of the file ' + archive +
'. This can happen if you dragged and dropped a file into the app. Please use the file or folder pickers instead.');
if (document.getElementById('configuration').style.display == 'none')
if (document.getElementById('configuration').style.display === 'none') {
document.getElementById('btnConfigure').click();
}
displayFileSelect();
}
}
return;
} else if (params.pickedFolder && params.webkitdirectory && archiveDirLegacy.files.length) {
processDirectoryOfFiles(archiveDirLegacy.files, archive);
return;
} else { // Check if user previously picked a specific file rather than a folder
if (params.pickedFile && typeof MSApp !== 'undefined') {
try {
selectedStorage = MSApp.createFileFromStorageFile(params.pickedFile);
setLocalArchiveFromFileList([selectedStorage]);
return;
} catch (err) {
// Probably user has moved or deleted the previously selected file
uiUtil.systemAlert('The previously picked archive can no longer be found!');
console.error('Picked archive not found: ' + err);
}
return;
} else if (params.pickedFile && typeof window.showOpenFilePicker === 'function') {
// Native FS API for single file
setLocalArchiveFromFileList([params.pickedFile]);
return;
} else if (params.pickedFile && params.webkitdirectory) {
// Webkitdirectory API for single file
setLocalArchiveFromFileList(archiveFilesLegacy.files);
return;
}
}
// There was no picked file or folder, so we'll try setting the default localStorage
@ -3056,6 +3118,67 @@ function setLocalArchiveFromArchiveList (archive) {
}
}
function processDirectoryOfFiles (fileHandles, archive) {
var fileHandle;
var fileset = [];
if (fileHandles) {
for (var i = 0; i < fileHandles.length; i++) {
if (fileHandles[i].name === archive) {
fileHandle = fileHandles[i];
break;
}
}
if (fileHandle) {
// Deal with split archives
if (/\.zim\w\w$/i.test(fileHandle.name)) {
var genericFileName = fileHandle.name.replace(/(\.zim)\w\w$/i, '$1');
var testFileName = new RegExp(genericFileName + '\\w\\w$');
for (i = 0; i < fileHandles.length; i++) {
if (testFileName.test(fileHandles[i].name)) {
if (fileHandles[i].getFile) {
// This gets a JS File object from a file handle
fileset.push(fileHandles[i].getFile().then(function (file) {
return file;
}));
} else {
fileset.push(fileHandles[i]);
}
}
}
} else {
// Deal with single unslpit archive
if (fileHandle.getFile) {
fileset.push(fileHandle.getFile().then(function (file) {
return file;
}));
} else {
fileset.push(fileHandle);
}
}
if (fileset.length) {
// Wait for all getFile Promises to resolve
Promise.all(fileset).then(function (resolvedFiles) {
setLocalArchiveFromFileList(resolvedFiles);
});
} else {
console.error('There was an error reading the picked file(s)!');
}
} else {
console.error('The picked file could not be found in the selected folder!');
var archiveList = [];
for (i = 0; i < fileHandles.length; i++) {
if (/\.zim(aa)?$/i.test(fileHandles[i].name)) {
archiveList.push(fileHandles[i].name);
}
}
populateDropDownListOfArchives(archiveList);
document.getElementById('btnConfigure').click();
}
} else {
console.log('There was an error obtaining the file handle(s).');
}
}
if (!params.disableDragAndDrop) {
// Define globalDropZone (universal drop area) and configDropZone (highlighting area on Config page)
var globalDropZone = document.getElementById('search-article');
@ -3082,8 +3205,6 @@ function displayFileSelect () {
UWPInstructions.style.display = 'block';
}
document.getElementById('rescanStorage').style.display = 'none';
// This handles use of the file picker
document.getElementById('archiveFiles').addEventListener('change', setLocalArchiveFromFileSelect);
}
function handleGlobalDragover (e) {
@ -3102,7 +3223,6 @@ function handleIframeDragover (e) {
function handleIframeDrop (e) {
e.stopPropagation();
e.preventDefault();
return;
}
function handleFileDrop (packet) {
@ -3110,6 +3230,7 @@ function handleFileDrop (packet) {
packet.preventDefault();
configDropZone.style.border = '';
var items = packet.dataTransfer.items;
// When dropping multiple files (e.g. a split archive), we cannot use the File System Access API
if (items && items.length === 1 && items[0].kind === 'file' && typeof items[0].getAsFileSystemHandle !== 'undefined') {
items[0].getAsFileSystemHandle().then(function (handle) {
if (handle.kind === 'file') {
@ -3123,10 +3244,12 @@ function handleFileDrop (packet) {
document.getElementById('openLocalFiles').style.display = 'none';
document.getElementById('rescanStorage').style.display = 'block';
document.getElementById('usage').style.display = 'none';
// We have to void the previous picked folder, because dragged files don't have a folder
// This also prevents a file-not-found alert to the user when picking a new directory
params.pickedFolder = null;
settingsStore.setItem('pickedFolder', '', Infinity);
params.rescan = false;
setLocalArchiveFromFileList(files);
// This clears the display of any previously picked archive in the file selector
document.getElementById('archiveFilesLegacy').value = '';
}
}
@ -3267,9 +3390,11 @@ function processNativeDirHandle (dirHandle, callback) {
if (callback) archiveList.push(entry);
// Hide all parts of split file except first in UI
else if (/\.zim(aa)?$/.test(entry.name)) archiveList.push(entry.name);
if (!params.pickedFolder.path) entry.getFile().then(function (file) {
params.pickedFolder.path = file.path;
})
if (!params.pickedFolder.path) {
entry.getFile().then(function (file) {
params.pickedFolder.path = file.path;
});
}
}
iterateAsyncDirEntryArray();
} else {
@ -3324,7 +3449,15 @@ function scanUWPFolderforArchives (folder) {
params.pickedFolder = folder;
// Query the folder.
var query = folder.createFileQuery();
query.getFilesAsync().done(processFilesArray);
query.getFilesAsync().done(function (files) {
processFilesArray(files, function (resolvedFiles) {
// If there is only one file in the folder, we should load it
if ((resolvedFiles.length === 1 || params.storedFile) && !params.rescan) {
var fileToLoad = params.storedFile || resolvedFiles[0].name;
setLocalArchiveFromArchiveList(fileToLoad);
}
});
});
} else {
// The picker was dismissed with no selected file
console.log('User closed folder picker without picking a file');
@ -3335,19 +3468,15 @@ function processFilesArray (files, callback) {
// Display file list
var archiveDisplay = document.getElementById('chooseArchiveFromLocalStorage');
if (files) {
var filteredFiles = [];
var archiveList = [];
files.forEach(function (file) {
if (/\.zim(aa)?$/i.test(file.fileType) || /\.zim(aa)?$/i.test(file)) {
if (/\.zim(aa)?$/i.test(file.fileType) || /\.zim(aa)?$/i.test(file) || /\.zim(aa)?$/i.test(file.name)) {
archiveList.push(file.name || file);
}
if (/\.zim(\w\w)?$/i.test(file.fileType) || /\.zim(\w\w)?$/i.test(file)) {
filteredFiles.push(file);
}
});
if (archiveList.length) {
document.getElementById('noZIMFound').style.display = 'none';
populateDropDownListOfArchives(archiveList);
populateDropDownListOfArchives(archiveList, true);
if (callback) callback(files, archiveList);
return;
}
@ -3362,8 +3491,9 @@ function processFilesArray (files, callback) {
function setLocalArchiveFromFileList (files) {
if (!files.length) {
if (document.getElementById('configuration').style.display == 'none')
document.getElementById('btnConfigure').click();
if (document.getElementById('configuration').style.display == 'none') {
document.getElementById('btnConfigure').click();
}
displayFileSelect();
return;
}
@ -3381,6 +3511,10 @@ function setLocalArchiveFromFileList (files) {
if (typeof window.fs !== 'undefined' && files[i].path) {
files[i].readMode = 'electron';
console.log('File path is: ' + files[i].path);
if (files.length === 1 || params.firstFileIndex) {
params.pickedFile = files[i].path;
settingsStore.setItem('pickedFile', params.pickedFile, Infinity);
}
}
}
// Check that user hasn't picked just part of split ZIM
@ -3436,8 +3570,22 @@ function setLocalArchiveFromFileList (files) {
}
// The archive is set : go back to home page to start searching
params.storedFile = archive._file._files[0].name;
params.storedFilePath = archive._file._files[0].path ? archive._file._files[0].path : '';
settingsStore.setItem('lastSelectedArchive', params.storedFile, Infinity);
settingsStore.setItem('lastSelectedArchivePath', archive._file._files[0].path ? archive._file._files[0].path : '', Infinity);
settingsStore.setItem('lastSelectedArchivePath', params.storedFilePath, Infinity);
// If we have dragged and dropped files into an Electron app, we should have access to the path, so we should store it
if (params.storedFilePath) {
params.pickedFolder = null;
params.pickedFile = params.storedFilePath;
settingsStore.setItem('pickedFolder', '', Infinity);
settingsStore.setItem('pickedFile', params.pickedFile, Infinity);
populateDropDownListOfArchives([params.storedFile], true);
settingsStore.setItem('listOfArchives', encodeURI(params.storedFile), Infinity);
// We have to remove the file handle to prevent it from launching next time
cache.idxDB('delete', 'pickedFSHandle', function () {
console.debug('File handle deleted');
});
}
var reloadLink = document.getElementById('reloadPackagedArchive');
if (reloadLink) {
if (params.packagedFile != params.storedFile) {
@ -3512,6 +3660,8 @@ function loadPackagedArchive () {
params.storedFile = params.packagedFile;
setLocalArchiveFromFileList(fileObjects);
populateDropDownListOfArchives(fileNames, true);
}).catch(function (err) {
console.error(err);
});
// createFakeFileObjectNode(params.packagedFile, params.archivePath + '/' + params.packagedFile, processFakeFile);
}
@ -3522,7 +3672,14 @@ function loadPackagedArchive () {
* Sets the localArchive from the File selects populated by user
*/
function setLocalArchiveFromFileSelect () {
setLocalArchiveFromFileList(document.getElementById('archiveFilesLegacy').files);
setLocalArchiveFromFileList(archiveFilesLegacy.files);
params.rescan = false;
}
/**
* Sets the localArchive from the directory selected by user
*/
function setLocalArchiveFromDirSelect () {
setLocalArchiveFromFileList(archiveDirLegacy.files);
params.rescan = false;
}
@ -3575,6 +3732,7 @@ function readNodeDirectoryAndCreateNodeFileObjects (folder, file) {
var selectedFileSet = [], selectedFileNamesSet = [];
var count = 0;
var fileHandle = typeof file === 'string' ? file : file[0];
// Electron may need to handle the path differently
if (folder === params.archivePath && /^file:/i.test(window.location.protocol)) {
folder = decodeURIComponent(window.location.href.replace(/www\/[^/?#]+(?:[?#].*)?$/, '') + folder);
}

View File

@ -23,6 +23,8 @@
'use strict';
/* global Windows, launchArguments */
// Set a global error handler to prevent app crashes
window.onerror = function (msg, url, line, col, error) {
console.error('Error caught in app [' + url + ':' + line + ']:\n' + msg, error);
@ -45,57 +47,58 @@ var params = {};
/**
* A global state object
*
*
* @type Object
*/
var appstate = {};
/******** UPDATE VERSION IN service-worker.js TO MATCH VERSION AND CHECK PWASERVER BELOW!!!!!!! *******/
params['appVersion'] = "2.5.4"; //DEV: Manually update this version when there is a new release: it is compared to the Settings Store "appVersion" in order to show first-time info, and the cookie is updated in app.js
/******* UPDATE THIS ^^^^^^ IN service worker AND PWA-SERVER BELOW !! ********************/
params['packagedFile'] = getSetting('packagedFile') || "wikipedia_en_100_mini_2023-06.zim"; //For packaged Kiwix JS (e.g. with Wikivoyage file), set this to the filename (for split files, give the first chunk *.zimaa) and place file(s) in default storage
params['archivePath'] = "archives"; //The directory containing the packaged archive(s) (relative to app's root directory)
params['fileVersion'] = getSetting('fileVersion') || "wikipedia_en_100_mini_2023-06.zim (2 June 2023)"; //This will be displayed in the app - optionally include date of ZIM file
// ******** UPDATE VERSION IN service-worker.js TO MATCH VERSION AND CHECK PWASERVER BELOW!!!!!!! *******
params['appVersion'] = '2.5.4'; // DEV: Manually update this version when there is a new release: it is compared to the Settings Store "appVersion" in order to show first-time info, and the cookie is updated in app.js
// ******* UPDATE THIS ^^^^^^ IN service worker AND PWA-SERVER BELOW !! ********************
params['packagedFile'] = getSetting('packagedFile') || 'wikipedia_en_100_mini_2023-06.zim'; // For packaged Kiwix JS (e.g. with Wikivoyage file), set this to the filename (for split files, give the first chunk *.zimaa) and place file(s) in default storage
params['archivePath'] = 'archives'; // The directory containing the packaged archive(s) (relative to app's root directory)
params['fileVersion'] = getSetting('fileVersion') || 'wikipedia_en_100_mini_2023-06.zim (2 June 2023)'; // This will be displayed in the app - optionally include date of ZIM file
// List of known start pages cached in the FS:
params['cachedStartPages'] = {
'wikipedia_en_medicine-app_maxi': 'A/Wikipedia:WikiProject_Medicine/Open_Textbook_of_Medicine2',
'wikipedia_en_medicine_maxi': 'A/Wikipedia:WikiProject_Medicine/Open_Textbook_of_Medicine2',
wikipedia_en_medicine_maxi: 'A/Wikipedia:WikiProject_Medicine/Open_Textbook_of_Medicine2',
// 'mdwiki_en_all_maxi': 'A/Wikipedia:WikiProject_Medicine/Open_Textbook_of_Medicine2',
'wikivoyage_en_all_maxi': 'A/Main_Page'
wikivoyage_en_all_maxi: 'A/Main_Page'
};
params['kiwixDownloadLink'] = "https://download.kiwix.org/zim/"; //Include final slash
params['kiwixHiddenDownloadLink'] = "https://master.download.kiwix.org/zim/";
/******* DEV: ENSURE SERVERS BELOW ARE LISTED IN package.appxmanifest ************/
params['PWAServer'] = "https://pwa.kiwix.org/"; // Production server
params['kiwixDownloadLink'] = 'https://download.kiwix.org/zim/'; // Include final slash
params['kiwixHiddenDownloadLink'] = 'https://master.download.kiwix.org/zim/';
/** ***** DEV: ENSURE SERVERS BELOW ARE LISTED IN package.appxmanifest ************/
params['PWAServer'] = 'https://pwa.kiwix.org/'; // Production server
// params['PWAServer'] = "https://kiwix.github.io/kiwix-js-windows/dist/"; // Test server
params['storeType'] = getBestAvailableStorageAPI();
params['appType'] = getAppType();
params['keyPrefix'] = 'kiwixjs-'; // Prefix to use for localStorage keys
// Maximum number of article titles to return (range is 5 - 100, default 30)
params['maxSearchResultsSize'] = ~~(getSetting('maxSearchResultsSize') || 30);
params['relativeFontSize'] = ~~(getSetting('relativeFontSize') || 100); //Sets the initial font size for articles (as a percentage) - user can adjust using zoom buttons
params['relativeUIFontSize'] = ~~(getSetting('relativeUIFontSize') || 100); //Sets the initial font size for UI (as a percentage) - user can adjust using slider in Config
params['cssSource'] = getSetting('cssSource') || "auto"; //Set default to "auto", "desktop" or "mobile"
params['removePageMaxWidth'] = getSetting('removePageMaxWidth') != null ? getSetting('removePageMaxWidth') : "auto"; //Set default for removing max-width restriction on Wikimedia pages ("auto" = removed in desktop, not in mobile; true = always remove; false = never remove)
params['displayHiddenBlockElements'] = getSetting('displayHiddenBlockElements') !== null ? getSetting('displayHiddenBlockElements') : "auto"; //Set default for displaying hidden block elements ("auto" = displayed in Wikimedia archives in mobile style)
params['openAllSections'] = getSetting('openAllSections') != null ? getSetting('openAllSections') : true; //Set default for opening all sections in ZIMs that have collapsible sections and headings ("auto" = let CSS decide according to screen width; true = always open until clicked by user; false = always closed until clicked by user)
params['cssCache'] = getSetting('cssCache') != null ? getSetting('cssCache') : true; //Set default to true to use cached CSS, false to use Zim only
params['cssTheme'] = getSetting('cssTheme') || 'light'; //Set default to 'auto', 'light', 'dark' or 'invert' to use respective themes for articles
params['cssUITheme'] = getSetting('cssUITheme') || 'light'; //Set default to 'auto', 'light' or 'dark' to use respective themes for UI'
params['relativeFontSize'] = ~~(getSetting('relativeFontSize') || 100); // Sets the initial font size for articles (as a percentage) - user can adjust using zoom buttons
params['relativeUIFontSize'] = ~~(getSetting('relativeUIFontSize') || 100); // Sets the initial font size for UI (as a percentage) - user can adjust using slider in Config
params['cssSource'] = getSetting('cssSource') || 'auto'; // Set default to "auto", "desktop" or "mobile"
params['removePageMaxWidth'] = getSetting('removePageMaxWidth') != null ? getSetting('removePageMaxWidth') : 'auto'; // Set default for removing max-width restriction on Wikimedia pages ("auto" = removed in desktop, not in mobile; true = always remove; false = never remove)
params['displayHiddenBlockElements'] = getSetting('displayHiddenBlockElements') !== null ? getSetting('displayHiddenBlockElements') : 'auto'; // Set default for displaying hidden block elements ("auto" = displayed in Wikimedia archives in mobile style)
params['openAllSections'] = getSetting('openAllSections') != null ? getSetting('openAllSections') : true; // Set default for opening all sections in ZIMs that have collapsible sections and headings ("auto" = let CSS decide according to screen width; true = always open until clicked by user; false = always closed until clicked by user)
params['cssCache'] = getSetting('cssCache') != null ? getSetting('cssCache') : true; // Set default to true to use cached CSS, false to use Zim only
params['cssTheme'] = getSetting('cssTheme') || 'light'; // Set default to 'auto', 'light', 'dark' or 'invert' to use respective themes for articles
params['cssUITheme'] = getSetting('cssUITheme') || 'light'; // Set default to 'auto', 'light' or 'dark' to use respective themes for UI'
params['resetDisplayOnResize'] = getSetting('resetDisplayOnResize') == true; // Default for the display reset feature that fixes bugs with secondary displays
params['imageDisplay'] = getSetting('imageDisplay') != null ? getSetting('imageDisplay') : true; //Set default to display images from Zim
params['manipulateImages'] = getSetting('manipulateImages') != null ? getSetting('manipulateImages') : true; //Makes dataURIs by default instead of BLOB URIs for images
params['linkToWikimediaImageFile'] = getSetting('linkToWikimediaImageFile') == true; //Links images to Wikimedia online version if ZIM archive is a Wikipedia archive
params['hideToolbars'] = getSetting('hideToolbars') != null ? getSetting('hideToolbars') : true; //Set default to true (hides both), 'top' (hides top only), or false (no hiding)
params['rememberLastPage'] = getSetting('rememberLastPage') != null ? getSetting('rememberLastPage') : true; //Set default option to remember the last visited page between sessions
params['imageDisplay'] = getSetting('imageDisplay') != null ? getSetting('imageDisplay') : true; // Set default to display images from Zim
params['manipulateImages'] = getSetting('manipulateImages') != null ? getSetting('manipulateImages') : true; // Makes dataURIs by default instead of BLOB URIs for images
params['linkToWikimediaImageFile'] = getSetting('linkToWikimediaImageFile') == true; // Links images to Wikimedia online version if ZIM archive is a Wikipedia archive
params['hideToolbars'] = getSetting('hideToolbars') != null ? getSetting('hideToolbars') : true; // Set default to true (hides both), 'top' (hides top only), or false (no hiding)
params['rememberLastPage'] = getSetting('rememberLastPage') != null ? getSetting('rememberLastPage') : true; // Set default option to remember the last visited page between sessions
params['assetsCache'] = getSetting('assetsCache') != null ? getSetting('assetsCache') : true; // Whether to use cache by default or not
params['appCache'] = getSetting('appCache') !== false; // Will be true by default unless explicitly set to false
params['useMathJax'] = getSetting('useMathJax') != null ? getSetting('useMathJax') : true; //Set default to true to display math formulae with MathJax, false to use fallback SVG images only
//params['showFileSelectors'] = getCookie('showFileSelectors') != null ? getCookie('showFileSelectors') : false; //Set to true to display hidden file selectors in packaged apps
params['showFileSelectors'] = true; //False will cause file selectors to be hidden on each load of the app (by ignoring cookie)
params['useMathJax'] = getSetting('useMathJax') != null ? getSetting('useMathJax') : true; // Set default to true to display math formulae with MathJax, false to use fallback SVG images only
// params['showFileSelectors'] = getCookie('showFileSelectors') != null ? getCookie('showFileSelectors') : false; //Set to true to display hidden file selectors in packaged apps
params['showFileSelectors'] = true; // False will cause file selectors to be hidden on each load of the app (by ignoring cookie)
params['hideActiveContentWarning'] = getSetting('hideActiveContentWarning') != null ? getSetting('hideActiveContentWarning') : false;
params['allowHTMLExtraction'] = getSetting('allowHTMLExtraction') == true;
params['alphaChar'] = getSetting('alphaChar') || 'A'; //Set default start of alphabet string (used by the Archive Index)
params['omegaChar'] = getSetting('omegaChar') || 'Z'; //Set default end of alphabet string
params['alphaChar'] = getSetting('alphaChar') || 'A'; // Set default start of alphabet string (used by the Archive Index)
params['omegaChar'] = getSetting('omegaChar') || 'Z'; // Set default end of alphabet string
params['contentInjectionMode'] = getSetting('contentInjectionMode') || ((navigator.serviceWorker && !window.nw) ? 'serviceworker' : 'jquery'); // Deafault to SW mode if the browser supports it
params['allowInternetAccess'] = getSetting('allowInternetAccess'); // Access disabled unless user specifically asked for it: NB allow this value to be null as we use it later
params['openExternalLinksInNewTabs'] = getSetting('openExternalLinksInNewTabs') !== null ? getSetting('openExternalLinksInNewTabs') : true; // Parameter to turn on/off opening external links in new tab
@ -104,7 +107,9 @@ params['windowOpener'] = getSetting('windowOpener'); // 'tab|window|false' A set
params['rightClickType'] = getSetting('rightClickType'); // 'single|double|false' A setting that determines whether a single or double right-click is used to open a new window/tab
params['navButtonsPos'] = getSetting('navButtonsPos') || 'bottom'; // 'top|bottom' A setting that determines where the back-forward nav buttons appear
//Do not touch these values unless you know what they do! Some are global variables, some are set programmatically
// Do not touch these values unless you know what they do! Some are global variables, some are set programmatically
params['cacheAPI'] = 'kiwixjs-assetsCache'; // Set the global Cache API database or cache name here, and synchronize with Service Worker
params['cacheIDB'] = 'kiwix-assetsCache'; // Set the global IndexedDB database here (Slightly different name to disambiguate)
params['imageDisplayMode'] = params.imageDisplay ? 'progressive' : 'manual';
params['storedFile'] = getSetting('lastSelectedArchive');
params.storedFile = launchArguments ? launchArguments.files[0].name : params.storedFile || params['packagedFile'] || '';
@ -114,16 +119,16 @@ params['storedFilePath'] = getSetting('lastSelectedArchivePath');
params.storedFilePath = params.storedFilePath ? decodeURIComponent(params.storedFilePath) : params.archivePath + '/' + params.packagedFile;
params.storedFilePath = launchArguments ? launchArguments.files[0].path || '' : params.storedFilePath;
params.originalPackagedFile = params.packagedFile;
params['localStorage'] = "";
params['pickedFile'] = launchArguments ? launchArguments.files[0] : "";
params['pickedFolder'] = "";
params['localStorage'] = '';
params['pickedFile'] = launchArguments ? launchArguments.files[0] : '';
params['pickedFolder'] = '';
params['themeChanged'] = params['themeChanged'] || false;
params['printIntercept'] = false;
params['printInterception'] = false;
params['appIsLaunching'] = true; // Allows some routines to tell if the app has just been launched
params['PWAInstalled'] = window.matchMedia('(display-mode: standalone)').matches; // Because user may reset the app, we have to test for standalone mode
params['falFileToken'] = "zimfile"; // UWP support
params['falFolderToken'] = "zimfilestore"; // UWP support
params['falFileToken'] = 'zimfile'; // UWP support
params['falFolderToken'] = 'zimfilestore'; // UWP support
params.pagesLoaded = 0; // Page counter used to show PWA Install Prompt only after user has played with the app for a while
params.localUWPSettings = /UWP/.test(params.appType) ? Windows.Storage.ApplicationData.current.localSettings.values : null;
appstate['target'] = 'iframe'; // The target for article loads (this should always be 'iframe' initially, and will only be changed as a result of user action)
@ -218,24 +223,24 @@ document.getElementById('manipulateImagesCheck').checked = params.manipulateImag
document.getElementById('removePageMaxWidthCheck').checked = params.removePageMaxWidth === true; // Will be false if false or auto
document.getElementById('removePageMaxWidthCheck').indeterminate = params.removePageMaxWidth === 'auto';
document.getElementById('removePageMaxWidthCheck').readOnly = params.removePageMaxWidth === 'auto';
document.getElementById('pageMaxWidthState').textContent = (params.removePageMaxWidth === "auto" ? "auto" : params.removePageMaxWidth ? "always" : "never");
document.getElementById('pageMaxWidthState').textContent = (params.removePageMaxWidth === 'auto' ? 'auto' : params.removePageMaxWidth ? 'always' : 'never');
document.getElementById('displayHiddenBlockElementsCheck').checked = params.displayHiddenBlockElements === true;
document.getElementById('displayHiddenBlockElementsCheck').indeterminate = params.displayHiddenBlockElements === 'auto';
document.getElementById('displayHiddenBlockElementsCheck').readOnly = params.displayHiddenBlockElements === 'auto';
document.getElementById('displayHiddenElementsState').textContent = (params.displayHiddenBlockElements === "auto" ? "auto" : params.displayHiddenBlockElements ? "always" : "never");
document.getElementById('displayHiddenElementsState').textContent = (params.displayHiddenBlockElements === 'auto' ? 'auto' : params.displayHiddenBlockElements ? 'always' : 'never');
document.getElementById('openAllSectionsCheck').checked = params.openAllSections;
document.getElementById('linkToWikimediaImageFileCheck').checked = params.linkToWikimediaImageFile;
document.getElementById('useOSMCheck').checked = /openstreetmap/.test(params.mapsURI);
document.getElementById('cssUIDarkThemeCheck').checked = params.cssUITheme == "dark"; // Will be true, or false if light or auto
document.getElementById('cssUIDarkThemeCheck').indeterminate = params.cssUITheme == "auto";
document.getElementById('cssUIDarkThemeCheck').readOnly = params.cssUITheme == "auto";
document.getElementById('cssUIDarkThemeCheck').checked = params.cssUITheme == 'dark'; // Will be true, or false if light or auto
document.getElementById('cssUIDarkThemeCheck').indeterminate = params.cssUITheme == 'auto';
document.getElementById('cssUIDarkThemeCheck').readOnly = params.cssUITheme == 'auto';
document.getElementById('cssUIDarkThemeState').innerHTML = params.cssUITheme;
document.getElementById('cssWikiDarkThemeCheck').checked = /dark|invert/.test(params.cssTheme);
document.getElementById('cssWikiDarkThemeCheck').indeterminate = params.cssTheme == "auto";
document.getElementById('cssWikiDarkThemeCheck').readOnly = params.cssTheme == "auto";
document.getElementById('cssWikiDarkThemeCheck').indeterminate = params.cssTheme == 'auto';
document.getElementById('cssWikiDarkThemeCheck').readOnly = params.cssTheme == 'auto';
document.getElementById('cssWikiDarkThemeState').innerHTML = params.cssTheme;
document.getElementById('darkInvert').style.display = /dark|invert|darkReader/i.test(params.cssTheme) ? "inline" : "none";
document.getElementById('darkDarkReader').style.display = params.contentInjectionMode === 'serviceworker' && /dark|invert|darkReader/i.test(params.cssTheme) ? "inline" : "none";
document.getElementById('darkInvert').style.display = /dark|invert|darkReader/i.test(params.cssTheme) ? 'inline' : 'none';
document.getElementById('darkDarkReader').style.display = params.contentInjectionMode === 'serviceworker' && /dark|invert|darkReader/i.test(params.cssTheme) ? 'inline' : 'none';
document.getElementById('cssWikiDarkThemeInvertCheck').checked = params.cssTheme == 'invert';
document.getElementById('cssWikiDarkThemeDarkReaderCheck').checked = params.cssTheme == 'darkReader';
document.getElementById('resetDisplayOnResizeCheck').checked = params.resetDisplayOnResize;
@ -248,17 +253,17 @@ document.getElementById('omegaCharTxt').value = params.omegaChar;
document.getElementById('titleSearchRange').value = params.maxSearchResultsSize;
document.getElementById('titleSearchRangeVal').innerHTML = params.maxSearchResultsSize;
document.getElementById('hideToolbarsCheck').checked = params.hideToolbars === true; // Will be false if false or 'top'
document.getElementById('hideToolbarsCheck').indeterminate = params.hideToolbars === "top";
document.getElementById('hideToolbarsCheck').readOnly = params.hideToolbars === "top";
document.getElementById('hideToolbarsState').innerHTML = (params.hideToolbars === "top" ? "top" : params.hideToolbars ? "both" : "never");
document.getElementById('hideToolbarsCheck').indeterminate = params.hideToolbars === 'top';
document.getElementById('hideToolbarsCheck').readOnly = params.hideToolbars === 'top';
document.getElementById('hideToolbarsState').innerHTML = (params.hideToolbars === 'top' ? 'top' : params.hideToolbars ? 'both' : 'never');
document.getElementById('openExternalLinksInNewTabsCheck').checked = params.openExternalLinksInNewTabs;
document.getElementById('disableDragAndDropCheck').checked = params.disableDragAndDrop;
document.getElementById('debugLibzimASMDrop').value = params.debugLibzimASM || '';
if (params.windowOpener === null) { // Setting has never been activated, so determine a sensible default
params.windowOpener = /UWP/.test(params.appType) && params.contentInjectionMode === 'jquery' ? false :
params.windowOpener = /UWP/.test(params.appType) && params.contentInjectionMode === 'jquery' ? false :
/iOS/.test(params.appType) ? false :
('MSBlobBuilder' in window || params.PWAInstalled) ? 'window' : // IE11/Edge Legacy/UWP work best in window mode, not in tab mode, as does installed PWA!
/PWA/.test(params.appType) ? 'tab' : false;
/PWA/.test(params.appType) ? 'tab' : false;
}
if (params.windowOpener) params.allowHTMLExtraction = false;
document.getElementById('allowHTMLExtractionCheck').checked = params.allowHTMLExtraction;
@ -302,7 +307,7 @@ if (params.packagedFileStub && params.appVersion !== getSetting('appVersion') &&
deleteSetting('listOfArchives');
params.localStorageUpgradeNeeded = true;
}
if (params.storedFile && typeof Windows !== 'undefined' && typeof Windows.Storage !== 'undefined') { //UWP
if (params.storedFile && typeof Windows !== 'undefined' && typeof Windows.Storage !== 'undefined') { // UWP
var futureAccessList = Windows.Storage.AccessCache.StorageApplicationPermissions.futureAccessList;
Windows.ApplicationModel.Package.current.installedLocation.getFolderAsync(params.archivePath).done(function (appFolder) {
params.localStorage = appFolder;
@ -310,13 +315,13 @@ if (params.storedFile && typeof Windows !== 'undefined' && typeof Windows.Storag
futureAccessList.getFolderAsync(params.falFolderToken).done(function (pickedFolder) {
params.pickedFolder = params.localStorageUpgradeNeeded ? params.localStorage : pickedFolder;
}, function (err) {
console.error("The previously picked folder is no longer accessible: " + err.message);
console.error('The previously picked folder is no longer accessible: ' + err.message);
});
}
}, function (err) {
console.error("This app doesn't appear to have access to local storage!");
console.error(new Error("This app doesn't appear to have access to local storage!"));
});
//If we don't already have a picked file (e.g. by launching app with click on a ZIM file), then retrieve it from futureAccessList if possible
// If we don't already have a picked file (e.g. by launching app with click on a ZIM file), then retrieve it from futureAccessList if possible
var listOfArchives = getSetting('listOfArchives');
// But don't get the picked file if we already have access to the folder and the file is in it!
if (listOfArchives && ~listOfArchives.indexOf(params.storedFile) && params.pickedFolder) {
@ -327,7 +332,7 @@ if (params.storedFile && typeof Windows !== 'undefined' && typeof Windows.Storag
futureAccessList.getFileAsync(params.falFileToken).done(function (file) {
if (file.name === params.storedFile) params.pickedFile = file;
}, function (err) {
console.error("The previously picked file is no longer accessible: " + err.message);
console.error('The previously picked file is no longer accessible: ' + err.message);
});
}
}
@ -412,7 +417,7 @@ function getSetting(name) {
// Use localStorage instead
result = localStorage.getItem(params.keyPrefix + name);
}
return result === null || result === "undefined" ? null : result === "true" ? true : result === "false" ? false : result;
return result === null || result === 'undefined' ? null : result === 'true' ? true : result === 'false' ? false : result;
}
function setSetting(name, val) {

View File

@ -1,71 +1,73 @@
/**
* 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 <http://www.gnu.org/licenses/>
*/
/* globals params, caches, assetsCache */
'use strict';
import settingsStore from './settingsStore.js';
import uiUtil from './uiUtil.js';
const CACHEAPI = 'kiwixjs-assetsCache'; // Set the database or cache name here, and synchronize with Service Worker
const CACHEIDB = 'kiwix-assetsCache'; // Slightly different name to disambiguate
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) {
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) {
assetsCache.capability = 'memory';
idxDB('count', function (result) {
if (result !== false) {
assetsCache.capability = 'indexedDB|' + assetsCache.capability;
} else {
console.log("inexedDB is not supported");
console.log('inexedDB is not supported');
}
// Test for Cache API
if('caches' in window && /https?:/i.test(window.location.protocol)) {
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'));
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") {
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('localStorage is not supported');
}
}
console.log('Setting storage type to ' + assetsCache.capability.match(/^[^|]+/)[0]);
@ -78,54 +80,56 @@ function test(callback) {
/**
* Counts the numnber of cached assets
*
*
* @param {Function} callback which will receive an array containing [cacheType, cacheCount]
*/
function count(callback) {
test(function(result) {
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});
});
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;
default:
// User has turned off caching
type = 'none';
description = 'None';
cacheCount = 'null';
}
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});
callback({ type: type, description: description, count: cacheCount });
}
});
// Refresh instructions to Service Worker
@ -133,10 +137,10 @@ function count(callback) {
// 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
action: {
assetsCache: params.assetsCache ? 'enable' : 'disable',
appCache: params.appCache ? 'enable' : 'disable',
checkCache: window.location.href
}
}, [channel.port2]);
}
@ -145,20 +149,20 @@ function count(callback) {
/**
* 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)
* '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
* @param {Function} callback Callback for write transactions only - mandatory for delete and write transactions
*/
function idxDB(keyOrCommand, valueOrCallback, callback) {
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') {
@ -179,11 +183,11 @@ function idxDB(keyOrCommand, valueOrCallback, callback) {
}
return;
}
// Open (or create) the database
var open = indexedDB.open(CACHEIDB, 1);
open.onerror = function(e) {
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') {
@ -191,21 +195,21 @@ function idxDB(keyOrCommand, valueOrCallback, callback) {
}
rtnFn(false);
};
// Create the schema
open.onupgradeneeded = function() {
open.onupgradeneeded = function () {
var db = open.result;
var store = db.createObjectStore(objStore);
};
open.onsuccess = function() {
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 || keyOrCommand === 'clear' ? db.transaction(objStore, "readwrite") : db.transaction(objStore);
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') {
@ -222,20 +226,20 @@ function idxDB(keyOrCommand, valueOrCallback, callback) {
processData = value !== null ? store.put(value, keyOrCommand) : store.get(keyOrCommand);
}
// Call the callback with the result
processData.onsuccess = function(e) {
processData.onsuccess = function (e) {
if (keyOrCommand === 'delete') {
rtnFn(true);
} else {
rtnFn(processData.result);
}
};
processData.onerror = function(e){
processData.onerror = function (e) {
console.error('IndexedDB command failed: ' + processData.error);
rtnFn(false);
};
// Close the db when the transaction is done
tx.oncomplete = function() {
tx.oncomplete = function () {
db.close();
};
};
@ -245,42 +249,42 @@ function idxDB(keyOrCommand, valueOrCallback, callback) {
* 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)
* '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) {
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) {
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) {
caches.open(CACHEAPI).then(function (cache) {
cache.match('../' + keyOrCommand).then(function (response) {
if (!response) {
rtnFn(null);
} else {
response.text().then(function(data) {
response.text().then(function (data) {
rtnFn(data);
});
}
}).catch(function(err) {
}).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) {
caches.open(CACHEAPI).then(function (cache) {
var contentLength;
if (typeof value === 'string') {
var m = encodeURIComponent(value).match(/%[89ABab]/g);
@ -300,9 +304,9 @@ function cacheAPI(keyOrCommand, valueOrCallback, callback, mimetype) {
headers: headers
};
var httpResponse = new Response(value, responseInit);
cache.put('../' + keyOrCommand, httpResponse).then(function() {
cache.put('../' + keyOrCommand, httpResponse).then(function () {
rtnFn(true);
}).catch(function(err) {
}).catch(function (err) {
console.error('Unable to store assets in Cache API!', err);
rtnFn(null);
});
@ -312,20 +316,20 @@ function cacheAPI(keyOrCommand, valueOrCallback, callback, mimetype) {
/**
* 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) {
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) {
setItem(zimFile, content, 'text/html', function (response) {
callback(response);
});
}
@ -333,12 +337,12 @@ function setArticle(zimFile, article, content, callback) {
/**
* 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) {
function getArticle (zimFile, article, callback) {
if (settingsStore.getItem(zimFile) === article) {
getItem(zimFile, callback);
} else {
@ -348,21 +352,21 @@ function getArticle(zimFile, article, callback) {
/**
* 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) {
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
// 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);
@ -374,11 +378,11 @@ function setItem(key, contents, mimetype, callback, isAsset) {
assetsCache.set(key, contents);
}
if (/^indexedDB/.test(assetsCache.capability)) {
idxDB(key, contents, function(result) {
idxDB(key, contents, function (result) {
callback(result);
});
} else if (/^cacheAPI/.test(assetsCache.capability)) {
cacheAPI(key, contents, function(result) {
cacheAPI(key, contents, function (result) {
callback(result);
}, mimetype);
} else {
@ -387,19 +391,19 @@ function setItem(key, contents, mimetype, callback, isAsset) {
}
/**
* Retrieves a ZIM file asset that has been cached with the addItem function
* 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) {
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
// 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
@ -414,11 +418,11 @@ function getItem(key, callback) {
contents = localStorage.getItem(key);
callback(contents);
} else if (/^cacheAPI/.test(assetsCache.capability)) {
cacheAPI(key, function(contents) {
callback(contents);
cacheAPI(key, function (contents) {
callback(contents);
});
} else if (/^indexedDB/.test(assetsCache.capability)) {
idxDB(key, function(contents) {
idxDB(key, function (contents) {
if (typeof contents !== 'undefined') {
// Also store in fast memory cache to prevent repaints
assetsCache.set(key, contents);
@ -427,20 +431,20 @@ function getItem(key, callback) {
});
} 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} 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<String|Uint8Array>} A Promise for the content
*/
function getItemFromCacheOrZIM(selectedArchive, key, dirEntry) {
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(/^[^/]+\//, '');
@ -457,33 +461,33 @@ function getItemFromCacheOrZIM(selectedArchive, key, dirEntry) {
return;
}
// Bypass getting dirEntry if we already have it
var getDirEntry = dirEntry ? Promise.resolve() :
selectedArchive.getDirEntryByPath(title);
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);
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 ('');
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 &&
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;
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');
@ -491,7 +495,7 @@ function getItemFromCacheOrZIM(selectedArchive, key, dirEntry) {
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)) {
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;
@ -516,7 +520,7 @@ function getItemFromCacheOrZIM(selectedArchive, key, dirEntry) {
});
}
}).catch(function (e) {
reject("could not find DirEntry for asset : " + title, e);
reject('could not find DirEntry for asset : ' + title, e);
});
});
});
@ -524,15 +528,15 @@ function getItemFromCacheOrZIM(selectedArchive, key, dirEntry) {
/**
* Clears caches (including cookie) according to the scope represented by the 'items' variable
*
* @param {String} items Either 'lastpages' (last visited pages of various archives) or 'all'
*
* @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) {
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;
@ -552,10 +556,10 @@ function clear(items, callback) {
localStorage.removeItem(key);
}
if (/indexedDB/.test(capability)) {
idxDB('delete', key, function(){});
idxDB('delete', key, function () {});
}
if (/cacheAPI/.test(capability)) {
cacheAPI('delete', key, function(){});
cacheAPI('delete', key, function () {});
}
itemsCount++;
}
@ -566,7 +570,7 @@ function clear(items, callback) {
var result;
if (/^(memory|indexedDB|cacheAPI)/.test(capability)) {
itemsCount += assetsCache.size;
result = "assetsCache";
result = 'assetsCache';
}
// Delete and reinitialize assetsCache
assetsCache = new Map();
@ -578,43 +582,43 @@ function clear(items, callback) {
localStorage.clear();
} else {
for (var i = localStorage.length; i--;) {
var key = localStorage.key(i);
key = localStorage.key(i);
if (/\.zim\w{0,2}/i.test(key)) {
localStorage.removeItem(key);
itemsCount++;
}
}
}
result = result ? result + " and localStorage" : "localStorage";
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) {
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);
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) {
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);
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);
result = result ? result + ' (' + itemsCount + ' items deleted)' : 'no assets to delete';
console.log('cache.clear: ' + result);
if (callback) callback(itemsCount);
}
}
@ -624,7 +628,7 @@ function clear(items, callback) {
* 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
@ -633,13 +637,13 @@ function clear(items, callback) {
* @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) {
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],
titles.push([tagArray[0],
decodeURIComponent(tagArray[1])]);
tagArray = regexpTagsWithAttribute.exec(html);
}
@ -649,8 +653,8 @@ function replaceAssetRefsWithUri(html, tags, attribute, zimFile, selectedArchive
// 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) {
titles.forEach(function (title) {
getItemFromCacheOrZIM(selectedArchive, zimFile + '/' + title[1], function (assetContent) {
assetsCache.busy--;
if (assetContent || assetContent === '') {
var newAssetTag = uiUtil.createNewAssetElement(title[0], attribute, assetContent);
@ -665,61 +669,60 @@ function replaceAssetRefsWithUri(html, tags, attribute, zimFile, selectedArchive
* 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':
function transform (string, filter) {
switch (filter) {
case 'html':
// Filter to remove any BOM (causes quirks mode in browser)
string = string.replace(/^[^<]*/, '');
// 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');
// Filter to open all heading sections
string = string.replace(/(class=["'][^"']*?collapsible-(?:heading|block)(?!\s+open-block))/g,
'$1 open-block');
break;
}
return string;
}
/**
* Provide
*
* @param {Object} fileHandle The file handle that we wish to verify with the Native Filesystem API
* 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<Boolean>} A Promise for a Boolean value indicating whether permission has been granted or not
*/
function verifyPermission(fileHandle, withWrite) {
function verifyPermission (fileHandle, withWrite) {
// if (window.fs) return Promise.resolve(true); // Electron
var opts = withWrite ? { mode: 'readwrite' } : {};
return fileHandle.queryPermission(opts).then(function(permission) {
if (permission === "granted") return true;
return fileHandle.requestPermission(opts).then(function(permission) {
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) {
}).catch(function (error) {
console.warn('Cannot use previously picked file handle programmatically (this is normal) ' + fileHandle.name, error);
});
});
});
}
/**
* Wraps a semaphor in a Promise. A function can signal that it is done by setting a sempahor to true,
/**
* 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.
*
* 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
* @returns {Promise} A promise that resolves when assetsCache[semaphor] is true
*/
function wait(semaphor, value) {
function wait (semaphor, value) {
var p = new Promise(function (resolve) {
setTimeout(function awaitCache() {
setTimeout(function awaitCache () {
if (assetsCache[semaphor]) {
return resolve(value);
}

View File

@ -2,7 +2,6 @@
/* global define, params */
import cache from './cache.js';
import uiUtil from './uiUtil.js';
var regexpCookieKeysToMigrate = new RegExp([
@ -102,7 +101,8 @@ function reset(object) {
// 3. Clear any IndexedDB entries
if (!object || object === 'indexedDB') {
if (/indexedDB/.test(assetsCache.capability)) {
cache.clear('reset');
window.indexedDB.deleteDatabase(params.indexedDB);
console.debug('All IndexedDB entries were deleted...');
}
}

View File

@ -20,7 +20,7 @@
* along with Kiwix (file LICENSE-GPLv3.txt). If not, see <http://www.gnu.org/licenses/>
*/
/* global fs */
/* global fs, params */
'use strict';
@ -273,6 +273,14 @@ function dataURItoUint8Array (dataURI) {
}
}
/**
* Detect whether the browser supports the webkitdirectory attribute on input elements
* @returns {Boolean} True if the webkitdirectory attribute is supported, false otherwise
*/
function webkitdirectorySupported () {
return 'webkitdirectory' in document.createElement('input') && !/iOS|Android/.test(params.appType);
}
/**
* Matches the outermost balanced constructs and their contents
* even if they have nested balanced constructs within them
@ -729,6 +737,7 @@ export default {
leftShift: leftShift,
matchOuter: matchOuter,
matchInner: matchInner,
webkitdirectorySupported: webkitdirectorySupported,
Hilitor: Hilitor,
getClosestForward: getClosestForward,
getClosestBack: getClosestBack,