mirror of
https://github.com/kiwix/kiwix-js-pwa.git
synced 2025-09-09 12:19:46 -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,
|
||||
* in order to capture the HTTP requests made by an article, and respond with the
|
||||
* corresponding content, coming from the archive
|
||||
@ -23,9 +23,14 @@
|
||||
*/
|
||||
'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) {
|
||||
event.waitUntil(self.skipWaiting());
|
||||
console.log("ServiceWorker installed");
|
||||
});
|
||||
|
||||
self.addEventListener('activate', function(event) {
|
||||
@ -33,7 +38,6 @@ self.addEventListener('activate', function(event) {
|
||||
// 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.log("ServiceWorker activated");
|
||||
});
|
||||
|
||||
var regexpRemoveUrlParameters = new RegExp(/([^?#]+)[?#].*$/);
|
||||
@ -54,85 +58,54 @@ function removeUrlParameters(url) {
|
||||
return url.replace(regexpRemoveUrlParameters, "$1");
|
||||
}
|
||||
|
||||
console.log("ServiceWorker startup");
|
||||
|
||||
var outgoingMessagePort = null;
|
||||
var fetchCaptureEnabled = false;
|
||||
self.addEventListener('fetch', fetchEventListener);
|
||||
console.log('fetchEventListener set');
|
||||
|
||||
self.addEventListener('message', function (event) {
|
||||
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];
|
||||
console.log('outgoingMessagePort initialized', outgoingMessagePort);
|
||||
fetchCaptureEnabled = true;
|
||||
console.log('fetchEventListener enabled');
|
||||
}
|
||||
if (event.data.action === 'disable') {
|
||||
console.log('Disable message received');
|
||||
// On 'disable' message, we delete the outgoingMessagePort and disable the fetchEventListener
|
||||
outgoingMessagePort = null;
|
||||
console.log('outgoingMessagePort deleted');
|
||||
fetchCaptureEnabled = false;
|
||||
console.log('fetchEventListener disabled');
|
||||
}
|
||||
});
|
||||
|
||||
// TODO : this way to recognize content types is temporary
|
||||
// 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
|
||||
// Pattern for ZIM file namespace - see https://wiki.openzim.org/wiki/ZIM_file_format#Namespaces
|
||||
var regexpZIMUrlWithNamespace = new RegExp(/(?:^|\/)([-ABIJMUVWX])\/(.+)/);
|
||||
|
||||
function fetchEventListener(event) {
|
||||
if (fetchCaptureEnabled) {
|
||||
console.log('ServiceWorker handling fetch event for : ' + 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) {
|
||||
var nameSpace;
|
||||
var title;
|
||||
var titleWithNameSpace;
|
||||
var contentType;
|
||||
var regexpResult = regexpZIMUrlWithNamespace.exec(event.request.url);
|
||||
nameSpace = regexpResult[1];
|
||||
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
|
||||
title = removeUrlParameters(decodeURIComponent(title));
|
||||
|
||||
@ -142,31 +115,44 @@ function fetchEventListener(event) {
|
||||
var messageChannel = new MessageChannel();
|
||||
messageChannel.port1.onmessage = function(event) {
|
||||
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 = {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: {
|
||||
'Content-Type': contentType
|
||||
}
|
||||
headers: headers
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
else if (event.data.action === 'sendRedirect') {
|
||||
resolve(Response.redirect(event.data.redirectUrl));
|
||||
}
|
||||
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);
|
||||
}
|
||||
};
|
||||
console.log('Eventlistener added to listen for an answer to ' + titleWithNameSpace);
|
||||
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,
|
||||
|
@ -97,7 +97,7 @@ div:not(.panel-success, .alert-message) {
|
||||
background: lightblue;
|
||||
}
|
||||
|
||||
#articleList a:hover, #articleList a.hover {
|
||||
#articleList a:hover, #articleList a.hover, .backgroundLightBlue {
|
||||
background: lightblue;
|
||||
}
|
||||
|
||||
|
@ -630,18 +630,23 @@
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<label class="radio">
|
||||
<input type="radio" name="contentInjectionMode" value="jquery" id="jQueryModeRadio" checked>
|
||||
<span class="radiobtn"></span>
|
||||
<b>JQuery</b> (slow and memory hungry, but safer)
|
||||
<strong>JQuery</strong> (stable and safe)
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="contentInjectionMode" value="serviceworker" id="serviceworkerModeRadio">
|
||||
<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>
|
||||
</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="messageChannelStatus"></div>
|
||||
</div>
|
||||
|
@ -26,14 +26,22 @@
|
||||
// This uses require.js to structure javascript:
|
||||
// http://requirejs.org/docs/api.html#define
|
||||
|
||||
define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module', 'transformStyles', 'kiwixServe'],
|
||||
function ($, zimArchiveLoader, util, uiUtil, cookies, q, module, transformStyles, kiwixServe) {
|
||||
define(['jquery', 'zimArchiveLoader', 'uiUtil', 'images', 'cookies', 'abstractFilesystemAccess', 'q', 'transformStyles', 'kiwixServe'],
|
||||
function ($, zimArchiveLoader, uiUtil, images, cookies, abstractFilesystemAccess, q, transformStyles, kiwixServe) {
|
||||
|
||||
/**
|
||||
* Maximum number of articles to display in a search
|
||||
* @type Integer
|
||||
*/
|
||||
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
|
||||
@ -1117,11 +1125,15 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
|
||||
* Displays or refreshes the API status shown to the user
|
||||
*/
|
||||
function refreshAPIStatus() {
|
||||
var apiStatusPanel = document.getElementById('apiStatusDiv');
|
||||
apiStatusPanel.classList.remove('panel-success', 'panel-warning');
|
||||
var apiPanelClass = 'panel-success';
|
||||
if (isMessageChannelAvailable()) {
|
||||
$('#messageChannelStatus').html("MessageChannel API available");
|
||||
$('#messageChannelStatus').removeClass("apiAvailable apiUnavailable")
|
||||
.addClass("apiAvailable");
|
||||
} else {
|
||||
apiPanelClass = 'panel-warning';
|
||||
$('#messageChannelStatus').html("MessageChannel API unavailable");
|
||||
$('#messageChannelStatus').removeClass("apiAvailable apiUnavailable")
|
||||
.addClass("apiUnavailable");
|
||||
@ -1132,15 +1144,18 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
|
||||
$('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable")
|
||||
.addClass("apiAvailable");
|
||||
} else {
|
||||
apiPanelClass = 'panel-warning';
|
||||
$('#serviceWorkerStatus').html("ServiceWorker API available, but not registered");
|
||||
$('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable")
|
||||
.addClass("apiUnavailable");
|
||||
}
|
||||
} else {
|
||||
apiPanelClass = 'panel-warning';
|
||||
$('#serviceWorkerStatus').html("ServiceWorker API unavailable");
|
||||
$('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable")
|
||||
.addClass("apiUnavailable");
|
||||
}
|
||||
apiStatusPanel.classList.add(apiPanelClass);
|
||||
}
|
||||
|
||||
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
|
||||
navigator.serviceWorker.controller.postMessage({'action': 'init'}, [tmpMessageChannel.port2]);
|
||||
messageChannel = tmpMessageChannel;
|
||||
console.log("init message sent to ServiceWorker");
|
||||
// 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);
|
||||
@ -1201,7 +1215,7 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
|
||||
if (!isServiceWorkerReady()) {
|
||||
$('#serviceWorkerStatus').html("ServiceWorker API available : trying to register it...");
|
||||
navigator.serviceWorker.register('../service-worker.js').then(function (reg) {
|
||||
console.log('serviceWorker registered', reg);
|
||||
// The ServiceWorker is registered
|
||||
serviceWorkerRegistration = reg;
|
||||
refreshAPIStatus();
|
||||
|
||||
@ -1210,6 +1224,8 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
|
||||
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();
|
||||
@ -1225,45 +1241,33 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
|
||||
}, 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/...)";
|
||||
}
|
||||
uiUtil.systemAlert(message);
|
||||
setContentInjectionMode("jquery");
|
||||
return;
|
||||
});
|
||||
} else {
|
||||
// We need to set this variable earlier else the ServiceWorker does not get reactivated
|
||||
contentInjectionMode = value;
|
||||
initOrKeepAliveServiceWorker();
|
||||
}
|
||||
}
|
||||
$('input:radio[name=contentInjectionMode]').prop('checked', false);
|
||||
$('input:radio[name=contentInjectionMode]').filter('[value="' + value + '"]').prop('checked', true);
|
||||
contentInjectionMode = value;
|
||||
images.setContentInjectionMode(contentInjectionMode);
|
||||
// Save the value in a cookie, so that to be able to keep it after a reload/restart
|
||||
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)
|
||||
var lastContentInjectionMode = cookies.getItem('lastContentInjectionMode');
|
||||
if (lastContentInjectionMode) {
|
||||
@ -1313,7 +1317,6 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
|
||||
* @type Array.<StorageFirefoxOS>
|
||||
*/
|
||||
var storages = [];
|
||||
//var storages = [appFolder.path]; //UWP @UWP
|
||||
function searchForArchivesInPreferencesOrStorage() {
|
||||
// First see if the list of archives is stored in the cookie
|
||||
var listOfArchivesFromCookie = cookies.getItem("listOfArchives");
|
||||
@ -1600,16 +1603,16 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
|
||||
function displayFileSelect() {
|
||||
document.getElementById('openLocalFiles').style.display = 'block';
|
||||
// Set the main drop zone
|
||||
scrollBoxDropZone.addEventListener('dragover', handleGlobalDragover, false);
|
||||
scrollBoxDropZone.addEventListener('dragover', handleGlobalDragover);
|
||||
scrollBoxDropZone.addEventListener('dragleave', function(e) {
|
||||
configDropZone.style.border = '';
|
||||
});
|
||||
// Also set a global drop zone (allows us to ensure Config is always displayed for the file drop)
|
||||
globalDropZone.addEventListener('dragover', function (e) {
|
||||
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';
|
||||
}, false);
|
||||
});
|
||||
globalDropZone.addEventListener('drop', handleFileDrop);
|
||||
// This handles use of the file picker
|
||||
document.getElementById('archiveFiles').addEventListener('change', setLocalArchiveFromFileSelect);
|
||||
@ -1825,11 +1828,12 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
|
||||
request.responseType = "blob";
|
||||
request.onreadystatechange = function () {
|
||||
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
|
||||
request.response.name = url;
|
||||
deferred.resolve(request.response);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
deferred.reject("HTTP status " + request.status + " when reading " + url);
|
||||
}
|
||||
}
|
||||
@ -2077,9 +2081,9 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
|
||||
function findDirEntryFromDirEntryIdAndLaunchArticleRead(dirEntryId) {
|
||||
if (selectedArchive.isReady()) {
|
||||
var dirEntry = selectedArchive.parseDirEntryId(dirEntryId);
|
||||
// Remove focus from search field to hide keyboard
|
||||
document.getElementById('searchArticles').focus();
|
||||
document.getElementById('searchingArticles').style.display = 'block';
|
||||
// Remove focus from search field to hide keyboard and to allow navigation keys to be used
|
||||
document.getElementById('articleContent').contentWindow.focus();
|
||||
$("#searchingArticles").show();
|
||||
if (dirEntry.isRedirect()) {
|
||||
selectedArchive.resolveRedirect(dirEntry, readArticle);
|
||||
} 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