mirror of
https://github.com/kiwix/kiwix-js.git
synced 2025-09-11 16:18:47 -04:00
This commit is contained in:
parent
0373e53c40
commit
e42995047c
10
.github/workflows/CI.yml
vendored
10
.github/workflows/CI.yml
vendored
@ -24,6 +24,16 @@ jobs:
|
|||||||
# Clone the repo and checkout the commit for which the workflow was triggered
|
# Clone the repo and checkout the commit for which the workflow was triggered
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Test integrity of app parameters
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
# Check that values of assetsCache and appVersion are correctly duplicated
|
||||||
|
chmod +x ./scripts/test_duplicate_values.sh
|
||||||
|
./scripts/test_duplicate_values.sh
|
||||||
|
# Check that PWAServer is correctly set in app.js
|
||||||
|
chmod +x ./scripts/test_pwa_server.sh
|
||||||
|
./scripts/test_pwa_server.sh
|
||||||
|
|
||||||
# Install Node.js LTS
|
# Install Node.js LTS
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
|
12
docker/dockerfile-moz-extension.pwa
Normal file
12
docker/dockerfile-moz-extension.pwa
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
FROM nginx:latest
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
RUN mkdir /usr/share/nginx/html/current/
|
||||||
|
COPY ./docker/index.nginx.html /usr/share/nginx/html/index.html
|
||||||
|
COPY ./manifest.json /usr/share/nginx/html/current/
|
||||||
|
COPY ./service-worker.js /usr/share/nginx/html/current/
|
||||||
|
COPY ./index.html /usr/share/nginx/html/current/
|
||||||
|
COPY ./CHANGELOG.md /usr/share/nginx/html/current/
|
||||||
|
COPY ./LICENSE-GPLv3.txt /usr/share/nginx/html/current/
|
||||||
|
COPY ./www /usr/share/nginx/html/current/www/
|
19
docker/index.nginx.html
Normal file
19
docker/index.nginx.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<!-- This file will be used by dockerfile-moz-extension to provide a redirect from the server's html directory to
|
||||||
|
the implementation. This is a convenience to any user who may visit the root of the server. However, note that
|
||||||
|
this is out of the scope of the Service Worker in the implementation's subdirectory, so this redirect will only
|
||||||
|
work if the client is online. The client will only be able to access the implementation OFFLINE if the browser
|
||||||
|
is pointed to <domain>/current/ or <domain>/current/www/index.html (or equivalent in a versioned directory).
|
||||||
|
The browser extension should always be pointed to the full path of the index.html to be loaded in SW mode.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="refresh" content="0; url=current/www/index.html">
|
||||||
|
<title>Redirection to index.html</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -1,8 +1,12 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="refresh" content="0; url=www/index.html">
|
<meta http-equiv="refresh" content="0; url=www/index.html">
|
||||||
<title>Redirection to index.html</title>
|
<title>Redirection to index.html</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
@ -33,6 +33,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"web_accessible_resources": ["www/index.html"],
|
||||||
|
|
||||||
"background": {
|
"background": {
|
||||||
"scripts": ["webextension/backgroundscript.js"]
|
"scripts": ["webextension/backgroundscript.js"]
|
||||||
},
|
},
|
||||||
|
133
scripts/Publish-Implementation.ps1
Normal file
133
scripts/Publish-Implementation.ps1
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
# This is a utility script which helps developers choose sensible values for publishing the implementation of this app
|
||||||
|
# to GitHub Pages, or to eh docker container. It is useful for testing and developing code in a specific branch. It checks
|
||||||
|
# app.js and service-worker.js for consistency, and checks that the underlying branch of a PR has been checked out
|
||||||
|
# (rather than the PR itself). It then calls the GitHub REST API for dispatching the workflow using the provided values.
|
||||||
|
#
|
||||||
|
# IMPORTANT: Ensure that your personal github token is in your local copy of the '/scripts' directory, saved as 'github_token'.
|
||||||
|
#
|
||||||
|
# You may run this script with commandline switches -machine_name (this could be 'dev'), -target (either 'ghpages' or 'docker'),
|
||||||
|
# the -branch_name, and -dryrun (this will show the changes that would be made if run without the -dryrun switch).
|
||||||
|
# Alternatively, if you do not provide these values, you will be prompted with sensible defaults.
|
||||||
|
|
||||||
|
# Prevents execution with unrecognized switches
|
||||||
|
[CmdletBinding()]
|
||||||
|
param (
|
||||||
|
[string]$machine_name = "",
|
||||||
|
[string]$target = "",
|
||||||
|
[string]$branch_name = "",
|
||||||
|
[switch]$dryrun = $false
|
||||||
|
)
|
||||||
|
|
||||||
|
# Provide parameters
|
||||||
|
$release_uri = 'https://api.github.com/repos/kiwix/kiwix-js/actions/workflows/publish-extension.yaml/dispatches'
|
||||||
|
|
||||||
|
$app_params = Select-String 'appVersion' "$PSScriptRoot\..\www\js\app.js" -List
|
||||||
|
$serviceworker = Select-String 'appVersion' "$PSScriptRoot\..\service-worker.js" -List
|
||||||
|
$suggested_build = ''
|
||||||
|
$app_tag = ''
|
||||||
|
if ($app_params -match 'params\[[''"]appVersion[''"]]\s*=\s*[''"]([^''"]+)') {
|
||||||
|
$app_tag = $matches[1]
|
||||||
|
$suggested_build = 'dev-' + $app_tag
|
||||||
|
} else {
|
||||||
|
"*** WARNING: App version is incorrectly set in app.js.`nPlease correct before continuing.`n"
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
$sw_tag = ''
|
||||||
|
if ($serviceworker -match 'appVersion\s*=\s*[''"]([^''"]+)') {
|
||||||
|
$sw_tag = $matches[1]
|
||||||
|
if ($sw_tag -ne $app_tag) {
|
||||||
|
"*** WARNING: The version in app.js [$app_tag] does not match the version in service-worker.js [$sw_tag]! ***"
|
||||||
|
"Please correct before continuing.`n"
|
||||||
|
exit
|
||||||
|
} else {
|
||||||
|
"`nVersion in app.js: $app_tag"
|
||||||
|
"Version in service-worker.js: $sw_tag`n"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"*** WARNING: App version is incorrectly set in service-worker.js.`nPlease correct before continuing.`n"
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path $PSScriptRoot/github_token -PathType Leaf) {
|
||||||
|
$github_token = Get-Content -Raw "$PSScriptRoot/github_token"
|
||||||
|
} else {
|
||||||
|
Write-Warning "Missing file github_token! Please add it to $PSScriptRoot to run this script.`n"
|
||||||
|
$github_token = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($machine_name -eq "") {
|
||||||
|
if (-Not $dryrun) {
|
||||||
|
$dryrun_check = Read-Host "Is this a dry run? [Y/N]"
|
||||||
|
$dryrun = -Not ( $dryrun_check -imatch 'n' )
|
||||||
|
If ($dryrun) {
|
||||||
|
"[DRYRUN]: Initiating dry run..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""
|
||||||
|
if ($target -eq "") {
|
||||||
|
$target = Read-Host "Which implementation (ghpages or docker) do you wish to update? Enter to accept suggested [ghpages]"
|
||||||
|
}
|
||||||
|
$machine_name = Read-Host "Give the name to use for the implementation, or Enter to accept suggested name [$suggested_build]"
|
||||||
|
""
|
||||||
|
if (-Not $machine_name) {
|
||||||
|
$machine_name = $suggested_build
|
||||||
|
$warning_message = "Please note that ""$app_tag"" will appear in the app as the appVersion. If you want to change that, press Ctrl-C`nand re-run this script entering a build number matching 9.9.9."
|
||||||
|
} elseif ($machine_name -match '^[\d.]+') {
|
||||||
|
$warning_message = "*** Please be aware that you have entered a release tag [$machine_name], and so it will be used as the appVersion of the container`n" +
|
||||||
|
"and will be visible to users. If this is NOT want you want, press Ctrl-C to abort this script, and re-run with the suggested build number."
|
||||||
|
}
|
||||||
|
if ($warning_message) { Write-Warning $warning_message }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-Not $target) {
|
||||||
|
$target = "ghpages"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($branch_name -eq "") {
|
||||||
|
$suggested_branch = &{ git branch --show-current }
|
||||||
|
$branch_name = Read-Host "`nGive the branch name to use of the implementation, or Enter to accept [$suggested_branch]"
|
||||||
|
if (-Not $branch_name) { $branch_name = $suggested_branch }
|
||||||
|
if ($branch_name -imatch '^pr/\d+') {
|
||||||
|
""
|
||||||
|
Write-Warning "You appear to have indicated a PR. Please check out the underlying branch to use this script,`nor else run it again and give the branch name at the prompt.`n"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"`nMachine name set to: $machine_name"
|
||||||
|
"Target set to: $target"
|
||||||
|
"Branch name set to: $branch_name"
|
||||||
|
|
||||||
|
if (-Not $dryrun -and -Not $github_token) {
|
||||||
|
"`nSupply token to continue.`n"
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set up dispatch_params object - for API see https://docs.github.com/en/rest/reference/actions#create-a-workflow-dispatch-event
|
||||||
|
$dispatch_params = @{
|
||||||
|
Uri = $release_uri
|
||||||
|
Method = 'POST'
|
||||||
|
Headers = @{
|
||||||
|
'Authorization' = "token $github_token"
|
||||||
|
'Accept' = 'application/vnd.github.v3+json'
|
||||||
|
}
|
||||||
|
Body = @{
|
||||||
|
'ref' = $branch_name
|
||||||
|
'inputs' = @{
|
||||||
|
'version' = $machine_name
|
||||||
|
'target' = $target
|
||||||
|
}
|
||||||
|
} | ConvertTo-Json
|
||||||
|
ContentType = "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
$dispatch_f = ($dispatch_params | Format-List | Out-String);
|
||||||
|
"`nDispatch parameters:`n$dispatch_f"
|
||||||
|
|
||||||
|
# Post to the release server
|
||||||
|
if (-Not $dryrun) {
|
||||||
|
Invoke-RestMethod @dispatch_params
|
||||||
|
"`nCheck for any error message above. An empty dispatch is normal, and indicates that the command was accepted.`n"
|
||||||
|
} else {
|
||||||
|
"[DRYRUN]: Complete.`n"
|
||||||
|
}
|
@ -58,7 +58,8 @@ else
|
|||||||
sed -i -e "s/$VERSION_TO_REPLACE/$MAJOR_NUMERIC_VERSION/" tmp/manifest.json
|
sed -i -e "s/$VERSION_TO_REPLACE/$MAJOR_NUMERIC_VERSION/" tmp/manifest.json
|
||||||
fi
|
fi
|
||||||
sed -i -e "s/$VERSION_TO_REPLACE/$VERSION/" tmp/manifest.webapp
|
sed -i -e "s/$VERSION_TO_REPLACE/$VERSION/" tmp/manifest.webapp
|
||||||
sed -i -e "s/$VERSION_TO_REPLACE/$VERSION/" tmp/www/index.html
|
sed -i -e "s/$VERSION_TO_REPLACE/$VERSION/" tmp/service-worker.js
|
||||||
|
sed -i -e "s/$VERSION_TO_REPLACE/$VERSION/" tmp/www/js/app.js
|
||||||
|
|
||||||
mkdir -p build
|
mkdir -p build
|
||||||
rm -rf build/*
|
rm -rf build/*
|
||||||
|
30
scripts/test_duplicate_values.sh
Normal file
30
scripts/test_duplicate_values.sh
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# This bash script tests whether app.js and service-worker.js have the same value for appVersion and for ASSETS_CACHE.
|
||||||
|
|
||||||
|
# Find the repo dir (it's the parent of the dir that contains this script)
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||||
|
REPO_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
# Check values in files
|
||||||
|
cd $REPO_DIR
|
||||||
|
SW_VERSION="$(grep 'appVersion\s=' service-worker.js | sed -E "s/[^[:digit:]]+([^\"']+).*/\1/")"
|
||||||
|
APP_VERSION="$(grep 'params\[.appVersion' www/js/app.js | sed -E "s/[^[:digit:]]+([^\"']+).*/\1/")"
|
||||||
|
echo "service-worker.js : $SW_VERSION"
|
||||||
|
echo "app.js : $APP_VERSION"
|
||||||
|
if [ $SW_VERSION == $APP_VERSION ] ; then
|
||||||
|
echo "Both values of 'appVersion' are identical"
|
||||||
|
else
|
||||||
|
echo "ERROR! Please ensure values for 'appVersion' in app.js and service-worker.js are identical!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
SW_ASSETS_CACHE="$( grep 'ASSETS_CACHE\s=' service-worker.js | sed -E "s/[^']+'([^']+).*/\1/")"
|
||||||
|
APP_ASSETS_CACHE="$(grep 'ASSETS_CACHE\s=' www/js/app.js | sed -E "s/[^']+'([^']+).*/\1/")"
|
||||||
|
echo "service-worker.js : $SW_ASSETS_CACHE"
|
||||||
|
echo "app.js : $APP_ASSETS_CACHE"
|
||||||
|
if [ $SW_ASSETS_CACHE == $APP_ASSETS_CACHE ] ; then
|
||||||
|
echo "Both values of 'ASSETS_CACHE' are identical"
|
||||||
|
else
|
||||||
|
echo "ERROR! Please ensure values for 'ASSETS_CACHE' in app.js and service-worker.js are identical!"
|
||||||
|
exit 1
|
||||||
|
fi
|
14
scripts/test_pwa_server.sh
Normal file
14
scripts/test_pwa_server.sh
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# This bash script tests whether PWAServer has been set correctly in app.js
|
||||||
|
|
||||||
|
SERVER=$(grep -E '^[^/]+params.+?PWAServer.+?http' ./www/js/app.js)
|
||||||
|
echo "The PWAServer is set to $SERVER"
|
||||||
|
SERVER_COUNT=$(grep -o 'PWAServer' <<< "$SERVER" | wc -l)
|
||||||
|
echo "$SERVER_COUNT server(s) are set in app.js"
|
||||||
|
if [[ $SERVER_COUNT > 1 || ! $SERVER =~ 'kiwix.org' ]]; then
|
||||||
|
echo "WARNING: The value of params['PWAServer'] is incorrectly set in app.js!"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "PWAServer is correctly set in app.js"
|
||||||
|
fi
|
@ -24,21 +24,39 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The name of the Cache API cache in which assets defined in regexpCachedContentTypes will be stored
|
* App version number - ENSURE IT MATCHES VALUE IN app.js
|
||||||
* The value is defined in app.js and will be passed to Service Worker on initialization (to avoid duplication)
|
* DEV: Changing this will cause the browser to recognize that the Service Worker has changed, and it will
|
||||||
* @type {String}
|
* download and install a new copy; we have to hard code this here because it is needed before any other file
|
||||||
|
* is cached in APP_CACHE
|
||||||
*/
|
*/
|
||||||
var CACHE_NAME;
|
const appVersion = '3.3-WIP';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A global Boolean that governs whether CACHE_NAME will be used
|
* The name of the Cache API cache in which assets defined in regexpCachedContentTypes will be stored
|
||||||
|
* The value is sometimes needed here before it can be passed from app.js, so we have to duplicate it
|
||||||
|
* @type {String}
|
||||||
|
*/
|
||||||
|
// DEV: Ensure this matches the name defined in app.js
|
||||||
|
const ASSETS_CACHE = 'kiwixjs-assetsCache';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the application cache to use for caching online code so that it can be used offline
|
||||||
|
* The cache name is made up of the prefix below and the appVersion: this is necessary so that when
|
||||||
|
* the app is updated, a new cache is created. The new cache will start being used after the user
|
||||||
|
* restarts the app, when we will also delete the old cache.
|
||||||
|
* @type {String}
|
||||||
|
*/
|
||||||
|
const APP_CACHE = 'kiwixjs-appCache-' + appVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A global Boolean that governs whether ASSETS_CACHE will be used
|
||||||
* Caching is on by default but can be turned off by the user in Configuration
|
* Caching is on by default but can be turned off by the user in Configuration
|
||||||
* @type {Boolean}
|
* @type {Boolean}
|
||||||
*/
|
*/
|
||||||
var useCache = true;
|
var useCache = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A regular expression that matches the Content-Types of assets that may be stored in CACHE_NAME
|
* A regular expression that matches the Content-Types of assets that may be stored in ASSETS_CACHE
|
||||||
* Add any further Content-Types you wish to cache to the regexp, separated by '|'
|
* Add any further Content-Types you wish to cache to the regexp, separated by '|'
|
||||||
* @type {RegExp}
|
* @type {RegExp}
|
||||||
*/
|
*/
|
||||||
@ -50,63 +68,156 @@ var regexpCachedContentTypes = /text\/css|text\/javascript|application\/javascri
|
|||||||
* 'example-extension' is included to show how to add another schema if necessary
|
* 'example-extension' is included to show how to add another schema if necessary
|
||||||
* @type {RegExp}
|
* @type {RegExp}
|
||||||
*/
|
*/
|
||||||
var regexpExcludedURLSchema = /^(?:chrome-extension|example-extension):/i;
|
var regexpExcludedURLSchema = /^(?:file|chrome-extension|example-extension):/i;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pattern for ZIM file namespace: see https://wiki.openzim.org/wiki/ZIM_file_format#Namespaces
|
* Pattern for ZIM file namespace: see https://wiki.openzim.org/wiki/ZIM_file_format#Namespaces
|
||||||
* In our case, there is also the ZIM file name used as a prefix in the URL
|
* In our case, there is also the ZIM file name used as a prefix in the URL
|
||||||
* @type {RegExp}
|
* @type {RegExp}
|
||||||
*/
|
*/
|
||||||
var regexpZIMUrlWithNamespace = /(?:^|\/)([^/]+\/)([-ABCIJMUVWX])\/(.+)/;
|
const regexpZIMUrlWithNamespace = /(?:^|\/)([^/]+\/)([-ABCIJMUVWX])\/(.+)/;
|
||||||
|
|
||||||
self.addEventListener('install', function (event) {
|
/**
|
||||||
event.waitUntil(self.skipWaiting());
|
* The list of files that the app needs in order to run entirely from offline code
|
||||||
|
*/
|
||||||
|
let precacheFiles = [
|
||||||
|
".", // This caches the redirect to www/index.html, in case a user launches the app from its root directory
|
||||||
|
"manifest.json",
|
||||||
|
"service-worker.js",
|
||||||
|
"www/css/app.css",
|
||||||
|
"www/css/bootstrap.css",
|
||||||
|
"www/css/kiwixJS_invert.css",
|
||||||
|
"www/css/kiwixJS_mwInvert.css",
|
||||||
|
"www/css/transition.css",
|
||||||
|
"www/img/icons/kiwix-256.png",
|
||||||
|
"www/img/icons/kiwix-32.png",
|
||||||
|
"www/img/icons/kiwix-60.png",
|
||||||
|
"www/img/spinner.gif",
|
||||||
|
"www/img/Icon_External_Link.png",
|
||||||
|
"www/index.html",
|
||||||
|
"www/article.html",
|
||||||
|
"www/main.html",
|
||||||
|
"www/js/app.js",
|
||||||
|
"www/js/init.js",
|
||||||
|
"www/js/lib/abstractFilesystemAccess.js",
|
||||||
|
"www/js/lib/arrayFromPolyfill.js",
|
||||||
|
"www/js/lib/bootstrap.bundle.js",
|
||||||
|
"www/js/lib/filecache.js",
|
||||||
|
"www/js/lib/jquery-3.2.1.slim.js",
|
||||||
|
"www/js/lib/promisePolyfill.js",
|
||||||
|
"www/js/lib/require.js",
|
||||||
|
"www/js/lib/settingsStore.js",
|
||||||
|
"www/js/lib/uiUtil.js",
|
||||||
|
"www/js/lib/utf8.js",
|
||||||
|
"www/js/lib/util.js",
|
||||||
|
"www/js/lib/xzdec_wrapper.js",
|
||||||
|
"www/js/lib/zstddec_wrapper.js",
|
||||||
|
"www/js/lib/zimArchive.js",
|
||||||
|
"www/js/lib/zimArchiveLoader.js",
|
||||||
|
"www/js/lib/zimDirEntry.js",
|
||||||
|
"www/js/lib/zimfile.js",
|
||||||
|
"www/js/lib/fontawesome/fontawesome.js",
|
||||||
|
"www/js/lib/fontawesome/solid.js",
|
||||||
|
"www/js/lib/xzdec-asm.js",
|
||||||
|
"www/js/lib/zstddec-asm.js",
|
||||||
|
"www/js/lib/xzdec-wasm.js",
|
||||||
|
"www/js/lib/xzdec-wasm.wasm",
|
||||||
|
"www/js/lib/zstddec-wasm.js",
|
||||||
|
"www/js/lib/zstddec-wasm.wasm"
|
||||||
|
];
|
||||||
|
|
||||||
|
// Process install event
|
||||||
|
self.addEventListener("install", function (event) {
|
||||||
|
console.debug("[SW] Install Event processing");
|
||||||
|
// DEV: We can't skip waiting because too many params are loaded at an early stage from the old file before the new one can activate...
|
||||||
|
// self.skipWaiting();
|
||||||
|
// We try to circumvent the browser's cache by adding a header to the Request, and it ensures all files are explicitly versioned
|
||||||
|
var requests = precacheFiles.map(function (urlPath) {
|
||||||
|
return new Request(urlPath + '?v' + appVersion, { cache: 'no-cache' });
|
||||||
|
});
|
||||||
|
if (!regexpExcludedURLSchema.test(requests[0].url)) event.waitUntil(
|
||||||
|
caches.open(APP_CACHE).then(function (cache) {
|
||||||
|
return Promise.all(
|
||||||
|
requests.map(function (request) {
|
||||||
|
return fetch(request).then(function (response) {
|
||||||
|
// Fail on 404, 500 etc
|
||||||
|
if (!response.ok) throw Error('Could not fetch ' + request.url);
|
||||||
|
return cache.put(request.url.replace(/\?v[^?/]+$/, ''), response);
|
||||||
|
}).catch(function (err) {
|
||||||
|
console.error('There was an error pre-caching files', err);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Allow sw to control current page
|
||||||
self.addEventListener('activate', function (event) {
|
self.addEventListener('activate', function (event) {
|
||||||
// "Claiming" the ServiceWorker is necessary to make it work right away,
|
// "Claiming" the ServiceWorker is necessary to make it work right away,
|
||||||
// without the need to reload the page.
|
// without the need to reload the page.
|
||||||
// See https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim
|
// See https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim
|
||||||
event.waitUntil(self.clients.claim());
|
event.waitUntil(self.clients.claim());
|
||||||
|
console.debug('[SW] Claiming clients for current page');
|
||||||
|
// Check all the cache keys, and delete any old caches
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then(function (keyList) {
|
||||||
|
return Promise.all(keyList.map(function (key) {
|
||||||
|
console.debug('[SW] Current cache key is ' + key);
|
||||||
|
if (key !== APP_CACHE && key !== ASSETS_CACHE) {
|
||||||
|
console.debug('[SW] App updated to version ' + appVersion + ': deleting old cache');
|
||||||
|
return caches.delete(key);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
var outgoingMessagePort = null;
|
let outgoingMessagePort = null;
|
||||||
var fetchCaptureEnabled = false;
|
let fetchCaptureEnabled = false;
|
||||||
|
|
||||||
self.addEventListener('fetch', function (event) {
|
self.addEventListener('fetch', function (event) {
|
||||||
if (fetchCaptureEnabled &&
|
// Only cache GET requests
|
||||||
regexpZIMUrlWithNamespace.test(event.request.url) &&
|
if (event.request.method !== "GET") return;
|
||||||
event.request.method === "GET") {
|
// Remove any querystring before requesting from the cache
|
||||||
|
var rqUrl = event.request.url.replace(/\?[^?]+$/i, '');
|
||||||
// The ServiceWorker will handle this request either from CACHE_NAME or from app.js
|
// Select cache depending on request format
|
||||||
|
var cache = /\.zim\//i.test(rqUrl) ? ASSETS_CACHE : APP_CACHE;
|
||||||
|
if (cache === ASSETS_CACHE && !fetchCaptureEnabled) return;
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
// First see if the content is in the cache
|
// First see if the content is in the cache
|
||||||
fromCache(event.request).then(
|
fromCache(cache, rqUrl).then(function (response) {
|
||||||
function (response) {
|
|
||||||
// The response was found in the cache so we respond with it
|
// The response was found in the cache so we respond with it
|
||||||
return response;
|
return response;
|
||||||
},
|
}, function () {
|
||||||
function () {
|
|
||||||
// The response was not found in the cache so we look for it in the ZIM
|
// The response was not found in the cache so we look for it in the ZIM
|
||||||
// and add it to the cache if it is an asset type (css or js)
|
// and add it to the cache if it is an asset type (css or js)
|
||||||
|
if (cache === ASSETS_CACHE && regexpZIMUrlWithNamespace.test(rqUrl)) {
|
||||||
return fetchRequestFromZIM(event).then(function (response) {
|
return fetchRequestFromZIM(event).then(function (response) {
|
||||||
// Add css or js assets to CACHE_NAME (or update their cache entries) unless the URL schema is not supported
|
// Add css or js assets to ASSETS_CACHE (or update their cache entries) unless the URL schema is not supported
|
||||||
if (regexpCachedContentTypes.test(response.headers.get('Content-Type')) &&
|
if (regexpCachedContentTypes.test(response.headers.get('Content-Type')) &&
|
||||||
!regexpExcludedURLSchema.test(event.request.url)) {
|
!regexpExcludedURLSchema.test(event.request.url)) {
|
||||||
event.waitUntil(updateCache(event.request, response.clone()));
|
event.waitUntil(updateCache(ASSETS_CACHE, event.request, response.clone()));
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
}).catch(function (msgPortData, title) {
|
}).catch(function (msgPortData, title) {
|
||||||
console.error('Invalid message received from app.js for ' + title, msgPortData);
|
console.error('Invalid message received from app.js for ' + title, msgPortData);
|
||||||
return msgPortData;
|
return msgPortData;
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// It's not an asset, or it doesn't match a ZIM URL pattern, so we should fetch it with Fetch API
|
||||||
|
return fetch(event.request).then(function (response) {
|
||||||
|
// If request was successful, add or update it in the cache, but be careful not to cache the ZIM archive itself!
|
||||||
|
if (!regexpExcludedURLSchema.test(rqUrl) && !/\.zim\w{0,2}$/i.test(rqUrl)) {
|
||||||
|
event.waitUntil(updateCache(APP_CACHE, event.request, response.clone()));
|
||||||
}
|
}
|
||||||
)
|
return response;
|
||||||
|
}).catch(function (error) {
|
||||||
|
console.debug("[SW] Network request failed and no cache.", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
|
||||||
// If event.respondWith() isn't called because this wasn't a request that we want to handle,
|
|
||||||
// then the default request/response behavior will automatically be used.
|
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('message', function (event) {
|
self.addEventListener('message', function (event) {
|
||||||
@ -123,13 +234,15 @@ self.addEventListener('message', function (event) {
|
|||||||
if (event.data.action.useCache) {
|
if (event.data.action.useCache) {
|
||||||
// Turns caching on or off (a string value of 'on' turns it on, any other string turns it off)
|
// Turns caching on or off (a string value of 'on' turns it on, any other string turns it off)
|
||||||
useCache = event.data.action.useCache === 'on';
|
useCache = event.data.action.useCache === 'on';
|
||||||
if (useCache) CACHE_NAME = event.data.cacheName;
|
console.debug('[SW] Caching was turned ' + event.data.action.useCache);
|
||||||
console.log('[SW] Caching was turned ' + event.data.action.useCache);
|
}
|
||||||
|
if (event.data.action === 'getCacheNames') {
|
||||||
|
event.ports[0].postMessage({ 'app': APP_CACHE, 'assets': ASSETS_CACHE });
|
||||||
}
|
}
|
||||||
if (event.data.action.checkCache) {
|
if (event.data.action.checkCache) {
|
||||||
// Checks and returns the caching strategy: checkCache key should contain a sample URL string to test
|
// Checks and returns the caching strategy: checkCache key should contain a sample URL string to test
|
||||||
testCacheAndCountAssets(event.data.action.checkCache).then(function (cacheArr) {
|
testCacheAndCountAssets(event.data.action.checkCache).then(function (cacheArr) {
|
||||||
event.ports[0].postMessage({ 'type': cacheArr[0], 'description': cacheArr[1], 'count': cacheArr[2] });
|
event.ports[0].postMessage({ type: cacheArr[0], name: cacheArr[1], description: cacheArr[2], count: cacheArr[3] });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -208,51 +321,53 @@ function removeUrlParameters(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Looks up a Request in CACHE_NAME and returns a Promise for the matched Response
|
* Looks up a Request in a cache and returns a Promise for the matched Response
|
||||||
* @param {Request} request The Request to fulfill from CACHE_NAME
|
* @param {String} cache The name of the cache to look in
|
||||||
|
* @param {String} requestUrl The Request URL to fulfill from cache
|
||||||
* @returns {Promise<Response>} A Promise for the cached Response, or rejects with strings 'disabled' or 'no-match'
|
* @returns {Promise<Response>} A Promise for the cached Response, or rejects with strings 'disabled' or 'no-match'
|
||||||
*/
|
*/
|
||||||
function fromCache(request) {
|
function fromCache(cache, requestUrl) {
|
||||||
// Prevents use of Cache API if user has disabled it
|
// Prevents use of Cache API if user has disabled it
|
||||||
if (!useCache) return Promise.reject('disabled');
|
if (!useCache && cache === ASSETS_CACHE) return Promise.reject('disabled');
|
||||||
return caches.open(CACHE_NAME).then(function (cache) {
|
return caches.open(cache).then(function (cacheObj) {
|
||||||
return cache.match(request).then(function (matching) {
|
return cacheObj.match(requestUrl).then(function (matching) {
|
||||||
if (!matching || matching.status === 404) {
|
if (!matching || matching.status === 404) {
|
||||||
return Promise.reject('no-match');
|
return Promise.reject('no-match');
|
||||||
}
|
}
|
||||||
console.log('[SW] Supplying ' + request.url + ' from ' + CACHE_NAME + '...');
|
console.debug('[SW] Supplying ' + requestUrl + ' from ' + cache + '...');
|
||||||
return matching;
|
return matching;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores or updates in CACHE_NAME the given Request/Response pair
|
* Stores or updates in a cache the given Request/Response pair
|
||||||
|
* @param {String} cache The name of the cache to open
|
||||||
* @param {Request} request The original Request object
|
* @param {Request} request The original Request object
|
||||||
* @param {Response} response The Response received from the server/ZIM
|
* @param {Response} response The Response received from the server/ZIM
|
||||||
* @returns {Promise} A Promise for the update action
|
* @returns {Promise} A Promise for the update action
|
||||||
*/
|
*/
|
||||||
function updateCache(request, response) {
|
function updateCache(cache, request, response) {
|
||||||
// Prevents use of Cache API if user has disabled it
|
// Prevents use of Cache API if user has disabled it
|
||||||
if (!useCache) return Promise.resolve();
|
if (!useCache && cache === ASSETS_CACHE) return Promise.resolve();
|
||||||
return caches.open(CACHE_NAME).then(function (cache) {
|
return caches.open(cache).then(function (cacheObj) {
|
||||||
console.log('[SW] Adding ' + request.url + ' to ' + CACHE_NAME + '...');
|
console.debug('[SW] Adding ' + request.url + ' to ' + cache + '...');
|
||||||
return cache.put(request, response);
|
return cacheObj.put(request, response);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests the caching strategy available to this app and if it is Cache API, count the
|
* Tests the caching strategy available to this app and if it is Cache API, count the
|
||||||
* number of assets in CACHE_NAME
|
* number of assets in ASSETS_CACHE
|
||||||
* @param {String} url A URL to test against excludedURLSchema
|
* @param {String} url A URL to test against excludedURLSchema
|
||||||
* @returns {Promise<Array>} A Promise for an array of format [cacheType, cacheDescription, assetCount]
|
* @returns {Promise<Array>} A Promise for an array of format [cacheType, cacheDescription, assetCount]
|
||||||
*/
|
*/
|
||||||
function testCacheAndCountAssets(url) {
|
function testCacheAndCountAssets(url) {
|
||||||
if (regexpExcludedURLSchema.test(url)) return Promise.resolve(['custom', 'Custom', '-']);
|
if (regexpExcludedURLSchema.test(url)) return Promise.resolve(['custom', 'custom', 'Custom', '-']);
|
||||||
if (!useCache) return Promise.resolve(['none', 'None', 0]);
|
if (!useCache) return Promise.resolve(['none', 'none', 'None', 0]);
|
||||||
return caches.open(CACHE_NAME).then(function (cache) {
|
return caches.open(ASSETS_CACHE).then(function (cache) {
|
||||||
return cache.keys().then(function (keys) {
|
return cache.keys().then(function (keys) {
|
||||||
return ['cacheAPI', 'Cache API', keys.length];
|
return ['cacheAPI', ASSETS_CACHE, 'Cache API', keys.length];
|
||||||
}).catch(function(err) {
|
}).catch(function(err) {
|
||||||
return err;
|
return err;
|
||||||
});
|
});
|
||||||
|
@ -55,7 +55,7 @@
|
|||||||
<section id="search-article" role="region">
|
<section id="search-article" role="region">
|
||||||
<header id="top">
|
<header id="top">
|
||||||
<nav class="navbar navbar-expand-md bg-light" role="navigation">
|
<nav class="navbar navbar-expand-md bg-light" role="navigation">
|
||||||
<a class="navbar-brand">Kiwix 3.3-WIP</a>
|
<a id="appVersion" class="navbar-brand">Kiwix</a>
|
||||||
|
|
||||||
<!-- Toggler/collapsible Button -->
|
<!-- Toggler/collapsible Button -->
|
||||||
<button class="navbar-toggler" type="button" data-toggle="collapse"
|
<button class="navbar-toggler" type="button" data-toggle="collapse"
|
||||||
@ -302,10 +302,10 @@
|
|||||||
supported in this mode. It can feel initially a little slower while commonly used assets are being cached,
|
supported in this mode. It can feel initially a little slower while commonly used assets are being cached,
|
||||||
but it soon equals JQuery mode in speed, at least in modern browsers. However, older browsers such as IE11 do
|
but it soon equals JQuery mode in speed, at least in modern browsers. However, older browsers such as IE11 do
|
||||||
not support this mode, and the app must be running in a secure context (<code>https:</code>, <code>localhost</code>,
|
not support this mode, and the app must be running in a secure context (<code>https:</code>, <code>localhost</code>,
|
||||||
or certain browser extensions). Unfortunately, this mode is not currently supported in Mozilla (Firefox) browser
|
or certain browser extensions). While this mode is not natively supported in Mozilla (Firefox) browser
|
||||||
extensions. It <i>is</i> supported in Chromium browser extensions (e.g. Chrome or new Edge). Note that this mode
|
extensions, we provide a functional workaround by re-launching the extension as a Progressive Web App (PWA).
|
||||||
cannot run with the <code>file:</code> protocol (but only IE11 and old Edge allow the app to run by launching
|
Note that this mode cannot run with the <code>file:</code> protocol (but only IE11 and old Edge allow the app to run
|
||||||
<code>index.html</code> from the file system).
|
by launching <code>index.html</code> from the file system).
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</p>
|
</p>
|
||||||
@ -403,6 +403,11 @@
|
|||||||
<div id="configuration" style="display: none;">
|
<div id="configuration" style="display: none;">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>Configuration</h2>
|
<h2>Configuration</h2>
|
||||||
|
<!-- Bootstrap alert box -->
|
||||||
|
<div id="updateAlert" class="alert alert-warning alert-dismissible" style="display: none;">
|
||||||
|
<button type="button" class="close" data-hide="alert">×</button>
|
||||||
|
<span id="persistentMessage"></span>
|
||||||
|
</div>
|
||||||
<p id="downloadInstruction">This application needs a ZIM archive to work. For download
|
<p id="downloadInstruction">This application needs a ZIM archive to work. For download
|
||||||
instructions, please see the About section</p>
|
instructions, please see the About section</p>
|
||||||
<div id="selectorsDisplay" style="display: none;">
|
<div id="selectorsDisplay" style="display: none;">
|
||||||
@ -582,7 +587,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- Bootstrap alert box -->
|
<!-- Bootstrap alert box -->
|
||||||
<div id="alertBoxHeader">
|
<div id="alertBoxHeader">
|
||||||
<div id="activeContent" style="display:none;" class="alert alert-warning alert-dismissible fade show">
|
<div id="activeContent" style="display:none;" class="kiwix-alert alert alert-warning alert-dismissible fade show">
|
||||||
<button type="button" class="close" data-hide="alert">×</button>
|
<button type="button" class="close" data-hide="alert">×</button>
|
||||||
<strong>Unable to display active content:</strong> This ZIM is not fully supported in jQuery mode.<br />
|
<strong>Unable to display active content:</strong> This ZIM is not fully supported in jQuery mode.<br />
|
||||||
Content may be available by searching above (type a space or a letter of the alphabet), or else
|
Content may be available by searching above (type a space or a letter of the alphabet), or else
|
||||||
@ -595,7 +600,7 @@
|
|||||||
<footer>
|
<footer>
|
||||||
<!-- Bootstrap alert box -->
|
<!-- Bootstrap alert box -->
|
||||||
<div id="alertBoxFooter">
|
<div id="alertBoxFooter">
|
||||||
<div id="downloadAlert" style="display:none;" class="alert alert-info alert-dismissible fade show">
|
<div id="downloadAlert" style="display:none;" class="kiwix-alert alert alert-info alert-dismissible fade show">
|
||||||
<button type="button" class="close" data-hide="alert">×</button>
|
<button type="button" class="close" data-hide="alert">×</button>
|
||||||
<span id="alertMessage"></span>
|
<span id="alertMessage"></span>
|
||||||
</div>
|
</div>
|
||||||
|
309
www/js/app.js
309
www/js/app.js
@ -38,11 +38,12 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The name of the Cache API cache to use for caching Service Worker requests and responses for certain asset types
|
* The name of the Cache API cache to use for caching Service Worker requests and responses for certain asset types
|
||||||
* This name will be passed to service-worker.js in messaging to avoid duplication: see comment in service-worker.js
|
* We need access to the cache name in app.js in order to complete utility actions when Service Worker is not initialized,
|
||||||
* We need access to this constant in app.js in order to complete utility actions when Service Worker is not initialized
|
* so we have to duplicate it here
|
||||||
* @type {String}
|
* @type {String}
|
||||||
*/
|
*/
|
||||||
const CACHE_NAME = 'kiwixjs-assetCache';
|
// DEV: Ensure this matches the name defined in service-worker.js (a check is provided in refreshCacheStatus() below)
|
||||||
|
const ASSETS_CACHE = 'kiwixjs-assetsCache';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Memory cache for CSS styles contained in ZIM: it significantly speeds up subsequent page display
|
* Memory cache for CSS styles contained in ZIM: it significantly speeds up subsequent page display
|
||||||
@ -64,27 +65,34 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
|||||||
*/
|
*/
|
||||||
var selectedArchive = null;
|
var selectedArchive = null;
|
||||||
|
|
||||||
// Set parameters and associated UI elements from the Settings Store
|
/**
|
||||||
// DEV: The params global object is declared in init.js so that it is available to modules
|
* Set parameters from the Settings Store, together with any defaults
|
||||||
params['storeType'] = settingsStore.getBestAvailableStorageAPI(); // A parameter to determine the Settings Store API in use
|
* Note that the params global object is declared in init.js so that it is available to modules
|
||||||
|
* WARNING: Only change these paramaeters if you know what you are doing
|
||||||
|
*/
|
||||||
|
// The current version number of this app
|
||||||
|
params['appVersion'] = '3.3-WIP'; // **IMPORTANT** Ensure this is the same as the version number in service-worker.js
|
||||||
|
// The PWA server (currently only for use with the Mozilla extension)
|
||||||
|
params['PWAServer'] = 'https://moz-extension.kiwix.org/current/'; // Include final slash!
|
||||||
|
// params['PWAServer'] = 'https://kiwix.github.io/kiwix-js/'; // DEV: Uncomment this line for testing code on GitHub Pages
|
||||||
|
// params['PWAServer'] = 'http://localhost:8080/'; // DEV: Uncomment this line (and adjust) for local testing
|
||||||
|
// A parameter to determine the Settings Store API in use
|
||||||
|
params['storeType'] = settingsStore.getBestAvailableStorageAPI();
|
||||||
params['hideActiveContentWarning'] = settingsStore.getItem('hideActiveContentWarning') === 'true';
|
params['hideActiveContentWarning'] = settingsStore.getItem('hideActiveContentWarning') === 'true';
|
||||||
params['showUIAnimations'] = settingsStore.getItem('showUIAnimations') ? settingsStore.getItem('showUIAnimations') === 'true' : true;
|
params['showUIAnimations'] = settingsStore.getItem('showUIAnimations') ? settingsStore.getItem('showUIAnimations') === 'true' : true;
|
||||||
document.getElementById('hideActiveContentWarningCheck').checked = params.hideActiveContentWarning;
|
|
||||||
document.getElementById('showUIAnimationsCheck').checked = params.showUIAnimations;
|
|
||||||
// Maximum number of article titles to return (range is 5 - 50, default 25)
|
// Maximum number of article titles to return (range is 5 - 50, default 25)
|
||||||
params['maxSearchResultsSize'] = settingsStore.getItem('maxSearchResultsSize') || 25;
|
params['maxSearchResultsSize'] = settingsStore.getItem('maxSearchResultsSize') || 25;
|
||||||
document.getElementById('titleSearchRange').value = params.maxSearchResultsSize;
|
|
||||||
document.getElementById('titleSearchRangeVal').innerHTML = params.maxSearchResultsSize;
|
|
||||||
// A global parameter that turns caching on or off and deletes the cache (it defaults to true unless explicitly turned off in UI)
|
// A global parameter that turns caching on or off and deletes the cache (it defaults to true unless explicitly turned off in UI)
|
||||||
params['useCache'] = settingsStore.getItem('useCache') !== 'false';
|
params['useCache'] = settingsStore.getItem('useCache') !== 'false';
|
||||||
// A parameter to set the app theme and, if necessary, the CSS theme for article content (defaults to 'light')
|
// A parameter to set the app theme and, if necessary, the CSS theme for article content (defaults to 'light')
|
||||||
params['appTheme'] = settingsStore.getItem('appTheme') || 'light'; // Currently implemented: light|dark|dark_invert|dark_mwInvert
|
params['appTheme'] = settingsStore.getItem('appTheme') || 'light'; // Currently implemented: light|dark|dark_invert|dark_mwInvert
|
||||||
document.getElementById('appThemeSelect').value = params.appTheme;
|
|
||||||
uiUtil.applyAppTheme(params.appTheme);
|
|
||||||
// A global parameter to turn on/off the use of Keyboard HOME Key to focus search bar
|
// A global parameter to turn on/off the use of Keyboard HOME Key to focus search bar
|
||||||
params['useHomeKeyToFocusSearchBar'] = settingsStore.getItem('useHomeKeyToFocusSearchBar') === 'true';
|
params['useHomeKeyToFocusSearchBar'] = settingsStore.getItem('useHomeKeyToFocusSearchBar') === 'true';
|
||||||
document.getElementById('useHomeKeyToFocusSearchBarCheck').checked = params.useHomeKeyToFocusSearchBar;
|
// A parameter to access the URL of any extension that this app was launched from
|
||||||
switchHomeKeyToFocusSearchBar();
|
params['referrerExtensionURL'] = settingsStore.getItem('referrerExtensionURL');
|
||||||
|
// A parameter to set the content injection mode ('jquery' or 'serviceworker') used by this app
|
||||||
|
params['contentInjectionMode'] = settingsStore.getItem('contentInjectionMode') || 'jquery'; // Defaults to jquery for now
|
||||||
|
|
||||||
// An object to hold the current search and its state (allows cancellation of search across modules)
|
// An object to hold the current search and its state (allows cancellation of search across modules)
|
||||||
appstate['search'] = {
|
appstate['search'] = {
|
||||||
'prefix': '', // A field to hold the original search string
|
'prefix': '', // A field to hold the original search string
|
||||||
@ -92,6 +100,66 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
|||||||
'type': '' // The type of the search: 'basic'|'full' (set automatically in search algorithm)
|
'type': '' // The type of the search: 'basic'|'full' (set automatically in search algorithm)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A Boolean to store the update status of the PWA version (currently only used with Firefox Extension)
|
||||||
|
appstate['pwaUpdateNeeded'] = false; // This will be set to true if the Service Worker has an update waiting
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply any override parameters that might be in the querystring.
|
||||||
|
* This is used for communication between the PWA and any local code (e.g. Firefox Extension), both ways.
|
||||||
|
* It is also possible for DEV (or user) to launch the app with certain settings, or to unset potentially
|
||||||
|
* problematic settings, by crafting the querystring appropriately.
|
||||||
|
*/
|
||||||
|
(function overrideParams() {
|
||||||
|
var regexpUrlParams = /[?&]([^=]+)=([^&]+)/g;
|
||||||
|
var matches = regexpUrlParams.exec(window.location.search);
|
||||||
|
while (matches) {
|
||||||
|
if (matches[1] && matches[2]) {
|
||||||
|
var paramKey = decodeURIComponent(matches[1]);
|
||||||
|
var paramVal = decodeURIComponent(matches[2]);
|
||||||
|
if (paramKey !== 'title') {
|
||||||
|
console.debug('Setting key-pair: ' + paramKey + ':' + paramVal);
|
||||||
|
// Make values Boolean if 'true'/'false'
|
||||||
|
paramVal = paramVal === 'true' || (paramVal === 'false' ? false : paramVal);
|
||||||
|
settingsStore.setItem(paramKey, paramVal, Infinity);
|
||||||
|
params[paramKey] = paramVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matches = regexpUrlParams.exec(window.location.search);
|
||||||
|
}
|
||||||
|
// If we are in the PWA version launched from an extension, send a 'success' message to the extension
|
||||||
|
if (params.referrerExtensionURL && ~window.location.href.indexOf(params.PWAServer)) {
|
||||||
|
var message = '?PWA_launch=success';
|
||||||
|
// DEV: To test failure of the PWA, you could pause on next line and set message to '?PWA_launch=fail'
|
||||||
|
// Note that, as a failsafe, the PWA_launch key is set to 'fail' (in the extension) before each PWA launch
|
||||||
|
// so we need to send a 'success' message each time the PWA is launched
|
||||||
|
var frame = document.createElement('iframe');
|
||||||
|
frame.id = 'kiwixComm';
|
||||||
|
frame.style.display = 'none';
|
||||||
|
document.body.appendChild(frame);
|
||||||
|
frame.src = params.referrerExtensionURL + '/www/index.html'+ message;
|
||||||
|
// Now remove redundant frame. We cannot use onload, because it doesn't give time for the script to run.
|
||||||
|
setTimeout(function () {
|
||||||
|
var kiwixComm = document.getElementById('kiwixComm');
|
||||||
|
// The only browser which does not support .remove() is IE11, but it will never run this code
|
||||||
|
if (kiwixComm) kiwixComm.remove();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the State and UI settings associated with parameters defined above
|
||||||
|
*/
|
||||||
|
document.getElementById('hideActiveContentWarningCheck').checked = params.hideActiveContentWarning;
|
||||||
|
document.getElementById('showUIAnimationsCheck').checked = params.showUIAnimations;
|
||||||
|
document.getElementById('titleSearchRange').value = params.maxSearchResultsSize;
|
||||||
|
document.getElementById('titleSearchRangeVal').innerHTML = encodeURIComponent(params.maxSearchResultsSize);
|
||||||
|
document.getElementById('appThemeSelect').value = params.appTheme;
|
||||||
|
uiUtil.applyAppTheme(params.appTheme);
|
||||||
|
document.getElementById('useHomeKeyToFocusSearchBarCheck').checked = params.useHomeKeyToFocusSearchBar;
|
||||||
|
switchHomeKeyToFocusSearchBar();
|
||||||
|
document.getElementById('appVersion').innerHTML = 'Kiwix ' + params.appVersion;
|
||||||
|
setContentInjectionMode(params.contentInjectionMode);
|
||||||
|
|
||||||
// Define globalDropZone (universal drop area) and configDropZone (highlighting area on Config page)
|
// Define globalDropZone (universal drop area) and configDropZone (highlighting area on Config page)
|
||||||
var globalDropZone = document.getElementById('search-article');
|
var globalDropZone = document.getElementById('search-article');
|
||||||
var configDropZone = document.getElementById('configuration');
|
var configDropZone = document.getElementById('configuration');
|
||||||
@ -130,7 +198,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
|||||||
// Do not initiate the same search if it is already in progress
|
// Do not initiate the same search if it is already in progress
|
||||||
if (appstate.search.prefix === prefix && !/^(cancelled|complete)$/.test(appstate.search.status)) return;
|
if (appstate.search.prefix === prefix && !/^(cancelled|complete)$/.test(appstate.search.status)) return;
|
||||||
$("#welcomeText").hide();
|
$("#welcomeText").hide();
|
||||||
$('.alert').hide();
|
$('.kiwix-alert').hide();
|
||||||
$("#searchingArticles").show();
|
$("#searchingArticles").show();
|
||||||
pushBrowserHistoryState(null, prefix);
|
pushBrowserHistoryState(null, prefix);
|
||||||
// Initiate the search
|
// Initiate the search
|
||||||
@ -306,9 +374,10 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
|||||||
$('#formArticleSearch').hide();
|
$('#formArticleSearch').hide();
|
||||||
$("#welcomeText").hide();
|
$("#welcomeText").hide();
|
||||||
$("#searchingArticles").hide();
|
$("#searchingArticles").hide();
|
||||||
$('.alert').hide();
|
$('.kiwix-alert').hide();
|
||||||
refreshAPIStatus();
|
refreshAPIStatus();
|
||||||
refreshCacheStatus();
|
refreshCacheStatus();
|
||||||
|
uiUtil.checkUpdateStatus(appstate);
|
||||||
// Use a timeout of 400ms because uiUtil.applyAnimationToSection uses a timeout of 300ms
|
// Use a timeout of 400ms because uiUtil.applyAnimationToSection uses a timeout of 300ms
|
||||||
setTimeout(resizeIFrame, 400);
|
setTimeout(resizeIFrame, 400);
|
||||||
return false;
|
return false;
|
||||||
@ -333,7 +402,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
|||||||
$("#welcomeText").hide();
|
$("#welcomeText").hide();
|
||||||
$('#articleListWithHeader').hide();
|
$('#articleListWithHeader').hide();
|
||||||
$("#searchingArticles").hide();
|
$("#searchingArticles").hide();
|
||||||
$('.alert').hide();
|
$('.kiwix-alert').hide();
|
||||||
// Use a timeout of 400ms because uiUtil.applyAnimationToSection uses a timeout of 300ms
|
// Use a timeout of 400ms because uiUtil.applyAnimationToSection uses a timeout of 300ms
|
||||||
setTimeout(resizeIFrame, 400);
|
setTimeout(resizeIFrame, 400);
|
||||||
return false;
|
return false;
|
||||||
@ -373,7 +442,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
|||||||
params.useCache = false;
|
params.useCache = false;
|
||||||
// Delete all caches
|
// Delete all caches
|
||||||
resetCssCache();
|
resetCssCache();
|
||||||
if ('caches' in window) caches.delete(CACHE_NAME);
|
if ('caches' in window) caches.delete(ASSETS_CACHE);
|
||||||
refreshCacheStatus();
|
refreshCacheStatus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -491,9 +560,9 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
|||||||
* If Service Worker is not available, the attributes of the memory cache are returned instead
|
* If Service Worker is not available, the attributes of the memory cache are returned instead
|
||||||
* @returns {Promise<Object>} A Promise for an object with cache attributes 'type', 'description', and 'count'
|
* @returns {Promise<Object>} A Promise for an object with cache attributes 'type', 'description', and 'count'
|
||||||
*/
|
*/
|
||||||
function getCacheAttributes() {
|
function getAssetsCacheAttributes() {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
if (contentInjectionMode === 'serviceworker') {
|
if (params.contentInjectionMode === 'serviceworker' && navigator.serviceWorker && navigator.serviceWorker.controller) {
|
||||||
// Create a Message Channel
|
// Create a Message Channel
|
||||||
var channel = new MessageChannel();
|
var channel = new MessageChannel();
|
||||||
// Handler for recieving message reply from service worker
|
// Handler for recieving message reply from service worker
|
||||||
@ -507,13 +576,13 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
|||||||
'action': {
|
'action': {
|
||||||
'useCache': params.useCache ? 'on' : 'off',
|
'useCache': params.useCache ? 'on' : 'off',
|
||||||
'checkCache': window.location.href
|
'checkCache': window.location.href
|
||||||
},
|
}
|
||||||
'cacheName': CACHE_NAME
|
|
||||||
}, [channel.port2]);
|
}, [channel.port2]);
|
||||||
} else {
|
} else {
|
||||||
// No Service Worker has been established, so we resolve the Promise with cssCache details only
|
// No Service Worker has been established, so we resolve the Promise with cssCache details only
|
||||||
resolve({
|
resolve({
|
||||||
'type': params.useCache ? 'memory' : 'none',
|
'type': params.useCache ? 'memory' : 'none',
|
||||||
|
'name': 'cssCache',
|
||||||
'description': params.useCache ? 'Memory' : 'None',
|
'description': params.useCache ? 'Memory' : 'None',
|
||||||
'count': cssCache.size
|
'count': cssCache.size
|
||||||
});
|
});
|
||||||
@ -528,7 +597,10 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
|||||||
// Update radio buttons and checkbox
|
// Update radio buttons and checkbox
|
||||||
document.getElementById('cachedAssetsModeRadio' + (params.useCache ? 'True' : 'False')).checked = true;
|
document.getElementById('cachedAssetsModeRadio' + (params.useCache ? 'True' : 'False')).checked = true;
|
||||||
// Get cache attributes, then update the UI with the obtained data
|
// Get cache attributes, then update the UI with the obtained data
|
||||||
getCacheAttributes().then(function (cache) {
|
getAssetsCacheAttributes().then(function (cache) {
|
||||||
|
if (cache.type === 'cacheAPI' && ASSETS_CACHE !== cache.name) {
|
||||||
|
console.error('DEV: The ASSETS_CACHE defined in app.js does not match the ASSETS_CACHE defined in service-worker.js!');
|
||||||
|
}
|
||||||
document.getElementById('cacheUsed').innerHTML = cache.description;
|
document.getElementById('cacheUsed').innerHTML = cache.description;
|
||||||
document.getElementById('assetsCount').innerHTML = cache.count;
|
document.getElementById('assetsCount').innerHTML = cache.count;
|
||||||
var cacheSettings = document.getElementById('performanceSettingsDiv');
|
var cacheSettings = document.getElementById('performanceSettingsDiv');
|
||||||
@ -543,8 +615,8 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentInjectionMode;
|
|
||||||
var keepAliveServiceWorkerHandle;
|
var keepAliveServiceWorkerHandle;
|
||||||
|
var serviceWorkerRegistration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send an 'init' message to the ServiceWorker with a new MessageChannel
|
* Send an 'init' message to the ServiceWorker with a new MessageChannel
|
||||||
@ -553,17 +625,28 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
|||||||
* and the application
|
* and the application
|
||||||
*/
|
*/
|
||||||
function initOrKeepAliveServiceWorker() {
|
function initOrKeepAliveServiceWorker() {
|
||||||
if (contentInjectionMode === 'serviceworker') {
|
var delay = DELAY_BETWEEN_KEEPALIVE_SERVICEWORKER;
|
||||||
|
if (params.contentInjectionMode === 'serviceworker') {
|
||||||
// Create a new messageChannel
|
// Create a new messageChannel
|
||||||
var tmpMessageChannel = new MessageChannel();
|
var tmpMessageChannel = new MessageChannel();
|
||||||
tmpMessageChannel.port1.onmessage = handleMessageChannelMessage;
|
tmpMessageChannel.port1.onmessage = handleMessageChannelMessage;
|
||||||
// Send the init message to the ServiceWorker, with this MessageChannel as a parameter
|
// Send the init message to the ServiceWorker, with this MessageChannel as a parameter
|
||||||
navigator.serviceWorker.controller.postMessage({'action': 'init'}, [tmpMessageChannel.port2]);
|
if (navigator.serviceWorker.controller) {
|
||||||
|
navigator.serviceWorker.controller.postMessage({
|
||||||
|
'action': 'init'
|
||||||
|
}, [tmpMessageChannel.port2]);
|
||||||
|
} else if (keepAliveServiceWorkerHandle) {
|
||||||
|
console.error('The Service Worker is active but is not controlling the current page! We have to reload.');
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
// If this is the first time we are initiating the SW, allow Promises to complete by delaying potential reload till next tick
|
||||||
|
delay = 0;
|
||||||
|
}
|
||||||
messageChannel = tmpMessageChannel;
|
messageChannel = tmpMessageChannel;
|
||||||
// Schedule to do it again regularly to keep the 2-way communication alive.
|
// Schedule to do it again regularly to keep the 2-way communication alive.
|
||||||
// See https://github.com/kiwix/kiwix-js/issues/145 to understand why
|
// See https://github.com/kiwix/kiwix-js/issues/145 to understand why
|
||||||
clearTimeout(keepAliveServiceWorkerHandle);
|
clearTimeout(keepAliveServiceWorkerHandle);
|
||||||
keepAliveServiceWorkerHandle = setTimeout(initOrKeepAliveServiceWorker, DELAY_BETWEEN_KEEPALIVE_SERVICEWORKER, false);
|
keepAliveServiceWorkerHandle = setTimeout(initOrKeepAliveServiceWorker, delay, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -575,19 +658,45 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
|||||||
* @param {String} value The chosen content injection mode : 'jquery' or 'serviceworker'
|
* @param {String} value The chosen content injection mode : 'jquery' or 'serviceworker'
|
||||||
*/
|
*/
|
||||||
function setContentInjectionMode(value) {
|
function setContentInjectionMode(value) {
|
||||||
|
params.contentInjectionMode = value;
|
||||||
if (value === 'jquery') {
|
if (value === 'jquery') {
|
||||||
if (isServiceWorkerReady()) {
|
if (params.referrerExtensionURL) {
|
||||||
// We need to disable the ServiceWorker
|
// We are in an extension, and the user may wish to revert to local code
|
||||||
// Unregistering it does not seem to work as expected : the ServiceWorker
|
var message = 'This will switch to using locally packaged code only. Some configuration settings may be lost.\n\n' +
|
||||||
// is indeed unregistered but still active...
|
'WARNING: After this, you may not be able to switch back to SW mode without an online connection!';
|
||||||
// So we have to disable it manually (even if it's still registered and active)
|
var launchLocal = function () {
|
||||||
navigator.serviceWorker.controller.postMessage({'action': 'disable'});
|
settingsStore.setItem('allowInternetAccess', false, Infinity);
|
||||||
messageChannel = null;
|
var uriParams = '?allowInternetAccess=false&contentInjectionMode=jquery&hideActiveContentWarning=false';
|
||||||
|
uriParams += '&appTheme=' + params.appTheme;
|
||||||
|
uriParams += '&showUIAnimations=' + params.showUIAnimations;
|
||||||
|
window.location.href = params.referrerExtensionURL + '/www/index.html' + uriParams;
|
||||||
|
'Beam me down, Scotty!';
|
||||||
|
};
|
||||||
|
var response = confirm(message);
|
||||||
|
if (response) {
|
||||||
|
launchLocal();
|
||||||
|
} else {
|
||||||
|
setContentInjectionMode('serviceworker');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Because the "outer" Service Worker still runs in a PWA app, we don't actually disable the SW in this context, but it will no longer
|
||||||
|
// be intercepting requests
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
serviceWorkerRegistration = null;
|
||||||
}
|
}
|
||||||
refreshAPIStatus();
|
refreshAPIStatus();
|
||||||
// User has switched to jQuery mode, so no longer needs CACHE_NAME
|
// User has switched to jQuery mode, so no longer needs ASSETS_CACHE
|
||||||
// We should empty it to prevent unnecessary space usage
|
// We should empty it and turn it off to prevent unnecessary space usage
|
||||||
if ('caches' in window) caches.delete(CACHE_NAME);
|
if ('caches' in window && isMessageChannelAvailable()) {
|
||||||
|
var channel = new MessageChannel();
|
||||||
|
if (isServiceWorkerAvailable() && navigator.serviceWorker.controller) {
|
||||||
|
navigator.serviceWorker.controller.postMessage({
|
||||||
|
'action': { 'useCache': 'off' }
|
||||||
|
}, [channel.port2]);
|
||||||
|
}
|
||||||
|
caches.delete(ASSETS_CACHE);
|
||||||
|
}
|
||||||
} else if (value === 'serviceworker') {
|
} else if (value === 'serviceworker') {
|
||||||
if (!isServiceWorkerAvailable()) {
|
if (!isServiceWorkerAvailable()) {
|
||||||
alert("The ServiceWorker API is not available on your device. Falling back to JQuery mode");
|
alert("The ServiceWorker API is not available on your device. Falling back to JQuery mode");
|
||||||
@ -599,14 +708,21 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
|||||||
setContentInjectionMode('jquery');
|
setContentInjectionMode('jquery');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var protocol = window.location.protocol;
|
||||||
if (!isServiceWorkerReady()) {
|
if (!isServiceWorkerReady()) {
|
||||||
$('#serviceWorkerStatus').html("ServiceWorker API available : trying to register it...");
|
$('#serviceWorkerStatus').html("ServiceWorker API available : trying to register it...");
|
||||||
|
if (navigator.serviceWorker.controller) {
|
||||||
|
console.log("Active service worker found, no need to register");
|
||||||
|
serviceWorkerRegistration = true;
|
||||||
|
// Remove any jQuery hooks from a previous jQuery session
|
||||||
|
$('#articleContent').contents().remove();
|
||||||
|
// Create the MessageChannel and send 'init'
|
||||||
|
initOrKeepAliveServiceWorker();
|
||||||
|
refreshAPIStatus();
|
||||||
|
} else {
|
||||||
navigator.serviceWorker.register('../service-worker.js').then(function (reg) {
|
navigator.serviceWorker.register('../service-worker.js').then(function (reg) {
|
||||||
// The ServiceWorker is registered
|
// The ServiceWorker is registered
|
||||||
serviceWorkerRegistration = reg;
|
serviceWorkerRegistration = reg;
|
||||||
refreshAPIStatus();
|
|
||||||
|
|
||||||
// We need to wait for the ServiceWorker to be activated
|
// We need to wait for the ServiceWorker to be activated
|
||||||
// before sending the first init message
|
// before sending the first init message
|
||||||
var serviceWorker = reg.installing || reg.waiting || reg.active;
|
var serviceWorker = reg.installing || reg.waiting || reg.active;
|
||||||
@ -614,12 +730,12 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
|||||||
if (statechangeevent.target.state === 'activated') {
|
if (statechangeevent.target.state === 'activated') {
|
||||||
// Remove any jQuery hooks from a previous jQuery session
|
// Remove any jQuery hooks from a previous jQuery session
|
||||||
$('#articleContent').contents().remove();
|
$('#articleContent').contents().remove();
|
||||||
// Create the MessageChannel
|
// Create the MessageChannel and send the 'init' message to the ServiceWorker
|
||||||
// and send the 'init' message to the ServiceWorker
|
|
||||||
initOrKeepAliveServiceWorker();
|
initOrKeepAliveServiceWorker();
|
||||||
// We need to refresh cache status here on first activation because SW was inaccessible till now
|
// We need to refresh cache status here on first activation because SW was inaccessible till now
|
||||||
// We also initialize the CACHE_NAME constant in SW here
|
// We also initialize the ASSETS_CACHE constant in SW here
|
||||||
refreshCacheStatus();
|
refreshCacheStatus();
|
||||||
|
refreshAPIStatus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (serviceWorker.state === 'activated') {
|
if (serviceWorker.state === 'activated') {
|
||||||
@ -629,25 +745,25 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
|||||||
// in case it has been stopped and lost its context
|
// in case it has been stopped and lost its context
|
||||||
initOrKeepAliveServiceWorker();
|
initOrKeepAliveServiceWorker();
|
||||||
}
|
}
|
||||||
}, function (err) {
|
refreshCacheStatus();
|
||||||
console.error('error while registering serviceWorker', err);
|
refreshAPIStatus();
|
||||||
|
}).catch(function (err) {
|
||||||
|
if (protocol === 'moz-extension:') {
|
||||||
|
launchMozillaExtensionServiceWorker();
|
||||||
|
} else {
|
||||||
|
console.error('Error while registering serviceWorker', err);
|
||||||
refreshAPIStatus();
|
refreshAPIStatus();
|
||||||
var message = "The ServiceWorker could not be properly registered. Switching back to jQuery mode. Error message : " + err;
|
var message = "The ServiceWorker could not be properly registered. Switching back to jQuery mode. Error message : " + err;
|
||||||
var protocol = window.location.protocol;
|
if (protocol === 'file:') {
|
||||||
if (protocol === 'moz-extension:') {
|
|
||||||
message += "\n\nYou seem to be using kiwix-js through a Firefox extension : ServiceWorkers are disabled by Mozilla in extensions.";
|
|
||||||
message += "\nPlease vote for https://bugzilla.mozilla.org/show_bug.cgi?id=1344561 so that some future Firefox versions support it";
|
|
||||||
}
|
|
||||||
else if (protocol === 'file:') {
|
|
||||||
message += "\n\nYou seem to be opening kiwix-js with the file:// protocol. You should open it through a web server : either through a local one (http://localhost/...) or through a remote one (but you need SSL : https://webserver/...)";
|
message += "\n\nYou seem to be opening kiwix-js with the file:// protocol. You should open it through a web server : either through a local one (http://localhost/...) or through a remote one (but you need SSL : https://webserver/...)";
|
||||||
}
|
}
|
||||||
alert(message);
|
alert(message);
|
||||||
setContentInjectionMode("jquery");
|
setContentInjectionMode("jquery");
|
||||||
return;
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// We need to set this variable earlier else the ServiceWorker does not get reactivated
|
// We need to reactivate Service Worker
|
||||||
contentInjectionMode = value;
|
|
||||||
initOrKeepAliveServiceWorker();
|
initOrKeepAliveServiceWorker();
|
||||||
}
|
}
|
||||||
// User has switched to Service Worker mode, so no longer needs the memory cache
|
// User has switched to Service Worker mode, so no longer needs the memory cache
|
||||||
@ -656,26 +772,12 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
|||||||
}
|
}
|
||||||
$('input:radio[name=contentInjectionMode]').prop('checked', false);
|
$('input:radio[name=contentInjectionMode]').prop('checked', false);
|
||||||
$('input:radio[name=contentInjectionMode]').filter('[value="' + value + '"]').prop('checked', true);
|
$('input:radio[name=contentInjectionMode]').filter('[value="' + value + '"]').prop('checked', true);
|
||||||
contentInjectionMode = value;
|
|
||||||
// Save the value in the Settings Store, so that to be able to keep it after a reload/restart
|
// Save the value in the Settings Store, so that to be able to keep it after a reload/restart
|
||||||
settingsStore.setItem('lastContentInjectionMode', value, Infinity);
|
settingsStore.setItem('contentInjectionMode', value, Infinity);
|
||||||
refreshCacheStatus();
|
refreshCacheStatus();
|
||||||
|
refreshAPIStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
// At launch, we try to set the last content injection mode (stored in Settings Store)
|
|
||||||
var lastContentInjectionMode = settingsStore.getItem('lastContentInjectionMode');
|
|
||||||
if (lastContentInjectionMode) {
|
|
||||||
setContentInjectionMode(lastContentInjectionMode);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setContentInjectionMode('jquery');
|
|
||||||
}
|
|
||||||
|
|
||||||
var serviceWorkerRegistration = null;
|
|
||||||
|
|
||||||
// We need to establish the caching capabilities before first page launch
|
|
||||||
refreshCacheStatus();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tells if the ServiceWorker API is available
|
* Tells if the ServiceWorker API is available
|
||||||
* https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker
|
* https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker
|
||||||
@ -711,6 +813,69 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
|||||||
return (serviceWorkerRegistration);
|
return (serviceWorkerRegistration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function launchMozillaExtensionServiceWorker () {
|
||||||
|
// DEV: See explanation below for why we access localStorage directly here
|
||||||
|
var PWASuccessfullyLaunched = localStorage.getItem(params.keyPrefix + 'PWA_launch') === 'success';
|
||||||
|
var allowInternetAccess = settingsStore.getItem('allowInternetAccess') === 'true';
|
||||||
|
var message = 'To enable the Service Worker, we need one-time access to our secure server ' +
|
||||||
|
'so that the app can re-launch as a Progressive Web App (PWA).\n\n' +
|
||||||
|
'The PWA will be able to run offline, but will auto-update periodically when online ' +
|
||||||
|
'as per the Service Worker spec.\n\n' +
|
||||||
|
'You can switch back any time by returning to JQuery mode.\n\n' +
|
||||||
|
'WARNING: This will attempt to access the following server: \n' + params.PWAServer + '\n';
|
||||||
|
var launchPWA = function () {
|
||||||
|
uiUtil.spinnerDisplay(false);
|
||||||
|
var uriParams = '?contentInjectionMode=serviceworker&allowInternetAccess=true';
|
||||||
|
uriParams += '&referrerExtensionURL=' + encodeURIComponent(window.location.href.replace(/\/www\/index.html.*$/i, ''));
|
||||||
|
if (!PWASuccessfullyLaunched || !allowInternetAccess) {
|
||||||
|
// Add any further params that should only be passed when the user is intentionally switching to SW mode
|
||||||
|
uriParams += '&appTheme=' + params.appTheme;
|
||||||
|
uriParams += '&showUIAnimations=' + params.showUIAnimations;
|
||||||
|
}
|
||||||
|
settingsStore.setItem('contentInjectionMode', 'serviceworker', Infinity);
|
||||||
|
// This is needed so that we get passthrough on subsequent launches
|
||||||
|
settingsStore.setItem('allowInternetAccess', true, Infinity);
|
||||||
|
// Signal failure of PWA until it has successfully launched (in init.js it will be changed to 'success')
|
||||||
|
// DEV: We write directly to localStorage instead of using settingsStore here because we need 100% certainty
|
||||||
|
// regarding the location of the key to be able to retrieve it in init.js before settingsStore is initialized
|
||||||
|
localStorage.setItem(params.keyPrefix + 'PWA_launch', 'fail');
|
||||||
|
window.location.href = params.PWAServer + 'www/index.html' + uriParams;
|
||||||
|
'Beam me up, Scotty!';
|
||||||
|
};
|
||||||
|
var checkPWAIsOnline = function () {
|
||||||
|
uiUtil.spinnerDisplay(true, 'Checking server access...');
|
||||||
|
uiUtil.checkServerIsAccessible(params.PWAServer + 'www/img/icons/kiwix-32.png', launchPWA, function () {
|
||||||
|
uiUtil.spinnerDisplay(false);
|
||||||
|
alert('The server is not currently accessible! ' +
|
||||||
|
'\n\n(Kiwix needs one-time access to the server to cache the PWA).' +
|
||||||
|
'\nPlease try again when you have a stable Internet connection.', 'Error!');
|
||||||
|
settingsStore.setItem('allowInternetAccess', false, Infinity);
|
||||||
|
setContentInjectionMode('jquery');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var response;
|
||||||
|
if (settingsStore.getItem('allowInternetAccess') === 'true') {
|
||||||
|
if (PWASuccessfullyLaunched) {
|
||||||
|
checkPWAIsOnline();
|
||||||
|
} else {
|
||||||
|
response = confirm('The last attempt to launch the PWA appears to have failed.\n\nDo you wish to try again?');
|
||||||
|
if (response) {
|
||||||
|
checkPWAIsOnline();
|
||||||
|
} else {
|
||||||
|
settingsStore.setItem('allowInternetAccess', false, Infinity);
|
||||||
|
setContentInjectionMode('jquery');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response = confirm(message);
|
||||||
|
if (response) checkPWAIsOnline();
|
||||||
|
else {
|
||||||
|
setContentInjectionMode('jquery');
|
||||||
|
settingsStore.setItem('allowInternetAccess', false, Infinity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type Array.<StorageFirefoxOS>
|
* @type Array.<StorageFirefoxOS>
|
||||||
@ -1169,7 +1334,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
|||||||
// but we should not do this when opening the landing page (or else one of the Unit Tests fails, at least on Chrome 58)
|
// but we should not do this when opening the landing page (or else one of the Unit Tests fails, at least on Chrome 58)
|
||||||
if (!params.isLandingPage) document.getElementById('articleContent').contentWindow.focus();
|
if (!params.isLandingPage) document.getElementById('articleContent').contentWindow.focus();
|
||||||
|
|
||||||
if (contentInjectionMode === 'serviceworker') {
|
if (params.contentInjectionMode === 'serviceworker') {
|
||||||
// In ServiceWorker mode, we simply set the iframe src.
|
// In ServiceWorker mode, we simply set the iframe src.
|
||||||
// (reading the backend is handled by the ServiceWorker itself)
|
// (reading the backend is handled by the ServiceWorker itself)
|
||||||
|
|
||||||
@ -1220,7 +1385,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We put the ZIM filename as a prefix in the URL, so that browser caches are separate for each ZIM file
|
// We put the ZIM filename as a prefix in the URL, so that browser caches are separate for each ZIM file
|
||||||
iframeArticleContent.src = "../" + selectedArchive._file._files[0].name + "/" + dirEntry.namespace + "/" + encodedUrl;
|
iframeArticleContent.src = "../" + selectedArchive._file.name + "/" + dirEntry.namespace + "/" + encodedUrl;
|
||||||
} else {
|
} else {
|
||||||
// In jQuery mode, we read the article content in the backend and manually insert it in the iframe
|
// In jQuery mode, we read the article content in the backend and manually insert it in the iframe
|
||||||
if (dirEntry.isRedirect()) {
|
if (dirEntry.isRedirect()) {
|
||||||
|
@ -30,6 +30,18 @@
|
|||||||
*/
|
*/
|
||||||
var params = {};
|
var params = {};
|
||||||
|
|
||||||
|
// The key prefix used by the settingsStore.js (see comment there for explanation), but we also need it below
|
||||||
|
params['keyPrefix'] = 'kiwixjs-'
|
||||||
|
|
||||||
|
// The following lines check the querystring for a communication from the PWA indicating it has successfully launched.
|
||||||
|
// If this querystring is received, then the app will set a success key in the extension's localStorage and then halt further processing.
|
||||||
|
// This is used to prevent a "boot loop" where the app will keep jumping to a failed install of the PWA.
|
||||||
|
if (/PWA_launch=/.test(window.location.search)) {
|
||||||
|
var match = /PWA_launch=([^&]+)/.exec(window.location.search);
|
||||||
|
localStorage.setItem(params.keyPrefix + 'PWA_launch', match[1]);
|
||||||
|
console.warn('Launch of PWA has been registered as "' + match[1] + '" by the extension. Exiting local code.');
|
||||||
|
} else {
|
||||||
|
|
||||||
require.config({
|
require.config({
|
||||||
baseUrl: 'js/lib',
|
baseUrl: 'js/lib',
|
||||||
paths: {
|
paths: {
|
||||||
@ -77,3 +89,5 @@ var webpMachine = false;
|
|||||||
webpMachine = true;
|
webpMachine = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
}
|
@ -34,15 +34,24 @@ define([], function () {
|
|||||||
*/
|
*/
|
||||||
var regexpCookieKeysToMigrate = new RegExp([
|
var regexpCookieKeysToMigrate = new RegExp([
|
||||||
'hideActiveContentWarning', 'showUIAnimations', 'appTheme', 'useCache',
|
'hideActiveContentWarning', 'showUIAnimations', 'appTheme', 'useCache',
|
||||||
'lastContentInjectionMode', 'listOfArchives', 'lastSelectedArchive'
|
'contentInjectionMode', 'listOfArchives', 'lastSelectedArchive'
|
||||||
].join('|'));
|
].join('|'));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A constant to set the prefix that will be added to keys when stored in localStorage: this is used to prevent
|
* A list of deprecated keys that should be removed. Add any further keys to the list of strings separated by a comma.
|
||||||
|
* @type {Array}
|
||||||
|
*/
|
||||||
|
var deprecatedKeys = [
|
||||||
|
'lastContentInjectionMode'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The prefix that will be added to keys when stored in localStorage: this is used to prevent
|
||||||
* potential collision of key names with localStorage keys used by code inside ZIM archives
|
* potential collision of key names with localStorage keys used by code inside ZIM archives
|
||||||
|
* It is set in init.js because it is needed early in app loading
|
||||||
* @type {String}
|
* @type {String}
|
||||||
*/
|
*/
|
||||||
const keyPrefix = 'kiwixjs-';
|
var keyPrefix = params.keyPrefix;
|
||||||
|
|
||||||
// Tests for available Storage APIs (document.cookie or localStorage) and returns the best available of these
|
// Tests for available Storage APIs (document.cookie or localStorage) and returns the best available of these
|
||||||
function getBestAvailableStorageAPI() {
|
function getBestAvailableStorageAPI() {
|
||||||
@ -72,6 +81,11 @@ define([], function () {
|
|||||||
// If both cookies and localStorage are supported, and document.cookie contains keys to migrate,
|
// If both cookies and localStorage are supported, and document.cookie contains keys to migrate,
|
||||||
// migrate settings to use localStorage
|
// migrate settings to use localStorage
|
||||||
if (kiwixCookieTest && localStorageTest && regexpCookieKeysToMigrate.test(document.cookie)) _migrateStorageSettings();
|
if (kiwixCookieTest && localStorageTest && regexpCookieKeysToMigrate.test(document.cookie)) _migrateStorageSettings();
|
||||||
|
// Remove any deprecated keys
|
||||||
|
deprecatedKeys.forEach(function (key) {
|
||||||
|
if (localStorageTest) localStorage.removeItem(keyPrefix + key);
|
||||||
|
settingsStore.removeItem(key); // Because this runs before we have returned a store type, this will remove from cookie too
|
||||||
|
});
|
||||||
// Note that if this function returns 'none', the cookie implementations below will run anyway. This is because storing a cookie
|
// Note that if this function returns 'none', the cookie implementations below will run anyway. This is because storing a cookie
|
||||||
// does not cause an exception even if cookies are blocked in some contexts, whereas accessing localStorage may cause an exception
|
// does not cause an exception even if cookies are blocked in some contexts, whereas accessing localStorage may cause an exception
|
||||||
return type;
|
return type;
|
||||||
|
@ -225,6 +225,76 @@ define(rqDef, function() {
|
|||||||
$("#searchingArticles").hide();
|
$("#searchingArticles").hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for update of Service Worker (PWA) and display information to user
|
||||||
|
*/
|
||||||
|
var updateAlert = document.getElementById('updateAlert');
|
||||||
|
function checkUpdateStatus(appstate) {
|
||||||
|
if ('serviceWorker' in navigator && !appstate.pwaUpdateNeeded) {
|
||||||
|
// Create a Message Channel
|
||||||
|
var channel = new MessageChannel();
|
||||||
|
// Handler for receiving message reply from service worker
|
||||||
|
channel.port1.onmessage = function (event) {
|
||||||
|
var cacheNames = event.data;
|
||||||
|
if (cacheNames.error) return;
|
||||||
|
else {
|
||||||
|
caches.keys().then(function (keyList) {
|
||||||
|
updateAlert.style.display = 'none';
|
||||||
|
var cachePrefix = cacheNames.app.replace(/^([^\d]+).+/, '$1');
|
||||||
|
keyList.forEach(function (key) {
|
||||||
|
if (key === cacheNames.app || key === cacheNames.assets) return;
|
||||||
|
// Ignore any keys that do not begin with the appCache prefix (they could be from other apps using the same domain)
|
||||||
|
if (key.indexOf(cachePrefix)) return;
|
||||||
|
// If we get here, then there is a cache key that does not match our version, i.e. a PWA-in-waiting
|
||||||
|
appstate.pwaUpdateNeeded = true;
|
||||||
|
updateAlert.style.display = 'block';
|
||||||
|
document.getElementById('persistentMessage').innerHTML = 'Version ' + key.replace(cachePrefix, '') +
|
||||||
|
' is ready to install. (Re-launch app to install.)';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (navigator.serviceWorker.controller) navigator.serviceWorker.controller.postMessage({
|
||||||
|
action: 'getCacheNames'
|
||||||
|
}, [channel.port2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updateAlert) updateAlert.querySelector('button[data-hide]').addEventListener('click', function () {
|
||||||
|
updateAlert.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a server is accessible by attempting to load a test image from the server
|
||||||
|
* @param {String} imageSrc The full URI of the image
|
||||||
|
* @param {any} onSuccess A function to call if the image can be loaded
|
||||||
|
* @param {any} onError A function to call if the image cannot be loaded
|
||||||
|
*/
|
||||||
|
function checkServerIsAccessible(imageSrc, onSuccess, onError) {
|
||||||
|
var image = new Image();
|
||||||
|
image.onload = onSuccess;
|
||||||
|
image.onerror = onError;
|
||||||
|
image.src = imageSrc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show or hide the spinner together with a message
|
||||||
|
* @param {Boolean} show True to show the spinner, false to hide it
|
||||||
|
* @param {String} message A message to display, or hide the message if null
|
||||||
|
*/
|
||||||
|
function spinnerDisplay(show, message) {
|
||||||
|
var searchingArticles = document.getElementById('searchingArticles');
|
||||||
|
var spinnerMessage = document.getElementById('cachingAssets');
|
||||||
|
if (show) searchingArticles.style.display = 'block';
|
||||||
|
else searchingArticles.style.display = 'none';
|
||||||
|
if (message) {
|
||||||
|
spinnerMessage.innerHTML = message;
|
||||||
|
spinnerMessage.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
spinnerMessage.innerHTML = 'Caching assets...';
|
||||||
|
spinnerMessage.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether an element is partially or fully inside the current viewport
|
* Checks whether an element is partially or fully inside the current viewport
|
||||||
*
|
*
|
||||||
@ -402,7 +472,7 @@ define(rqDef, function() {
|
|||||||
}
|
}
|
||||||
// If we are in Config and a real document has been loaded already, expose return link so user can see the result of the change
|
// If we are in Config and a real document has been loaded already, expose return link so user can see the result of the change
|
||||||
// DEV: The Placeholder string below matches the dummy article.html that is loaded before any articles are loaded
|
// DEV: The Placeholder string below matches the dummy article.html that is loaded before any articles are loaded
|
||||||
if (document.getElementById('liConfigureNav').classList.contains('active') &&
|
if (document.getElementById('liConfigureNav').classList.contains('active') && doc &&
|
||||||
doc.title !== "Placeholder for injecting an article into the iframe") {
|
doc.title !== "Placeholder for injecting an article into the iframe") {
|
||||||
showReturnLink();
|
showReturnLink();
|
||||||
}
|
}
|
||||||
@ -453,6 +523,9 @@ define(rqDef, function() {
|
|||||||
removeUrlParameters: removeUrlParameters,
|
removeUrlParameters: removeUrlParameters,
|
||||||
displayActiveContentWarning: displayActiveContentWarning,
|
displayActiveContentWarning: displayActiveContentWarning,
|
||||||
displayFileDownloadAlert: displayFileDownloadAlert,
|
displayFileDownloadAlert: displayFileDownloadAlert,
|
||||||
|
checkUpdateStatus: checkUpdateStatus,
|
||||||
|
checkServerIsAccessible: checkServerIsAccessible,
|
||||||
|
spinnerDisplay: spinnerDisplay,
|
||||||
isElementInView: isElementInView,
|
isElementInView: isElementInView,
|
||||||
htmlEscapeChars: htmlEscapeChars,
|
htmlEscapeChars: htmlEscapeChars,
|
||||||
removeAnimationClasses: removeAnimationClasses,
|
removeAnimationClasses: removeAnimationClasses,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user