mirror of
https://github.com/kiwix/kiwix-js-pwa.git
synced 2025-09-10 12:53:54 -04:00
Port some changes from Kiwix JS
Former-commit-id: c104efb632b0d23bd7376e277f22e169a2814e55 [formerly a5dfd669c5676262a410c7bd5a7351cc4bdc6318] Former-commit-id: e3fd809660f7229c985727d21d411bc5d4197b76
This commit is contained in:
parent
13b106bdbe
commit
c3d8dc4643
@ -1,4 +1,4 @@
|
|||||||
/**
|
/**
|
||||||
* service-worker.js : Service Worker implementation,
|
* service-worker.js : Service Worker implementation,
|
||||||
* in order to capture the HTTP requests made by an article, and respond with the
|
* in order to capture the HTTP requests made by an article, and respond with the
|
||||||
* corresponding content, coming from the archive
|
* corresponding content, coming from the archive
|
||||||
@ -23,9 +23,14 @@
|
|||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A global Boolean that governs whether images are displayed
|
||||||
|
* app.js can alter this variable via messaging
|
||||||
|
*/
|
||||||
|
var imageDisplay;
|
||||||
|
|
||||||
self.addEventListener('install', function(event) {
|
self.addEventListener('install', function(event) {
|
||||||
event.waitUntil(self.skipWaiting());
|
event.waitUntil(self.skipWaiting());
|
||||||
console.log("ServiceWorker installed");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('activate', function(event) {
|
self.addEventListener('activate', function(event) {
|
||||||
@ -33,7 +38,6 @@ self.addEventListener('activate', function(event) {
|
|||||||
// 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.log("ServiceWorker activated");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var regexpRemoveUrlParameters = new RegExp(/([^?#]+)[?#].*$/);
|
var regexpRemoveUrlParameters = new RegExp(/([^?#]+)[?#].*$/);
|
||||||
@ -54,85 +58,54 @@ function removeUrlParameters(url) {
|
|||||||
return url.replace(regexpRemoveUrlParameters, "$1");
|
return url.replace(regexpRemoveUrlParameters, "$1");
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("ServiceWorker startup");
|
|
||||||
|
|
||||||
var outgoingMessagePort = null;
|
var outgoingMessagePort = null;
|
||||||
var fetchCaptureEnabled = false;
|
var fetchCaptureEnabled = false;
|
||||||
self.addEventListener('fetch', fetchEventListener);
|
self.addEventListener('fetch', fetchEventListener);
|
||||||
console.log('fetchEventListener set');
|
|
||||||
|
|
||||||
self.addEventListener('message', function (event) {
|
self.addEventListener('message', function (event) {
|
||||||
if (event.data.action === 'init') {
|
if (event.data.action === 'init') {
|
||||||
console.log('Init message received', event.data);
|
// On 'init' message, we initialize the outgoingMessagePort and enable the fetchEventListener
|
||||||
outgoingMessagePort = event.ports[0];
|
outgoingMessagePort = event.ports[0];
|
||||||
console.log('outgoingMessagePort initialized', outgoingMessagePort);
|
|
||||||
fetchCaptureEnabled = true;
|
fetchCaptureEnabled = true;
|
||||||
console.log('fetchEventListener enabled');
|
|
||||||
}
|
}
|
||||||
if (event.data.action === 'disable') {
|
if (event.data.action === 'disable') {
|
||||||
console.log('Disable message received');
|
// On 'disable' message, we delete the outgoingMessagePort and disable the fetchEventListener
|
||||||
outgoingMessagePort = null;
|
outgoingMessagePort = null;
|
||||||
console.log('outgoingMessagePort deleted');
|
|
||||||
fetchCaptureEnabled = false;
|
fetchCaptureEnabled = false;
|
||||||
console.log('fetchEventListener disabled');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO : this way to recognize content types is temporary
|
// Pattern for ZIM file namespace - see https://wiki.openzim.org/wiki/ZIM_file_format#Namespaces
|
||||||
// It must be replaced by reading the actual MIME-Type from the backend
|
|
||||||
var regexpJPEG = new RegExp(/\.jpe?g$/i);
|
|
||||||
var regexpPNG = new RegExp(/\.png$/i);
|
|
||||||
var regexpJS = new RegExp(/\.js/i);
|
|
||||||
var regexpCSS = new RegExp(/\.css$/i);
|
|
||||||
var regexpSVG = new RegExp(/\.svg$/i);
|
|
||||||
|
|
||||||
// Pattern for ZIM file namespace - see http://www.openzim.org/wiki/ZIM_file_format#Namespaces
|
|
||||||
var regexpZIMUrlWithNamespace = new RegExp(/(?:^|\/)([-ABIJMUVWX])\/(.+)/);
|
var regexpZIMUrlWithNamespace = new RegExp(/(?:^|\/)([-ABIJMUVWX])\/(.+)/);
|
||||||
|
|
||||||
function fetchEventListener(event) {
|
function fetchEventListener(event) {
|
||||||
if (fetchCaptureEnabled) {
|
if (fetchCaptureEnabled) {
|
||||||
console.log('ServiceWorker handling fetch event for : ' + event.request.url);
|
|
||||||
|
|
||||||
if (regexpZIMUrlWithNamespace.test(event.request.url)) {
|
if (regexpZIMUrlWithNamespace.test(event.request.url)) {
|
||||||
|
// The ServiceWorker will handle this request
|
||||||
|
|
||||||
console.log('Asking app.js for a content', event.request.url);
|
// If the user has disabled the display of images, and the browser wants an image, respond with empty SVG
|
||||||
|
// A URL with "?kiwix-display" query string acts as a passthrough so that the regex will not match and
|
||||||
|
// the image will be fetched by app.js
|
||||||
|
// DEV: If you need to hide more image types, add them to regex below and also edit equivalent regex in app.js
|
||||||
|
if (imageDisplay !== 'all' && /(^|\/)[IJ]\/.*\.(jpe?g|png|svg|gif)($|[?#])(?!kiwix-display)/i.test(event.request.url)) {
|
||||||
|
var svgResponse;
|
||||||
|
if (imageDisplay === 'manual')
|
||||||
|
svgResponse = "<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1'><rect width='1' height='1' style='fill:lightblue'/></svg>";
|
||||||
|
else
|
||||||
|
svgResponse = "<svg xmlns='http://www.w3.org/2000/svg'/>";
|
||||||
|
event.respondWith(new Response(svgResponse, { headers: { 'Content-Type': 'image/svg+xml' } }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's ask app.js for that content
|
||||||
event.respondWith(new Promise(function(resolve, reject) {
|
event.respondWith(new Promise(function(resolve, reject) {
|
||||||
var nameSpace;
|
var nameSpace;
|
||||||
var title;
|
var title;
|
||||||
var titleWithNameSpace;
|
var titleWithNameSpace;
|
||||||
var contentType;
|
|
||||||
var regexpResult = regexpZIMUrlWithNamespace.exec(event.request.url);
|
var regexpResult = regexpZIMUrlWithNamespace.exec(event.request.url);
|
||||||
nameSpace = regexpResult[1];
|
nameSpace = regexpResult[1];
|
||||||
title = regexpResult[2];
|
title = regexpResult[2];
|
||||||
|
|
||||||
// The namespace defines the type of content. See http://www.openzim.org/wiki/ZIM_file_format#Namespaces
|
|
||||||
// TODO : read the contentType from the ZIM file instead of hard-coding it here
|
|
||||||
if (nameSpace === 'A') {
|
|
||||||
console.log("It's an article : " + title);
|
|
||||||
contentType = 'text/html';
|
|
||||||
}
|
|
||||||
else if (nameSpace === 'I' || nameSpace === 'J') {
|
|
||||||
console.log("It's an image : " + title);
|
|
||||||
if (regexpJPEG.test(title)) {
|
|
||||||
contentType = 'image/jpeg';
|
|
||||||
}
|
|
||||||
else if (regexpPNG.test(title)) {
|
|
||||||
contentType = 'image/png';
|
|
||||||
}
|
|
||||||
else if (regexpSVG.test(title)) {
|
|
||||||
contentType = 'image/svg+xml';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (nameSpace === '-') {
|
|
||||||
console.log("It's a layout dependency : " + title);
|
|
||||||
if (regexpJS.test(title)) {
|
|
||||||
contentType = 'text/javascript';
|
|
||||||
}
|
|
||||||
else if (regexpCSS.test(title)) {
|
|
||||||
contentType = 'text/css';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need to remove the potential parameters in the URL
|
// We need to remove the potential parameters in the URL
|
||||||
title = removeUrlParameters(decodeURIComponent(title));
|
title = removeUrlParameters(decodeURIComponent(title));
|
||||||
|
|
||||||
@ -142,31 +115,44 @@ function fetchEventListener(event) {
|
|||||||
var messageChannel = new MessageChannel();
|
var messageChannel = new MessageChannel();
|
||||||
messageChannel.port1.onmessage = function(event) {
|
messageChannel.port1.onmessage = function(event) {
|
||||||
if (event.data.action === 'giveContent') {
|
if (event.data.action === 'giveContent') {
|
||||||
console.log('content message received for ' + titleWithNameSpace, event.data);
|
// Content received from app.js
|
||||||
|
var contentLength = event.data.content ? event.data.content.byteLength : null;
|
||||||
|
var contentType = event.data.mimetype;
|
||||||
|
// Set the imageDisplay variable if it has been sent in the event data
|
||||||
|
imageDisplay = typeof event.data.imageDisplay !== 'undefined' ?
|
||||||
|
event.data.imageDisplay : imageDisplay;
|
||||||
|
var headers = new Headers ();
|
||||||
|
if (contentLength) headers.set('Content-Length', contentLength);
|
||||||
|
if (contentType) headers.set('Content-Type', contentType);
|
||||||
|
// Test if the content is a video or audio file
|
||||||
|
// See kiwix-js #519 and openzim/zimwriterfs #113 for why we test for invalid types like "mp4" or "webm" (without "video/")
|
||||||
|
// The full list of types produced by zimwriterfs is in https://github.com/openzim/zimwriterfs/blob/master/src/tools.cpp
|
||||||
|
if (contentLength >= 1 && /^(video|audio)|(^|\/)(mp4|webm|og[gmv]|mpeg)$/i.test(contentType)) {
|
||||||
|
// In case of a video (at least), Chrome and Edge need these HTTP headers else seeking doesn't work
|
||||||
|
// (even if we always send all the video content, not the requested range, until the backend supports it)
|
||||||
|
headers.set('Accept-Ranges', 'bytes');
|
||||||
|
headers.set('Content-Range', 'bytes 0-' + (contentLength-1) + '/' + contentLength);
|
||||||
|
}
|
||||||
var responseInit = {
|
var responseInit = {
|
||||||
status: 200,
|
status: 200,
|
||||||
statusText: 'OK',
|
statusText: 'OK',
|
||||||
headers: {
|
headers: headers
|
||||||
'Content-Type': contentType
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var httpResponse = new Response(event.data.content, responseInit);
|
var httpResponse = new Response(event.data.content, responseInit);
|
||||||
|
|
||||||
console.log('ServiceWorker responding to the HTTP request for ' + titleWithNameSpace + ' (size=' + event.data.content.length + ' octets)' , httpResponse);
|
// Let's send the content back from the ServiceWorker
|
||||||
resolve(httpResponse);
|
resolve(httpResponse);
|
||||||
}
|
}
|
||||||
else if (event.data.action === 'sendRedirect') {
|
else if (event.data.action === 'sendRedirect') {
|
||||||
resolve(Response.redirect(event.data.redirectUrl));
|
resolve(Response.redirect(event.data.redirectUrl));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.log('Invalid message received from app.js for ' + titleWithNameSpace, event.data);
|
console.error('Invalid message received from app.js for ' + titleWithNameSpace, event.data);
|
||||||
reject(event.data);
|
reject(event.data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
console.log('Eventlistener added to listen for an answer to ' + titleWithNameSpace);
|
|
||||||
outgoingMessagePort.postMessage({'action': 'askForContent', 'title': titleWithNameSpace}, [messageChannel.port2]);
|
outgoingMessagePort.postMessage({'action': 'askForContent', 'title': titleWithNameSpace}, [messageChannel.port2]);
|
||||||
console.log('Message sent to app.js through outgoingMessagePort');
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
// If event.respondWith() isn't called because this wasn't a request that we want to handle,
|
// If event.respondWith() isn't called because this wasn't a request that we want to handle,
|
||||||
|
@ -97,7 +97,7 @@ div:not(.panel-success, .alert-message) {
|
|||||||
background: lightblue;
|
background: lightblue;
|
||||||
}
|
}
|
||||||
|
|
||||||
#articleList a:hover, #articleList a.hover {
|
#articleList a:hover, #articleList a.hover, .backgroundLightBlue {
|
||||||
background: lightblue;
|
background: lightblue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -630,18 +630,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-danger" id="contentInjectionModeDiv">
|
<div class="panel panel-danger" id="contentInjectionModeDiv">
|
||||||
<div class="panel-heading">Content injection mode: do not touch unless you know what you're doing!</div>
|
<div class="panel-heading">Content injection mode</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<label class="radio">
|
<label class="radio">
|
||||||
<input type="radio" name="contentInjectionMode" value="jquery" id="jQueryModeRadio" checked>
|
<input type="radio" name="contentInjectionMode" value="jquery" id="jQueryModeRadio" checked>
|
||||||
<span class="radiobtn"></span>
|
<span class="radiobtn"></span>
|
||||||
<b>JQuery</b> (slow and memory hungry, but safer)
|
<strong>JQuery</strong> (stable and safe)
|
||||||
</label>
|
</label>
|
||||||
<label class="radio">
|
<label class="radio">
|
||||||
<input type="radio" name="contentInjectionMode" value="serviceworker" id="serviceworkerModeRadio">
|
<input type="radio" name="contentInjectionMode" value="serviceworker" id="serviceworkerModeRadio">
|
||||||
<span class="radiobtn"></span>
|
<span class="radiobtn"></span>
|
||||||
<b>ServiceWorker</b> (faster but unstable, and not supported by all platforms)
|
<strong>ServiceWorker</strong> (not available on all platforms, but supports more ZIM files)
|
||||||
</label>
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-warning" id="apiStatusDiv">
|
||||||
|
<div class="panel-heading">API Status</div>
|
||||||
|
<div class="panel-body">
|
||||||
<div id="serviceWorkerStatus"></div>
|
<div id="serviceWorkerStatus"></div>
|
||||||
<div id="messageChannelStatus"></div>
|
<div id="messageChannelStatus"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,14 +26,22 @@
|
|||||||
// This uses require.js to structure javascript:
|
// This uses require.js to structure javascript:
|
||||||
// http://requirejs.org/docs/api.html#define
|
// http://requirejs.org/docs/api.html#define
|
||||||
|
|
||||||
define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module', 'transformStyles', 'kiwixServe'],
|
define(['jquery', 'zimArchiveLoader', 'uiUtil', 'images', 'cookies', 'abstractFilesystemAccess', 'q', 'transformStyles', 'kiwixServe'],
|
||||||
function ($, zimArchiveLoader, util, uiUtil, cookies, q, module, transformStyles, kiwixServe) {
|
function ($, zimArchiveLoader, uiUtil, images, cookies, abstractFilesystemAccess, q, transformStyles, kiwixServe) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum number of articles to display in a search
|
* Maximum number of articles to display in a search
|
||||||
* @type Integer
|
* @type Integer
|
||||||
*/
|
*/
|
||||||
var MAX_SEARCH_RESULT_SIZE = params.results; //This value is controlled in init.js, as are all parameters
|
var MAX_SEARCH_RESULT_SIZE = params.results; //This value is controlled in init.js, as are all parameters
|
||||||
|
/**
|
||||||
|
* The delay (in milliseconds) between two "keepalive" messages
|
||||||
|
* sent to the ServiceWorker (so that it is not stopped by
|
||||||
|
* the browser, and keeps the MessageChannel to communicate
|
||||||
|
* with the application)
|
||||||
|
* @type Integer
|
||||||
|
*/
|
||||||
|
var DELAY_BETWEEN_KEEPALIVE_SERVICEWORKER = 30000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type ZIMArchive
|
* @type ZIMArchive
|
||||||
@ -1117,11 +1125,15 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
|
|||||||
* Displays or refreshes the API status shown to the user
|
* Displays or refreshes the API status shown to the user
|
||||||
*/
|
*/
|
||||||
function refreshAPIStatus() {
|
function refreshAPIStatus() {
|
||||||
|
var apiStatusPanel = document.getElementById('apiStatusDiv');
|
||||||
|
apiStatusPanel.classList.remove('panel-success', 'panel-warning');
|
||||||
|
var apiPanelClass = 'panel-success';
|
||||||
if (isMessageChannelAvailable()) {
|
if (isMessageChannelAvailable()) {
|
||||||
$('#messageChannelStatus').html("MessageChannel API available");
|
$('#messageChannelStatus').html("MessageChannel API available");
|
||||||
$('#messageChannelStatus').removeClass("apiAvailable apiUnavailable")
|
$('#messageChannelStatus').removeClass("apiAvailable apiUnavailable")
|
||||||
.addClass("apiAvailable");
|
.addClass("apiAvailable");
|
||||||
} else {
|
} else {
|
||||||
|
apiPanelClass = 'panel-warning';
|
||||||
$('#messageChannelStatus').html("MessageChannel API unavailable");
|
$('#messageChannelStatus').html("MessageChannel API unavailable");
|
||||||
$('#messageChannelStatus').removeClass("apiAvailable apiUnavailable")
|
$('#messageChannelStatus').removeClass("apiAvailable apiUnavailable")
|
||||||
.addClass("apiUnavailable");
|
.addClass("apiUnavailable");
|
||||||
@ -1132,15 +1144,18 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
|
|||||||
$('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable")
|
$('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable")
|
||||||
.addClass("apiAvailable");
|
.addClass("apiAvailable");
|
||||||
} else {
|
} else {
|
||||||
|
apiPanelClass = 'panel-warning';
|
||||||
$('#serviceWorkerStatus').html("ServiceWorker API available, but not registered");
|
$('#serviceWorkerStatus').html("ServiceWorker API available, but not registered");
|
||||||
$('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable")
|
$('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable")
|
||||||
.addClass("apiUnavailable");
|
.addClass("apiUnavailable");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
apiPanelClass = 'panel-warning';
|
||||||
$('#serviceWorkerStatus').html("ServiceWorker API unavailable");
|
$('#serviceWorkerStatus').html("ServiceWorker API unavailable");
|
||||||
$('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable")
|
$('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable")
|
||||||
.addClass("apiUnavailable");
|
.addClass("apiUnavailable");
|
||||||
}
|
}
|
||||||
|
apiStatusPanel.classList.add(apiPanelClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentInjectionMode;
|
var contentInjectionMode;
|
||||||
@ -1160,7 +1175,6 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
|
|||||||
// 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]);
|
navigator.serviceWorker.controller.postMessage({'action': 'init'}, [tmpMessageChannel.port2]);
|
||||||
messageChannel = tmpMessageChannel;
|
messageChannel = tmpMessageChannel;
|
||||||
console.log("init message sent to ServiceWorker");
|
|
||||||
// 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);
|
||||||
@ -1201,7 +1215,7 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
|
|||||||
if (!isServiceWorkerReady()) {
|
if (!isServiceWorkerReady()) {
|
||||||
$('#serviceWorkerStatus').html("ServiceWorker API available : trying to register it...");
|
$('#serviceWorkerStatus').html("ServiceWorker API available : trying to register it...");
|
||||||
navigator.serviceWorker.register('../service-worker.js').then(function (reg) {
|
navigator.serviceWorker.register('../service-worker.js').then(function (reg) {
|
||||||
console.log('serviceWorker registered', reg);
|
// The ServiceWorker is registered
|
||||||
serviceWorkerRegistration = reg;
|
serviceWorkerRegistration = reg;
|
||||||
refreshAPIStatus();
|
refreshAPIStatus();
|
||||||
|
|
||||||
@ -1210,6 +1224,8 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
|
|||||||
var serviceWorker = reg.installing || reg.waiting || reg.active;
|
var serviceWorker = reg.installing || reg.waiting || reg.active;
|
||||||
serviceWorker.addEventListener('statechange', function (statechangeevent) {
|
serviceWorker.addEventListener('statechange', function (statechangeevent) {
|
||||||
if (statechangeevent.target.state === 'activated') {
|
if (statechangeevent.target.state === 'activated') {
|
||||||
|
// Remove any jQuery hooks from a previous jQuery session
|
||||||
|
$('#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();
|
||||||
@ -1225,45 +1241,33 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
|
|||||||
}, function (err) {
|
}, function (err) {
|
||||||
console.error('error while registering serviceWorker', err);
|
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 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/...)";
|
||||||
|
}
|
||||||
|
uiUtil.systemAlert(message);
|
||||||
|
setContentInjectionMode("jquery");
|
||||||
|
return;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
// We need to set this variable earlier else the ServiceWorker does not get reactivated
|
||||||
|
contentInjectionMode = value;
|
||||||
initOrKeepAliveServiceWorker();
|
initOrKeepAliveServiceWorker();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$('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;
|
contentInjectionMode = value;
|
||||||
|
images.setContentInjectionMode(contentInjectionMode);
|
||||||
// Save the value in a cookie, so that to be able to keep it after a reload/restart
|
// Save the value in a cookie, so that to be able to keep it after a reload/restart
|
||||||
cookies.setItem('lastContentInjectionMode', value, Infinity);
|
cookies.setItem('lastContentInjectionMode', value, Infinity);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* If the ServiceWorker mode is selected, warn the user before activating it
|
|
||||||
* @param chosenContentInjectionMode The mode that the user has chosen
|
|
||||||
*/
|
|
||||||
function checkWarnServiceWorkerMode(chosenContentInjectionMode) {
|
|
||||||
if (chosenContentInjectionMode === 'serviceworker' && !cookies.hasItem("warnedServiceWorkerMode")) {
|
|
||||||
// The user selected the "serviceworker" mode, which is still unstable
|
|
||||||
// So let's display a warning to the user
|
|
||||||
|
|
||||||
// If the focus is on the search field, we have to move it,
|
|
||||||
// else the keyboard hides the message
|
|
||||||
if ($("#prefix").is(":focus")) {
|
|
||||||
$("#searchArticles").focus();
|
|
||||||
}
|
|
||||||
if (confirm("The 'Service Worker' mode is still UNSTABLE for now." +
|
|
||||||
" It happens that the application needs to be reinstalled (or the ServiceWorker manually removed)." +
|
|
||||||
" Please confirm with OK that you're ready to face this kind of bugs, or click Cancel to stay in 'jQuery' mode.")) {
|
|
||||||
// We will not display this warning again for one day
|
|
||||||
cookies.setItem("warnedServiceWorkerMode", true, 86400);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// At launch, we try to set the last content injection mode (stored in a cookie)
|
// At launch, we try to set the last content injection mode (stored in a cookie)
|
||||||
var lastContentInjectionMode = cookies.getItem('lastContentInjectionMode');
|
var lastContentInjectionMode = cookies.getItem('lastContentInjectionMode');
|
||||||
if (lastContentInjectionMode) {
|
if (lastContentInjectionMode) {
|
||||||
@ -1313,7 +1317,6 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
|
|||||||
* @type Array.<StorageFirefoxOS>
|
* @type Array.<StorageFirefoxOS>
|
||||||
*/
|
*/
|
||||||
var storages = [];
|
var storages = [];
|
||||||
//var storages = [appFolder.path]; //UWP @UWP
|
|
||||||
function searchForArchivesInPreferencesOrStorage() {
|
function searchForArchivesInPreferencesOrStorage() {
|
||||||
// First see if the list of archives is stored in the cookie
|
// First see if the list of archives is stored in the cookie
|
||||||
var listOfArchivesFromCookie = cookies.getItem("listOfArchives");
|
var listOfArchivesFromCookie = cookies.getItem("listOfArchives");
|
||||||
@ -1600,16 +1603,16 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
|
|||||||
function displayFileSelect() {
|
function displayFileSelect() {
|
||||||
document.getElementById('openLocalFiles').style.display = 'block';
|
document.getElementById('openLocalFiles').style.display = 'block';
|
||||||
// Set the main drop zone
|
// Set the main drop zone
|
||||||
scrollBoxDropZone.addEventListener('dragover', handleGlobalDragover, false);
|
scrollBoxDropZone.addEventListener('dragover', handleGlobalDragover);
|
||||||
scrollBoxDropZone.addEventListener('dragleave', function (e) {
|
scrollBoxDropZone.addEventListener('dragleave', function(e) {
|
||||||
configDropZone.style.border = '';
|
configDropZone.style.border = '';
|
||||||
});
|
});
|
||||||
// Also set a global drop zone (allows us to ensure Config is always displayed for the file drop)
|
// Also set a global drop zone (allows us to ensure Config is always displayed for the file drop)
|
||||||
globalDropZone.addEventListener('dragover', function (e) {
|
globalDropZone.addEventListener('dragover', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (document.getElementById('configuration').style.display === 'none') document.getElementById('btnConfigure').click();
|
if (configDropZone.style.display === 'none') document.getElementById('btnConfigure').click();
|
||||||
e.dataTransfer.dropEffect = 'link';
|
e.dataTransfer.dropEffect = 'link';
|
||||||
}, false);
|
});
|
||||||
globalDropZone.addEventListener('drop', handleFileDrop);
|
globalDropZone.addEventListener('drop', handleFileDrop);
|
||||||
// This handles use of the file picker
|
// This handles use of the file picker
|
||||||
document.getElementById('archiveFiles').addEventListener('change', setLocalArchiveFromFileSelect);
|
document.getElementById('archiveFiles').addEventListener('change', setLocalArchiveFromFileSelect);
|
||||||
@ -1825,11 +1828,12 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
|
|||||||
request.responseType = "blob";
|
request.responseType = "blob";
|
||||||
request.onreadystatechange = function () {
|
request.onreadystatechange = function () {
|
||||||
if (request.readyState === XMLHttpRequest.DONE) {
|
if (request.readyState === XMLHttpRequest.DONE) {
|
||||||
if (request.status >= 200 && request.status < 300 || request.status === 0) {
|
if ((request.status >= 200 && request.status < 300) || request.status === 0) {
|
||||||
// Hack to make this look similar to a file
|
// Hack to make this look similar to a file
|
||||||
request.response.name = url;
|
request.response.name = url;
|
||||||
deferred.resolve(request.response);
|
deferred.resolve(request.response);
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
deferred.reject("HTTP status " + request.status + " when reading " + url);
|
deferred.reject("HTTP status " + request.status + " when reading " + url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2077,9 +2081,9 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
|
|||||||
function findDirEntryFromDirEntryIdAndLaunchArticleRead(dirEntryId) {
|
function findDirEntryFromDirEntryIdAndLaunchArticleRead(dirEntryId) {
|
||||||
if (selectedArchive.isReady()) {
|
if (selectedArchive.isReady()) {
|
||||||
var dirEntry = selectedArchive.parseDirEntryId(dirEntryId);
|
var dirEntry = selectedArchive.parseDirEntryId(dirEntryId);
|
||||||
// Remove focus from search field to hide keyboard
|
// Remove focus from search field to hide keyboard and to allow navigation keys to be used
|
||||||
document.getElementById('searchArticles').focus();
|
document.getElementById('articleContent').contentWindow.focus();
|
||||||
document.getElementById('searchingArticles').style.display = 'block';
|
$("#searchingArticles").show();
|
||||||
if (dirEntry.isRedirect()) {
|
if (dirEntry.isRedirect()) {
|
||||||
selectedArchive.resolveRedirect(dirEntry, readArticle);
|
selectedArchive.resolveRedirect(dirEntry, readArticle);
|
||||||
} else {
|
} else {
|
||||||
|
183
www/js/lib/images.js
Normal file
183
www/js/lib/images.js
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
/**
|
||||||
|
* images.js : Functions for the processing of images
|
||||||
|
*
|
||||||
|
* Copyright 2013-2019 Mossroy and contributors
|
||||||
|
* License GPL v3:
|
||||||
|
*
|
||||||
|
* This file is part of Kiwix.
|
||||||
|
*
|
||||||
|
* Kiwix is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Kiwix is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Kiwix (file LICENSE-GPLv3.txt). If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
define(['uiUtil', 'cookies'], function(uiUtil, cookies) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declare a module-specific variable defining the contentInjectionMode. Its value may be
|
||||||
|
* changed in setContentInjectionMode()
|
||||||
|
*/
|
||||||
|
var contentInjectionMode = cookies.getItem('lastContentInjectionMode');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterates over an array or collection of image nodes, extracting the image data from the ZIM
|
||||||
|
* and inserting a BLOB URL to each image in the image's src attribute
|
||||||
|
*
|
||||||
|
* @param {Object} images An array or collection of DOM image nodes
|
||||||
|
* @param {Object} selectedArchive The ZIM archive picked by the user
|
||||||
|
*/
|
||||||
|
function extractImages(images, selectedArchive) {
|
||||||
|
Array.prototype.slice.call(images).forEach(function (image) {
|
||||||
|
var imageUrl = image.getAttribute('data-kiwixurl');
|
||||||
|
if (!imageUrl) return;
|
||||||
|
image.removeAttribute('data-kiwixurl');
|
||||||
|
var title = decodeURIComponent(imageUrl);
|
||||||
|
if (contentInjectionMode === 'serviceworker') {
|
||||||
|
image.src = imageUrl + '?kiwix-display';
|
||||||
|
} else {
|
||||||
|
selectedArchive.getDirEntryByTitle(title).then(function (dirEntry) {
|
||||||
|
return selectedArchive.readBinaryFile(dirEntry, function (fileDirEntry, content) {
|
||||||
|
image.style.background = '';
|
||||||
|
var mimetype = dirEntry.getMimetype();
|
||||||
|
uiUtil.feedNodeWithBlob(image, 'src', content, mimetype);
|
||||||
|
});
|
||||||
|
}).fail(function (e) {
|
||||||
|
console.error('Could not find DirEntry for image: ' + title, e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterates over an array or collection of image nodes, preparing each node for manual image
|
||||||
|
* extraction when user taps the indicated area
|
||||||
|
*
|
||||||
|
* @param {Object} images An array or collection of DOM image nodes
|
||||||
|
* @param {Object} selectedArchive The ZIM archive picked by the user
|
||||||
|
*/
|
||||||
|
function setupManualImageExtraction(images, selectedArchive) {
|
||||||
|
Array.prototype.slice.call(images).forEach(function (image) {
|
||||||
|
var originalHeight = image.getAttribute('height') || '';
|
||||||
|
//Ensure 36px clickable image height so user can request images by tapping
|
||||||
|
image.height = '36';
|
||||||
|
if (contentInjectionMode ==='jquery') {
|
||||||
|
image.src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E";
|
||||||
|
image.style.background = 'lightblue';
|
||||||
|
}
|
||||||
|
image.dataset.kiwixheight = originalHeight;
|
||||||
|
image.addEventListener('click', function (e) {
|
||||||
|
// If the image clicked on hasn't been extracted yet, cancel event bubbling, so that we don't navigate
|
||||||
|
// away from the article if the image is hyperlinked
|
||||||
|
if (image.dataset.kiwixurl) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
var visibleImages = queueImages(images);
|
||||||
|
visibleImages.forEach(function (image) {
|
||||||
|
if (image.dataset.kiwixheight) image.height = image.dataset.kiwixheight;
|
||||||
|
else image.removeAttribute('height');
|
||||||
|
// Line below provides a visual indication to users of slow browsers that their click has been registered and
|
||||||
|
// images are being fetched; this is not necessary in SW mode because SW is only supported by faster browsers
|
||||||
|
if (contentInjectionMode ==='jquery') image.style.background = 'lightgray';
|
||||||
|
});
|
||||||
|
extractImages(visibleImages, selectedArchive);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts an array or collection of image nodes, returning a list of those that are inside the visible viewport
|
||||||
|
*
|
||||||
|
* @param {Object} images An array or collection of DOM image nodes
|
||||||
|
* @returns {Array} An array of image nodes that are within the visible viewport
|
||||||
|
*/
|
||||||
|
function queueImages(images) {
|
||||||
|
var visibleImages = [];
|
||||||
|
for (var i = 0, l = images.length; i < l; i++) {
|
||||||
|
if (!images[i].dataset.kiwixurl) continue;
|
||||||
|
if (uiUtil.isElementInView(images[i])) {
|
||||||
|
visibleImages.push(images[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return visibleImages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares an array or collection of image nodes that have been disabled in Service Worker for manual extraction
|
||||||
|
*
|
||||||
|
* @param {Object} images An array or collection of DOM image nodes
|
||||||
|
* @param {String} displayType If 'progressive', lazyLoad will be used; if 'manual', setupManualImageExtraction will be used
|
||||||
|
*/
|
||||||
|
function prepareImagesServiceWorker (images, displayType) {
|
||||||
|
var zimImages = [];
|
||||||
|
for (var i = 0, l = images.length; i < l; i++) {
|
||||||
|
// DEV: make sure list of file types here is the same as the list in Service Worker code
|
||||||
|
if (/(^|\/)[IJ]\/.*\.(jpe?g|png|svg|gif)($|[?#])/i.test(images[i].src)) {
|
||||||
|
images[i].dataset.kiwixurl = images[i].getAttribute('src');
|
||||||
|
zimImages.push(images[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (displayType === 'manual') {
|
||||||
|
setupManualImageExtraction(zimImages);
|
||||||
|
} else {
|
||||||
|
lazyLoad(zimImages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes an array or collection of image nodes so that they will be lazy loaded (progressive extraction)
|
||||||
|
*
|
||||||
|
* @param {Object} images And array or collection of DOM image nodes which will be processed for
|
||||||
|
* progressive image extraction
|
||||||
|
* @param {Object} selectedArchive The archive picked by the user
|
||||||
|
*/
|
||||||
|
function lazyLoad(images, selectedArchive) {
|
||||||
|
var queue;
|
||||||
|
var getImages = function() {
|
||||||
|
queue = queueImages(images);
|
||||||
|
extractImages(queue, selectedArchive);
|
||||||
|
};
|
||||||
|
getImages();
|
||||||
|
// Sometimes the page hasn't been rendered when getImages() is run, especially in Firefox, so run again after delay
|
||||||
|
setTimeout(getImages, 700);
|
||||||
|
if (queue.length === images.length) return;
|
||||||
|
// There are images remaining, so set up an event listener to load more images once user has stopped scrolling the iframe
|
||||||
|
var iframe = document.getElementById('articleContent');
|
||||||
|
var iframeWindow = iframe.contentWindow;
|
||||||
|
iframeWindow.addEventListener('scroll', function() {
|
||||||
|
clearTimeout(timer);
|
||||||
|
// Waits for a period after scroll start to start checking for images
|
||||||
|
var timer = setTimeout(getImages, 600);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility to set the contentInjectionmode in this module
|
||||||
|
* It should be called when the user changes the contentInjectionMode
|
||||||
|
*
|
||||||
|
* @param {String} injectionMode The contentInjectionMode selected by the user
|
||||||
|
*/
|
||||||
|
function setContentInjectionMode(injectionMode) {
|
||||||
|
contentInjectionMode = injectionMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functions and classes exposed by this module
|
||||||
|
*/
|
||||||
|
return {
|
||||||
|
extractImages: extractImages,
|
||||||
|
setupManualImageExtraction: setupManualImageExtraction,
|
||||||
|
prepareImagesServiceWorker: prepareImagesServiceWorker,
|
||||||
|
lazyLoad: lazyLoad,
|
||||||
|
setContentInjectionMode: setContentInjectionMode
|
||||||
|
};
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user