mirror of
https://github.com/kiwix/kiwix-js-pwa.git
synced 2025-08-03 11:28:21 -04:00
parent
54cb4aa8c9
commit
759d59b04b
@ -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
|
||||
|
8
.github/workflows/build-electron.yml
vendored
8
.github/workflows/build-electron.yml
vendored
@ -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"
|
||||
|
12
package.json
12
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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*.*",
|
||||
|
@ -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"
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
390
www/js/app.js
390
www/js/app.js
@ -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);
|
||||
}
|
||||
|
121
www/js/init.js
121
www/js/init.js
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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...');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user