Skip to content

Commit

Permalink
Zim browser integration #1127 (#846)
Browse files Browse the repository at this point in the history
Added in-app support to download/browse Zim files
  • Loading branch information
Rishabhg71 committed Oct 9, 2023
1 parent 69f8554 commit 671cd9a
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 34 deletions.
1 change: 1 addition & 0 deletions i18n/en.jsonp.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions i18n/es.jsonp.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions i18n/fr.jsonp.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 4 additions & 0 deletions www/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ input[type="file"] {
display: none;
}

iframe {
border: 0;
}

.custom-file-upload {
border: 2px solid darkgray;
display: inline-block;
Expand Down
19 changes: 13 additions & 6 deletions www/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,9 @@ <h3 id="other" data-i18n="about-other-platforms">Other platforms/versions</h3>
<br/>
<br/>
</div>

<div id='library' style="display: none; color: white;">
<iframe width="100%" id="libraryContent" src="./library.html"></iframe>
</div>
<div id="configuration" style="display: none;">
<div class="container">
<h2 data-i18n="configure-title">Configuration</h2>
Expand All @@ -467,11 +469,16 @@ <h2 data-i18n="configure-title">Configuration</h2>
<div id="openLocalFiles" style="display: none;">
<p data-i18n="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):</p>
<label class="btn btn-light custom-file-upload">
<input type="file" id="archiveFiles" multiple class="btn"
accept="application/octet-stream,.zim,.zimaa,.zimab,.zimac,.zimad,.zimae,.zimaf,.zimag,.zimah,.zimai,.zimaj,.zimak,.zimal,.zimam,.ziman,.zimao,.zimap,.zimaq,.zimar,.zimas,.zimat,.zimau,.zimav,.zimaw,.zimax,.zimay,.zimaz,.zimba,.zimbb,.zimbc,.zimbd,.zimbe,.zimbf,.zimbg,.zimbh,.zimbi,.zimbj,.zimbk,.zimbl,.zimbm,.zimbn,.zimbo,.zimbp,.zimbq,.zimbr,.zimbs,.zimbt,.zimbu,.zimbv,.zimbw,.zimbx,.zimby,.zimbz" />
<span data-i18n="home-btn-fileselect">Select ZIM file(s)</span>
</label>
<span>
<label class="btn btn-light custom-file-upload">
<input type="file" id="archiveFiles" multiple class="btn"
accept="application/octet-stream,.zim,.zimaa,.zimab,.zimac,.zimad,.zimae,.zimaf,.zimag,.zimah,.zimai,.zimaj,.zimak,.zimal,.zimam,.ziman,.zimao,.zimap,.zimaq,.zimar,.zimas,.zimat,.zimau,.zimav,.zimaw,.zimax,.zimay,.zimaz,.zimba,.zimbb,.zimbc,.zimbd,.zimbe,.zimbf,.zimbg,.zimbh,.zimbi,.zimbj,.zimbk,.zimbl,.zimbm,.zimbn,.zimbo,.zimbp,.zimbq,.zimbr,.zimbs,.zimbt,.zimbu,.zimbv,.zimbw,.zimbx,.zimby,.zimbz" />
<span data-i18n="home-btn-fileselect">Select ZIM file(s)</span>
</label>
<label class="btn btn-light custom-file-upload" id="libraryBtn" data-i18n="configure-btn-library">
Browse ZIM Library
</label>
</span>
<br />
<strong id="jqueryCompatibility" data-i18n="configure-static-content">Only ZIMs with static content (e.g. Wiki-style) are supported in JQuery mode.<br /></strong>
<span data-i18n="configure-supportedarchives">For information on ZIM compatibility, see</span> <a href="#usage" data-i18n="configure-about-usage-link" class="aboutLinks">About (Usage)</a>.<br />
Expand Down
55 changes: 40 additions & 15 deletions www/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
37 changes: 36 additions & 1 deletion www/js/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};

Expand Down Expand Up @@ -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.
Expand Down
29 changes: 25 additions & 4 deletions www/js/lib/uiUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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';
}
Expand All @@ -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
Expand All @@ -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);
}
}
}

Expand All @@ -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(/_.*$/, '');
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down
9 changes: 1 addition & 8 deletions www/js/lib/zimfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions www/library.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Placeholder for injecting an article into the iframe</title>
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self' data: * 'unsafe-inline' 'unsafe-eval'; frame-src 'self' moz-extension: chrome-extension: *; object-src 'none';"
/>
</head>
<body>
<iframe width="100%" id="libraryIframe" style="border: none;"></iframe>
</body>
</html>

0 comments on commit 671cd9a

Please sign in to comment.