From 671cd9a132932e8c1064e7d4ac48e65d16924010 Mon Sep 17 00:00:00 2001
From: Rishabh Gautam <47270636+Rishabhg71@users.noreply.github.com>
Date: Tue, 10 Oct 2023 00:45:17 +0530
Subject: [PATCH] Zim browser integration #1127 (#846)
Added in-app support to download/browse Zim files
---
i18n/en.jsonp.js | 1 +
i18n/es.jsonp.js | 1 +
i18n/fr.jsonp.js | 1 +
service-worker.js | 1 +
www/css/app.css | 4 ++++
www/index.html | 19 ++++++++++-----
www/js/app.js | 55 +++++++++++++++++++++++++++++++------------
www/js/init.js | 37 ++++++++++++++++++++++++++++-
www/js/lib/uiUtil.js | 29 +++++++++++++++++++----
www/js/lib/zimfile.js | 9 +------
www/library.html | 14 +++++++++++
11 files changed, 137 insertions(+), 34 deletions(-)
create mode 100644 www/library.html
diff --git a/i18n/en.jsonp.js b/i18n/en.jsonp.js
index 2baba11d4..f4233a670 100644
--- a/i18n/en.jsonp.js
+++ b/i18n/en.jsonp.js
@@ -18,6 +18,7 @@ document.localeJson = {
"configure": "Configure",
"configure-title": "Configuration",
"configure-about-usage-link": "About (Usage)",
+ "configure-btn-library": "Browse ZIM Library",
"configure-download-instructions": "This application needs a ZIM archive to work. For full instructions, please see the section",
"configure-select-instructions": "Please select or drag and drop a .zim file (or all the .zimaa, .zimab etc in case of a split ZIM file):",
"configure-selectordisplay": "Drag and drop a new ZIM file, or",
diff --git a/i18n/es.jsonp.js b/i18n/es.jsonp.js
index 5bd090314..623981f3e 100644
--- a/i18n/es.jsonp.js
+++ b/i18n/es.jsonp.js
@@ -18,6 +18,7 @@ document.localeJson = {
"configure": "Configurar",
"configure-title": "Configuración",
"configure-about-usage-link": "Información (Uso)",
+ "configure-btn-library": "Biblioteca ZIM",
"configure-download-instructions": "Esta aplicación necesita un archivo ZIM para funcionar. Para instrucciones completas, vea la sección",
"configure-select-instructions": "Seleccione o arrastre y suelte un archivo .zim (o todos los .zimaa, .zimab etc en caso de un archivo dividido):",
"configure-selectordisplay": "Arrastre y suelte un nuevo archivo ZIM, o",
diff --git a/i18n/fr.jsonp.js b/i18n/fr.jsonp.js
index 0b811fdb5..2d0c0dff3 100644
--- a/i18n/fr.jsonp.js
+++ b/i18n/fr.jsonp.js
@@ -18,6 +18,7 @@ document.localeJson = {
"configure": "Configuration",
"configure-title": "Configuration",
"configure-about-usage-link": "Informations (Utilisation)",
+ "configure-btn-library": "Bibliothèque ZIM",
"configure-download-instructions": "Cette application a besoin d'un fichier ZIM pour fonctionner. Pour des instructions complètes, veuillez consulter la section",
"configure-select-instructions": "Veuillez sélectionner ou glisser-déposer un fichier .zim (ou tous les .zimaa, .zimab etc. dans le cas d'un fichier ZIM découpé) :",
"configure-selectordisplay": "Glisser-déposer un nouveau fichier ZIM, ou",
diff --git a/service-worker.js b/service-worker.js
index 4b7edb008..7c7fa5de5 100644
--- a/service-worker.js
+++ b/service-worker.js
@@ -118,6 +118,7 @@ const precacheFiles = [
'www/img/Icon_External_Link.png',
'www/index.html',
'www/article.html',
+ 'www/library.html',
'www/main.html',
'www/js/app.js',
'www/js/init.js',
diff --git a/www/css/app.css b/www/css/app.css
index c705a0f2d..d9d5586ae 100644
--- a/www/css/app.css
+++ b/www/css/app.css
@@ -100,6 +100,10 @@ input[type="file"] {
display: none;
}
+iframe {
+ border: 0;
+}
+
.custom-file-upload {
border: 2px solid darkgray;
display: inline-block;
diff --git a/www/index.html b/www/index.html
index a5e5b7e19..e1dc67f55 100644
--- a/www/index.html
+++ b/www/index.html
@@ -440,7 +440,9 @@
Other platforms/versions
-
+
+
+
Configuration
@@ -467,11 +469,16 @@
Configuration
Please select or drag and drop a .zim file (or all the .zimaa, .zimab etc in
case of a split ZIM file):
-
+
+
+
+ Only ZIMs with static content (e.g. Wiki-style) are supported in JQuery mode. For information on ZIM compatibility, seeAbout (Usage).
diff --git a/www/js/app.js b/www/js/app.js
index 6b579e8cf..9b50c76e4 100644
--- a/www/js/app.js
+++ b/www/js/app.js
@@ -136,23 +136,33 @@ darkPreference.onchange = function () {
* Resize the IFrame height, so that it fills the whole available height in the window
*/
function resizeIFrame () {
- var headerStyles = getComputedStyle(document.getElementById('top'));
- var iframe = document.getElementById('articleContent');
- var region = document.getElementById('search-article');
- if (iframe.style.display === 'none') {
- // We are in About or Configuration, so we only set the region height
- region.style.height = window.innerHeight + 'px';
- } else {
- // IE cannot retrieve computed headerStyles till the next paint, so we wait a few ticks
- setTimeout(function () {
- // Get header height *including* its bottom margin
- var headerHeight = parseFloat(headerStyles.height) + parseFloat(headerStyles.marginBottom);
- iframe.style.height = window.innerHeight - headerHeight + 'px';
- // We have to allow a minimum safety margin of 10px for 'iframe' and 'header' to fit within 'region'
- region.style.height = window.innerHeight + 10 + 'px';
- }, 100);
+ const headerStyles = getComputedStyle(document.getElementById('top'));
+ const articleContent = document.getElementById('articleContent');
+ const libraryContent = document.getElementById('libraryContent');
+ const frames = [articleContent, libraryContent];
+ const region = document.getElementById('search-article');
+ const nestedFrame = libraryContent.contentWindow.document.getElementById('libraryIframe');
+
+ for (let i = 0; i < frames.length; i++) {
+ const iframe = frames[i];
+ if (iframe.style.display === 'none') {
+ // We are in About or Configuration, so we only set the region height
+ region.style.height = window.innerHeight + 'px';
+ nestedFrame.style.height = window.innerHeight - 110 + 'px';
+ } else {
+ // IE cannot retrieve computed headerStyles till the next paint, so we wait a few ticks
+ setTimeout(function () {
+ // Get header height *including* its bottom margin
+ const headerHeight = parseFloat(headerStyles.height) + parseFloat(headerStyles.marginBottom);
+ iframe.style.height = window.innerHeight - headerHeight + 'px';
+ // We have to allow a minimum safety margin of 10px for 'iframe' and 'header' to fit within 'region'
+ region.style.height = window.innerHeight + 10 + 'px';
+ nestedFrame.style.height = window.innerHeight - 110 + 'px';
+ }, 100);
+ }
}
}
+
document.addEventListener('DOMContentLoaded', function () {
getDefaultLanguageAndTranslateApp();
resizeIFrame();
@@ -1296,6 +1306,21 @@ function handleFileDrop (packet) {
document.getElementById('archiveFiles').value = null;
}
+document.getElementById('libraryBtn').addEventListener('click', function (e) {
+ e.preventDefault();
+
+ const libraryContent = document.getElementById('libraryContent');
+ const iframe = libraryContent.contentWindow.document.getElementById('libraryIframe');
+ try {
+ // eslint-disable-next-line no-new-func
+ Function('try{}catch{}')();
+ iframe.setAttribute('src', params.libraryUrl);
+ uiUtil.tabTransitionToSection('library', params.showUIAnimations);
+ } catch (error) {
+ window.open(params.altLibraryUrl, '_blank')
+ }
+});
+
// Add event listener to link which allows user to show file selectors
document.getElementById('selectorsDisplayLink').addEventListener('click', function (e) {
e.preventDefault();
diff --git a/www/js/init.js b/www/js/init.js
index 9530d2140..bc78891ca 100644
--- a/www/js/init.js
+++ b/www/js/init.js
@@ -29,7 +29,40 @@
* A global parameter object for storing variables that need to be remembered between page loads,
* or across different functions and modules
*
- * @type Object
+ * @typedef {Object} AppParams
+ * @property {string} appVersion - The version number of the application.
+ * @property {string} PWAServer - The URL of the PWA server for use with the browser extensions in ServiceWorker mode.
+ * @property {string} storeType - A parameter to determine the Settings Store API in use.
+ * @property {string} keyPrefix - The key prefix used by the settingsStore.js.
+ * @property {boolean} hideActiveContentWarning - A boolean indicating whether to hide the active content warning.
+ * @property {boolean} showUIAnimations - A boolean indicating whether to show UI animations.
+ * @property {number} maxSearchResultsSize - The maximum number of article titles to return.
+ * @property {boolean} assetsCache - A boolean indicating whether to cache assets.
+ * @property {boolean} appCache - A boolean indicating whether to cache the PWA's code.
+ * @property {string} appTheme - A parameter to set the app theme and, if necessary, the CSS theme for article content.
+ * @property {boolean} useHomeKeyToFocusSearchBar - A global parameter to turn on/off the use of Keyboard HOME Key to focus search bar.
+ * @property {boolean} openExternalLinksInNewTabs - A global parameter to turn on/off opening external links in new tab (for ServiceWorker mode).
+ * @property {string} overrideBrowserLanguage - A global language override.
+ * @property {boolean} disableDragAndDrop - A parameter to disable drag-and-drop.
+ * @property {string} referrerExtensionURL - A parameter to access the URL of any extension that this app was launched from.
+ * @property {boolean} defaultModeChangeAlertDisplayed - A parameter to keep track of the fact that the user has been informed of the switch to SW mode by default.
+ * @property {string} contentInjectionMode - A parameter to set the content injection mode ('jquery' or 'serviceworker') used by this app.
+ * @property {boolean} useCanvasElementsForWebpTranscoding - A parameter to circumvent anti-fingerprinting technology in browsers that do not support WebP natively by substituting images directly with the canvas elements produced by the WebP polyfill.
+ * @property {string} libraryUrl - The URL of the Kiwix library.
+ * @property {string} altLibraryUrl - The alternative URL of the Kiwix library in non-supported browsers.
+ * @property {DecompressorAPI} decompressorAPI
+
+/**
+ * A property of the global params object to track the assembler machine type and the last used decompressor (for reporting to the API panel)
+ * This is populated in the Emscripten wrappers
+ * @typedef {Object} DecompressorAPI
+ * @property {String} assemblerMachineType The assembler machine type supported and/or loaded by this app: 'ASM' or 'WASM'
+ * @property {String} decompressorLastUsed The decompressor that was last used to decode a compressed cluster (currently 'XZ' or 'ZSTD')
+ * @property {String} errorStatus A description of any detected error in loading a decompressor
+ */
+
+/**
+ * @type {AppParams}
*/
var params = {};
@@ -77,6 +110,8 @@ params['contentInjectionMode'] = getSetting('contentInjectionMode') ||
// A parameter to circumvent anti-fingerprinting technology in browsers that do not support WebP natively by substituting images
// directly with the canvas elements produced by the WebP polyfill [kiwix-js #835]. NB This is only currently used in jQuery mode.
params['useCanvasElementsForWebpTranscoding'] = null; // Value is determined in uiUtil.determineCanvasElementsWorkaround(), called when setting the content injection mode
+params['libraryUrl'] = 'https://library.kiwix.org/'; // Url for iframe that will be loaded to download new zim files
+params['altLibraryUrl'] = 'https://download.kiwix.org/zim/'; // Alternative Url for iframe (for use with unsupported browsers) that will be loaded to download new zim files
/**
* Apply any override parameters that might be in the querystring.
diff --git a/www/js/lib/uiUtil.js b/www/js/lib/uiUtil.js
index 3521eea36..c70d165f8 100644
--- a/www/js/lib/uiUtil.js
+++ b/www/js/lib/uiUtil.js
@@ -500,8 +500,9 @@ function removeAnimationClasses () {
const config = document.getElementById('configuration');
const about = document.getElementById('about');
const home = document.getElementById('articleContent');
+ const library = document.getElementById('library');
- const tabs = [config, about, home]
+ const tabs = [config, about, home, library]
tabs.forEach(tab => {
tab.classList.remove('slideIn_L');
tab.classList.remove('slideIn_R');
@@ -560,7 +561,9 @@ function fromSection () {
const isConfigPageVisible = !$('#configuration').is(':hidden');
const isAboutPageVisible = !$('#about').is(':hidden');
const isArticlePageVisible = !$('#articleContent').is(':hidden');
+ const isLibraryPageVisible = !$('#library').is(':hidden');
if (isConfigPageVisible) return 'config';
+ if (isLibraryPageVisible) return 'library';
else if (isAboutPageVisible) return 'about';
else if (isArticlePageVisible) return 'home';
}
@@ -576,6 +579,7 @@ function tabTransitionToSection (toSection, isAnimationRequired = false) {
// all the references of the sections/tabs
const config = document.getElementById('configuration');
const about = document.getElementById('about');
+ const library = document.getElementById('library');
const home = document.getElementById('articleContent');
// references of extra elements that are in UI but not tabs
@@ -594,29 +598,43 @@ function tabTransitionToSection (toSection, isAnimationRequired = false) {
if (toSection === 'home') {
if (from === 'config') slideToRight(home, config);
if (from === 'about') slideToRight(home, about);
+ if (from === 'library') slideToRight(home, library);
+
showElements(extraNavBtns, extraArticleSearch, extraWelcomeText, extraKiwixAlert);
} else if (toSection === 'config') {
if (from === 'about') slideToRight(config, about);
+ if (from === 'library') slideToRight(config, library);
if (from === 'home') slideToLeft(config, home);
+
hideElements(extraNavBtns, extraArticleSearch, extraWelcomeText, extraSearchingArticles, extraKiwixAlert);
} else if (toSection === 'about') {
+ if (from === 'library') slideToRight(about, library);
if (from === 'home') slideToLeft(about, home);
if (from === 'config') slideToLeft(about, config);
+
+ hideElements(extraNavBtns, extraArticleSearch, extraWelcomeText, extraSearchingArticles, extraKiwixAlert);
+ } else if (toSection === 'library') {
+ // it will be always coming from config page
+ slideToLeft(library, config);
hideElements(extraNavBtns, extraArticleSearch, extraWelcomeText, extraSearchingArticles, extraKiwixAlert);
}
} else {
if (toSection === 'home') {
- hideElements(config, about);
+ hideElements(config, about, library);
showElements(home, extraNavBtns, extraArticleSearch, extraWelcomeText);
}
if (toSection === 'config') {
- hideElements(about, home, extraNavBtns, extraArticleSearch, extraWelcomeText, extraSearchingArticles, extraKiwixAlert);
+ hideElements(about, home, library, extraNavBtns, extraArticleSearch, extraWelcomeText, extraSearchingArticles, extraKiwixAlert);
showElements(config);
}
if (toSection === 'about') {
- hideElements(config, home);
+ hideElements(config, home, library);
showElements(about);
}
+ if (toSection === 'library') {
+ hideElements(config, about, home, extraNavBtns, extraArticleSearch, extraWelcomeText, extraSearchingArticles, extraKiwixAlert);
+ showElements(library);
+ }
}
}
@@ -642,6 +660,7 @@ function applyAppTheme (theme) {
var footer = document.querySelector('footer');
var oldTheme = htmlEl.dataset.theme || '';
var iframe = document.getElementById('articleContent');
+ const library = document.getElementById('libraryContent');
var doc = iframe.contentDocument;
var kiwixJSSheet = doc ? doc.getElementById('kiwixJSTheme') || null : null;
var oldAppTheme = oldTheme.replace(/_.*$/, '');
@@ -674,6 +693,7 @@ function applyAppTheme (theme) {
// If there is no ContentTheme or we are applying a different ContentTheme, remove any previously applied ContentTheme
if (oldContentTheme && oldContentTheme !== contentTheme) {
iframe.classList.remove(oldContentTheme);
+ library.classList.remove(oldContentTheme);
if (kiwixJSSheet) {
kiwixJSSheet.disabled = true;
kiwixJSSheet.parentNode.removeChild(kiwixJSSheet);
@@ -682,6 +702,7 @@ function applyAppTheme (theme) {
// Apply the requested ContentTheme (if not already attached)
if (contentTheme && (!kiwixJSSheet || !~kiwixJSSheet.href.search('kiwixJS' + contentTheme + '.css'))) {
iframe.classList.add(contentTheme);
+ library.classList.add(contentTheme);
// Use an absolute reference because Service Worker needs this (if an article loaded in SW mode is in a ZIM
// subdirectory, then relative links injected into the article will not work as expected)
// Note that location.pathname returns the path plus the filename, but is useful because it removes any query string
diff --git a/www/js/lib/zimfile.js b/www/js/lib/zimfile.js
index 69e757fab..407e753af 100644
--- a/www/js/lib/zimfile.js
+++ b/www/js/lib/zimfile.js
@@ -54,14 +54,7 @@ if (!String.prototype.startsWith) {
});
}
-/**
- * A global variable to track the assembler machine type and the last used decompressor (for reporting to the API panel)
- * This is populated in the Emscripten wrappers
- * @type {Object}
- * @property {String} assemblerMachineType The assembler machine type supported and/or loaded by this app: 'ASM' or 'WASM'
- * @property {String} decompressorLastUsed The decompressor that was last used to decode a compressed cluster (currently 'XZ' or 'ZSTD')
- * @property {String} errorStatus A description of any detected error in loading a decompressor
- */
+// to learn more read init.js:57 or search DecompressorAPI in init.js
params.decompressorAPI = {
assemblerMachineType: null,
decompressorLastUsed: null,
diff --git a/www/library.html b/www/library.html
new file mode 100644
index 000000000..19cff84d8
--- /dev/null
+++ b/www/library.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Placeholder for injecting an article into the iframe
+
+
+
+
+
+