mirror of
https://github.com/kiwix/kiwix-js.git
synced 2025-08-03 11:16:38 -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
|
||||
- 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
|
||||
- uses: actions/setup-node@v2
|
||||
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>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<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>
|
||||
|
||||
</html>
|
@ -32,6 +32,8 @@
|
||||
"id": "kiwix-html5-unlisted@kiwix.org"
|
||||
}
|
||||
},
|
||||
|
||||
"web_accessible_resources": ["www/index.html"],
|
||||
|
||||
"background": {
|
||||
"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
|
||||
fi
|
||||
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
|
||||
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';
|
||||
|
||||
/**
|
||||
* The name of the Cache API cache in which assets defined in regexpCachedContentTypes will be stored
|
||||
* The value is defined in app.js and will be passed to Service Worker on initialization (to avoid duplication)
|
||||
* @type {String}
|
||||
* App version number - ENSURE IT MATCHES VALUE IN app.js
|
||||
* DEV: Changing this will cause the browser to recognize that the Service Worker has changed, and it will
|
||||
* 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
|
||||
* @type {Boolean}
|
||||
*/
|
||||
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 '|'
|
||||
* @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
|
||||
* @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
|
||||
* In our case, there is also the ZIM file name used as a prefix in the URL
|
||||
* @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) {
|
||||
// "Claiming" the ServiceWorker is necessary to make it work right away,
|
||||
// without the need to reload the page.
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/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;
|
||||
var fetchCaptureEnabled = false;
|
||||
let outgoingMessagePort = null;
|
||||
let fetchCaptureEnabled = false;
|
||||
|
||||
self.addEventListener('fetch', function (event) {
|
||||
if (fetchCaptureEnabled &&
|
||||
regexpZIMUrlWithNamespace.test(event.request.url) &&
|
||||
event.request.method === "GET") {
|
||||
|
||||
// The ServiceWorker will handle this request either from CACHE_NAME or from app.js
|
||||
|
||||
event.respondWith(
|
||||
// First see if the content is in the cache
|
||||
fromCache(event.request).then(
|
||||
function (response) {
|
||||
// The response was found in the cache so we respond with it
|
||||
// Only cache GET requests
|
||||
if (event.request.method !== "GET") return;
|
||||
// Remove any querystring before requesting from the cache
|
||||
var rqUrl = event.request.url.replace(/\?[^?]+$/i, '');
|
||||
// Select cache depending on request format
|
||||
var cache = /\.zim\//i.test(rqUrl) ? ASSETS_CACHE : APP_CACHE;
|
||||
if (cache === ASSETS_CACHE && !fetchCaptureEnabled) return;
|
||||
event.respondWith(
|
||||
// First see if the content is in the cache
|
||||
fromCache(cache, rqUrl).then(function (response) {
|
||||
// The response was found in the cache so we respond with it
|
||||
return response;
|
||||
}, function () {
|
||||
// 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)
|
||||
if (cache === ASSETS_CACHE && regexpZIMUrlWithNamespace.test(rqUrl)) {
|
||||
return fetchRequestFromZIM(event).then(function (response) {
|
||||
// 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')) &&
|
||||
!regexpExcludedURLSchema.test(event.request.url)) {
|
||||
event.waitUntil(updateCache(ASSETS_CACHE, event.request, response.clone()));
|
||||
}
|
||||
return response;
|
||||
},
|
||||
function () {
|
||||
// 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)
|
||||
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
|
||||
if (regexpCachedContentTypes.test(response.headers.get('Content-Type')) &&
|
||||
!regexpExcludedURLSchema.test(event.request.url)) {
|
||||
event.waitUntil(updateCache(event.request, response.clone()));
|
||||
}
|
||||
return response;
|
||||
}).catch(function (msgPortData, title) {
|
||||
console.error('Invalid message received from app.js for ' + title, msgPortData);
|
||||
return msgPortData;
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
// 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.
|
||||
}).catch(function (msgPortData, title) {
|
||||
console.error('Invalid message received from app.js for ' + title, 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);
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('message', function (event) {
|
||||
@ -123,13 +234,15 @@ self.addEventListener('message', function (event) {
|
||||
if (event.data.action.useCache) {
|
||||
// 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';
|
||||
if (useCache) CACHE_NAME = event.data.cacheName;
|
||||
console.log('[SW] Caching was turned ' + event.data.action.useCache);
|
||||
console.debug('[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) {
|
||||
// Checks and returns the caching strategy: checkCache key should contain a sample URL string to test
|
||||
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
|
||||
* @param {Request} request The Request to fulfill from CACHE_NAME
|
||||
* Looks up a Request in a cache and returns a Promise for the matched Response
|
||||
* @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'
|
||||
*/
|
||||
function fromCache(request) {
|
||||
function fromCache(cache, requestUrl) {
|
||||
// Prevents use of Cache API if user has disabled it
|
||||
if (!useCache) return Promise.reject('disabled');
|
||||
return caches.open(CACHE_NAME).then(function (cache) {
|
||||
return cache.match(request).then(function (matching) {
|
||||
if (!useCache && cache === ASSETS_CACHE) return Promise.reject('disabled');
|
||||
return caches.open(cache).then(function (cacheObj) {
|
||||
return cacheObj.match(requestUrl).then(function (matching) {
|
||||
if (!matching || matching.status === 404) {
|
||||
return Promise.reject('no-match');
|
||||
}
|
||||
console.log('[SW] Supplying ' + request.url + ' from ' + CACHE_NAME + '...');
|
||||
console.debug('[SW] Supplying ' + requestUrl + ' from ' + cache + '...');
|
||||
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 {Response} response The Response received from the server/ZIM
|
||||
* @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
|
||||
if (!useCache) return Promise.resolve();
|
||||
return caches.open(CACHE_NAME).then(function (cache) {
|
||||
console.log('[SW] Adding ' + request.url + ' to ' + CACHE_NAME + '...');
|
||||
return cache.put(request, response);
|
||||
if (!useCache && cache === ASSETS_CACHE) return Promise.resolve();
|
||||
return caches.open(cache).then(function (cacheObj) {
|
||||
console.debug('[SW] Adding ' + request.url + ' to ' + cache + '...');
|
||||
return cacheObj.put(request, response);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns {Promise<Array>} A Promise for an array of format [cacheType, cacheDescription, assetCount]
|
||||
*/
|
||||
function testCacheAndCountAssets(url) {
|
||||
if (regexpExcludedURLSchema.test(url)) return Promise.resolve(['custom', 'Custom', '-']);
|
||||
if (!useCache) return Promise.resolve(['none', 'None', 0]);
|
||||
return caches.open(CACHE_NAME).then(function (cache) {
|
||||
if (regexpExcludedURLSchema.test(url)) return Promise.resolve(['custom', 'custom', 'Custom', '-']);
|
||||
if (!useCache) return Promise.resolve(['none', 'none', 'None', 0]);
|
||||
return caches.open(ASSETS_CACHE).then(function (cache) {
|
||||
return cache.keys().then(function (keys) {
|
||||
return ['cacheAPI', 'Cache API', keys.length];
|
||||
return ['cacheAPI', ASSETS_CACHE, 'Cache API', keys.length];
|
||||
}).catch(function(err) {
|
||||
return err;
|
||||
});
|
||||
|
@ -55,7 +55,7 @@
|
||||
<section id="search-article" role="region">
|
||||
<header id="top">
|
||||
<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 -->
|
||||
<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,
|
||||
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>,
|
||||
or certain browser extensions). Unfortunately, this mode is not currently 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
|
||||
cannot run with the <code>file:</code> protocol (but only IE11 and old Edge allow the app to run by launching
|
||||
<code>index.html</code> from the file system).
|
||||
or certain browser extensions). While this mode is not natively supported in Mozilla (Firefox) browser
|
||||
extensions, we provide a functional workaround by re-launching the extension as a Progressive Web App (PWA).
|
||||
Note that this mode cannot run with the <code>file:</code> protocol (but only IE11 and old Edge allow the app to run
|
||||
by launching <code>index.html</code> from the file system).
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
@ -403,6 +403,11 @@
|
||||
<div id="configuration" style="display: none;">
|
||||
<div class="container">
|
||||
<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
|
||||
instructions, please see the About section</p>
|
||||
<div id="selectorsDisplay" style="display: none;">
|
||||
@ -582,7 +587,7 @@
|
||||
</div>
|
||||
<!-- Bootstrap alert box -->
|
||||
<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>
|
||||
<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
|
||||
@ -595,7 +600,7 @@
|
||||
<footer>
|
||||
<!-- Bootstrap alert box -->
|
||||
<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>
|
||||
<span id="alertMessage"></span>
|
||||
</div>
|
||||
|
359
www/js/app.js
359
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
|
||||
* This name will be passed to service-worker.js in messaging to avoid duplication: see comment in service-worker.js
|
||||
* We need access to this constant in app.js in order to complete utility actions when Service Worker is not initialized
|
||||
* We need access to the cache name in app.js in order to complete utility actions when Service Worker is not initialized,
|
||||
* so we have to duplicate it here
|
||||
* @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
|
||||
@ -64,34 +65,101 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
||||
*/
|
||||
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
|
||||
params['storeType'] = settingsStore.getBestAvailableStorageAPI(); // A parameter to determine the Settings Store API in use
|
||||
/**
|
||||
* Set parameters from the Settings Store, together with any defaults
|
||||
* 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['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)
|
||||
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)
|
||||
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')
|
||||
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
|
||||
params['useHomeKeyToFocusSearchBar'] = settingsStore.getItem('useHomeKeyToFocusSearchBar') === 'true';
|
||||
document.getElementById('useHomeKeyToFocusSearchBarCheck').checked = params.useHomeKeyToFocusSearchBar;
|
||||
switchHomeKeyToFocusSearchBar();
|
||||
// A parameter to access the URL of any extension that this app was launched from
|
||||
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)
|
||||
appstate['search'] = {
|
||||
'prefix': '', // A field to hold the original search string
|
||||
'status': '', // The status of the search: ''|'init'|'interim'|'cancelled'|'complete'
|
||||
'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)
|
||||
var globalDropZone = document.getElementById('search-article');
|
||||
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
|
||||
if (appstate.search.prefix === prefix && !/^(cancelled|complete)$/.test(appstate.search.status)) return;
|
||||
$("#welcomeText").hide();
|
||||
$('.alert').hide();
|
||||
$('.kiwix-alert').hide();
|
||||
$("#searchingArticles").show();
|
||||
pushBrowserHistoryState(null, prefix);
|
||||
// Initiate the search
|
||||
@ -306,9 +374,10 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
||||
$('#formArticleSearch').hide();
|
||||
$("#welcomeText").hide();
|
||||
$("#searchingArticles").hide();
|
||||
$('.alert').hide();
|
||||
$('.kiwix-alert').hide();
|
||||
refreshAPIStatus();
|
||||
refreshCacheStatus();
|
||||
uiUtil.checkUpdateStatus(appstate);
|
||||
// Use a timeout of 400ms because uiUtil.applyAnimationToSection uses a timeout of 300ms
|
||||
setTimeout(resizeIFrame, 400);
|
||||
return false;
|
||||
@ -333,7 +402,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
||||
$("#welcomeText").hide();
|
||||
$('#articleListWithHeader').hide();
|
||||
$("#searchingArticles").hide();
|
||||
$('.alert').hide();
|
||||
$('.kiwix-alert').hide();
|
||||
// Use a timeout of 400ms because uiUtil.applyAnimationToSection uses a timeout of 300ms
|
||||
setTimeout(resizeIFrame, 400);
|
||||
return false;
|
||||
@ -373,7 +442,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
||||
params.useCache = false;
|
||||
// Delete all caches
|
||||
resetCssCache();
|
||||
if ('caches' in window) caches.delete(CACHE_NAME);
|
||||
if ('caches' in window) caches.delete(ASSETS_CACHE);
|
||||
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
|
||||
* @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) {
|
||||
if (contentInjectionMode === 'serviceworker') {
|
||||
if (params.contentInjectionMode === 'serviceworker' && navigator.serviceWorker && navigator.serviceWorker.controller) {
|
||||
// Create a Message Channel
|
||||
var channel = new MessageChannel();
|
||||
// Handler for recieving message reply from service worker
|
||||
@ -507,13 +576,13 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
||||
'action': {
|
||||
'useCache': params.useCache ? 'on' : 'off',
|
||||
'checkCache': window.location.href
|
||||
},
|
||||
'cacheName': CACHE_NAME
|
||||
}
|
||||
}, [channel.port2]);
|
||||
} else {
|
||||
// No Service Worker has been established, so we resolve the Promise with cssCache details only
|
||||
resolve({
|
||||
'type': params.useCache ? 'memory' : 'none',
|
||||
'name': 'cssCache',
|
||||
'description': params.useCache ? 'Memory' : 'None',
|
||||
'count': cssCache.size
|
||||
});
|
||||
@ -528,7 +597,10 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
||||
// Update radio buttons and checkbox
|
||||
document.getElementById('cachedAssetsModeRadio' + (params.useCache ? 'True' : 'False')).checked = true;
|
||||
// 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('assetsCount').innerHTML = cache.count;
|
||||
var cacheSettings = document.getElementById('performanceSettingsDiv');
|
||||
@ -543,8 +615,8 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
||||
});
|
||||
}
|
||||
|
||||
var contentInjectionMode;
|
||||
var keepAliveServiceWorkerHandle;
|
||||
var serviceWorkerRegistration;
|
||||
|
||||
/**
|
||||
* Send an 'init' message to the ServiceWorker with a new MessageChannel
|
||||
@ -553,20 +625,31 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
||||
* and the application
|
||||
*/
|
||||
function initOrKeepAliveServiceWorker() {
|
||||
if (contentInjectionMode === 'serviceworker') {
|
||||
var delay = DELAY_BETWEEN_KEEPALIVE_SERVICEWORKER;
|
||||
if (params.contentInjectionMode === 'serviceworker') {
|
||||
// Create a new messageChannel
|
||||
var tmpMessageChannel = new MessageChannel();
|
||||
tmpMessageChannel.port1.onmessage = handleMessageChannelMessage;
|
||||
// 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;
|
||||
// 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
|
||||
clearTimeout(keepAliveServiceWorkerHandle);
|
||||
keepAliveServiceWorkerHandle = setTimeout(initOrKeepAliveServiceWorker, DELAY_BETWEEN_KEEPALIVE_SERVICEWORKER, false);
|
||||
keepAliveServiceWorkerHandle = setTimeout(initOrKeepAliveServiceWorker, delay, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the given injection mode.
|
||||
* This involves registering (or re-enabling) the Service Worker if necessary
|
||||
@ -575,19 +658,45 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
||||
* @param {String} value The chosen content injection mode : 'jquery' or 'serviceworker'
|
||||
*/
|
||||
function setContentInjectionMode(value) {
|
||||
params.contentInjectionMode = value;
|
||||
if (value === 'jquery') {
|
||||
if (isServiceWorkerReady()) {
|
||||
// We need to disable the ServiceWorker
|
||||
// Unregistering it does not seem to work as expected : the ServiceWorker
|
||||
// is indeed unregistered but still active...
|
||||
// So we have to disable it manually (even if it's still registered and active)
|
||||
navigator.serviceWorker.controller.postMessage({'action': 'disable'});
|
||||
messageChannel = null;
|
||||
if (params.referrerExtensionURL) {
|
||||
// We are in an extension, and the user may wish to revert to local code
|
||||
var message = 'This will switch to using locally packaged code only. Some configuration settings may be lost.\n\n' +
|
||||
'WARNING: After this, you may not be able to switch back to SW mode without an online connection!';
|
||||
var launchLocal = function () {
|
||||
settingsStore.setItem('allowInternetAccess', false, Infinity);
|
||||
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();
|
||||
// User has switched to jQuery mode, so no longer needs CACHE_NAME
|
||||
// We should empty it to prevent unnecessary space usage
|
||||
if ('caches' in window) caches.delete(CACHE_NAME);
|
||||
// User has switched to jQuery mode, so no longer needs ASSETS_CACHE
|
||||
// We should empty it and turn it off to prevent unnecessary space usage
|
||||
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') {
|
||||
if (!isServiceWorkerAvailable()) {
|
||||
alert("The ServiceWorker API is not available on your device. Falling back to JQuery mode");
|
||||
@ -599,55 +708,62 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
||||
setContentInjectionMode('jquery');
|
||||
return;
|
||||
}
|
||||
|
||||
var protocol = window.location.protocol;
|
||||
if (!isServiceWorkerReady()) {
|
||||
$('#serviceWorkerStatus').html("ServiceWorker API available : trying to register it...");
|
||||
navigator.serviceWorker.register('../service-worker.js').then(function (reg) {
|
||||
// The ServiceWorker is registered
|
||||
serviceWorkerRegistration = reg;
|
||||
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();
|
||||
|
||||
// We need to wait for the ServiceWorker to be activated
|
||||
// before sending the first init message
|
||||
var serviceWorker = reg.installing || reg.waiting || reg.active;
|
||||
serviceWorker.addEventListener('statechange', function(statechangeevent) {
|
||||
if (statechangeevent.target.state === 'activated') {
|
||||
// Remove any jQuery hooks from a previous jQuery session
|
||||
$('#articleContent').contents().remove();
|
||||
// Create the MessageChannel
|
||||
} else {
|
||||
navigator.serviceWorker.register('../service-worker.js').then(function (reg) {
|
||||
// The ServiceWorker is registered
|
||||
serviceWorkerRegistration = reg;
|
||||
// We need to wait for the ServiceWorker to be activated
|
||||
// before sending the first init message
|
||||
var serviceWorker = reg.installing || reg.waiting || reg.active;
|
||||
serviceWorker.addEventListener('statechange', function(statechangeevent) {
|
||||
if (statechangeevent.target.state === 'activated') {
|
||||
// Remove any jQuery hooks from a previous jQuery session
|
||||
$('#articleContent').contents().remove();
|
||||
// Create the MessageChannel and send the 'init' message to the ServiceWorker
|
||||
initOrKeepAliveServiceWorker();
|
||||
// We need to refresh cache status here on first activation because SW was inaccessible till now
|
||||
// We also initialize the ASSETS_CACHE constant in SW here
|
||||
refreshCacheStatus();
|
||||
refreshAPIStatus();
|
||||
}
|
||||
});
|
||||
if (serviceWorker.state === 'activated') {
|
||||
// Even if the ServiceWorker is already activated,
|
||||
// We need to re-create the MessageChannel
|
||||
// and send the 'init' message to the ServiceWorker
|
||||
// in case it has been stopped and lost its context
|
||||
initOrKeepAliveServiceWorker();
|
||||
// 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
|
||||
refreshCacheStatus();
|
||||
}
|
||||
refreshCacheStatus();
|
||||
refreshAPIStatus();
|
||||
}).catch(function (err) {
|
||||
if (protocol === 'moz-extension:') {
|
||||
launchMozillaExtensionServiceWorker();
|
||||
} else {
|
||||
console.error('Error while registering serviceWorker', err);
|
||||
refreshAPIStatus();
|
||||
var message = "The ServiceWorker could not be properly registered. Switching back to jQuery mode. Error message : " + err;
|
||||
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/...)";
|
||||
}
|
||||
alert(message);
|
||||
setContentInjectionMode("jquery");
|
||||
}
|
||||
});
|
||||
if (serviceWorker.state === 'activated') {
|
||||
// Even if the ServiceWorker is already activated,
|
||||
// We need to re-create the MessageChannel
|
||||
// and send the 'init' message to the ServiceWorker
|
||||
// in case it has been stopped and lost its context
|
||||
initOrKeepAliveServiceWorker();
|
||||
}
|
||||
}, function (err) {
|
||||
console.error('error while registering serviceWorker', err);
|
||||
refreshAPIStatus();
|
||||
var message = "The ServiceWorker could not be properly registered. Switching back to jQuery mode. Error message : " + err;
|
||||
var protocol = window.location.protocol;
|
||||
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/...)";
|
||||
}
|
||||
alert(message);
|
||||
setContentInjectionMode("jquery");
|
||||
return;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// We need to set this variable earlier else the ServiceWorker does not get reactivated
|
||||
contentInjectionMode = value;
|
||||
// We need to reactivate Service Worker
|
||||
initOrKeepAliveServiceWorker();
|
||||
}
|
||||
// User has switched to Service Worker mode, so no longer needs the memory cache
|
||||
@ -656,25 +772,11 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
||||
}
|
||||
$('input:radio[name=contentInjectionMode]').prop('checked', false);
|
||||
$('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
|
||||
settingsStore.setItem('lastContentInjectionMode', value, Infinity);
|
||||
settingsStore.setItem('contentInjectionMode', value, Infinity);
|
||||
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
|
||||
@ -710,6 +812,69 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
||||
// Return true if the serviceWorkerRegistration is not null and not undefined
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
@ -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)
|
||||
if (!params.isLandingPage) document.getElementById('articleContent').contentWindow.focus();
|
||||
|
||||
if (contentInjectionMode === 'serviceworker') {
|
||||
if (params.contentInjectionMode === 'serviceworker') {
|
||||
// In ServiceWorker mode, we simply set the iframe src.
|
||||
// (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
|
||||
iframeArticleContent.src = "../" + selectedArchive._file._files[0].name + "/" + dirEntry.namespace + "/" + encodedUrl;
|
||||
iframeArticleContent.src = "../" + selectedArchive._file.name + "/" + dirEntry.namespace + "/" + encodedUrl;
|
||||
} else {
|
||||
// In jQuery mode, we read the article content in the backend and manually insert it in the iframe
|
||||
if (dirEntry.isRedirect()) {
|
||||
|
@ -30,50 +30,64 @@
|
||||
*/
|
||||
var params = {};
|
||||
|
||||
require.config({
|
||||
baseUrl: 'js/lib',
|
||||
paths: {
|
||||
'jquery': 'jquery-3.2.1.slim',
|
||||
'bootstrap': 'bootstrap.bundle',
|
||||
'webpHeroBundle': 'webpHeroBundle_0.0.0-dev.27',
|
||||
'fontawesome': 'fontawesome/fontawesome',
|
||||
'fontawesome-solid': 'fontawesome/solid'
|
||||
},
|
||||
shim: {
|
||||
'jquery': {
|
||||
exports: '$'
|
||||
// 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({
|
||||
baseUrl: 'js/lib',
|
||||
paths: {
|
||||
'jquery': 'jquery-3.2.1.slim',
|
||||
'bootstrap': 'bootstrap.bundle',
|
||||
'webpHeroBundle': 'webpHeroBundle_0.0.0-dev.27',
|
||||
'fontawesome': 'fontawesome/fontawesome',
|
||||
'fontawesome-solid': 'fontawesome/solid'
|
||||
},
|
||||
'bootstrap': {
|
||||
deps: ['jquery', 'fontawesome', 'fontawesome-solid']
|
||||
},
|
||||
'webpHeroBundle': ''
|
||||
}
|
||||
});
|
||||
shim: {
|
||||
'jquery': {
|
||||
exports: '$'
|
||||
},
|
||||
'bootstrap': {
|
||||
deps: ['jquery', 'fontawesome', 'fontawesome-solid']
|
||||
},
|
||||
'webpHeroBundle': ''
|
||||
}
|
||||
});
|
||||
|
||||
var req = ['bootstrap']; // Baseline Require array
|
||||
var req = ['bootstrap']; // Baseline Require array
|
||||
|
||||
// Add polyfills to the Require array only if needed
|
||||
if (!('Promise' in self)) req.push('promisePolyfill');
|
||||
if (!('from' in Array)) req.push('arrayFromPolyfill');
|
||||
// Add polyfills to the Require array only if needed
|
||||
if (!('Promise' in self)) req.push('promisePolyfill');
|
||||
if (!('from' in Array)) req.push('arrayFromPolyfill');
|
||||
|
||||
requirejs(req, function () {
|
||||
requirejs(['../app']);
|
||||
});
|
||||
requirejs(req, function () {
|
||||
requirejs(['../app']);
|
||||
});
|
||||
|
||||
// Test if WebP is natively supported, and if not, set webpMachine to true. The value of webpMachine
|
||||
// will determine whether the WebP Polyfills will be loaded (currently only used in uiUtil.js)
|
||||
var webpMachine = false;
|
||||
// Test if WebP is natively supported, and if not, set webpMachine to true. The value of webpMachine
|
||||
// will determine whether the WebP Polyfills will be loaded (currently only used in uiUtil.js)
|
||||
var webpMachine = false;
|
||||
|
||||
// We use a self-invoking function here to avoid defining unnecessary global functions and variables
|
||||
(function (callback) {
|
||||
// Tests for native WebP support
|
||||
var webP = new Image();
|
||||
webP.onload = webP.onerror = function () {
|
||||
callback(webP.height === 2);
|
||||
};
|
||||
webP.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA';
|
||||
})(function (support) {
|
||||
if (!support) {
|
||||
webpMachine = true;
|
||||
}
|
||||
});
|
||||
// We use a self-invoking function here to avoid defining unnecessary global functions and variables
|
||||
(function (callback) {
|
||||
// Tests for native WebP support
|
||||
var webP = new Image();
|
||||
webP.onload = webP.onerror = function () {
|
||||
callback(webP.height === 2);
|
||||
};
|
||||
webP.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA';
|
||||
})(function (support) {
|
||||
if (!support) {
|
||||
webpMachine = true;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
@ -34,15 +34,24 @@ define([], function () {
|
||||
*/
|
||||
var regexpCookieKeysToMigrate = new RegExp([
|
||||
'hideActiveContentWarning', 'showUIAnimations', 'appTheme', 'useCache',
|
||||
'lastContentInjectionMode', 'listOfArchives', 'lastSelectedArchive'
|
||||
'contentInjectionMode', 'listOfArchives', 'lastSelectedArchive'
|
||||
].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
|
||||
* It is set in init.js because it is needed early in app loading
|
||||
* @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
|
||||
function getBestAvailableStorageAPI() {
|
||||
@ -72,6 +81,11 @@ define([], function () {
|
||||
// If both cookies and localStorage are supported, and document.cookie contains keys to migrate,
|
||||
// migrate settings to use localStorage
|
||||
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
|
||||
// does not cause an exception even if cookies are blocked in some contexts, whereas accessing localStorage may cause an exception
|
||||
return type;
|
||||
|
@ -225,6 +225,76 @@ define(rqDef, function() {
|
||||
$("#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
|
||||
*
|
||||
@ -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
|
||||
// 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") {
|
||||
showReturnLink();
|
||||
}
|
||||
@ -453,6 +523,9 @@ define(rqDef, function() {
|
||||
removeUrlParameters: removeUrlParameters,
|
||||
displayActiveContentWarning: displayActiveContentWarning,
|
||||
displayFileDownloadAlert: displayFileDownloadAlert,
|
||||
checkUpdateStatus: checkUpdateStatus,
|
||||
checkServerIsAccessible: checkServerIsAccessible,
|
||||
spinnerDisplay: spinnerDisplay,
|
||||
isElementInView: isElementInView,
|
||||
htmlEscapeChars: htmlEscapeChars,
|
||||
removeAnimationClasses: removeAnimationClasses,
|
||||
|
Loading…
x
Reference in New Issue
Block a user