diff --git a/www/js/app.js b/www/js/app.js index 8f9af7db..b606fddf 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -1,605 +1,606 @@ -/** - * app.js : User Interface implementation - * This file handles the interaction between the application and the user - * - * Copyright 2013 Mossroy - * License GPL v3: - * - * This file is part of Evopedia. - * - * Evopedia 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. - * - * Evopedia 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 Evopedia (file LICENSE-GPLv3.txt). If not, see - */ - -// This uses require.js to structure javascript: -// http://requirejs.org/docs/api.html#define - -define(function(require) { - - var $ = require('jquery'); - - // Evopedia javascript dependencies - var evopediaTitle = require('title'); - var evopediaArchive = require('archive'); - var util = require('util'); - var cookies = require('cookies'); - var geometry = require('geometry'); - var osabstraction = require('osabstraction'); - - // Maximum number of titles to display in a search - var MAX_SEARCH_RESULT_SIZE = 50; - - // Maximum distance (in degrees) where to search for articles around me - // In fact, we use a square around the user, not a circle - // This square has a length of twice the value of this constant - // One degree is ~111 km at the equator - var MAX_DISTANCE_ARTICLES_NEARBY = 0.1; - - var localArchive = null; - - // Define behavior of HTML elements - $('#searchTitles').on('click', function(e) { - searchTitlesFromPrefix($('#prefix').val()); - $("#welcomeText").hide(); - $("#readingArticle").hide(); - if ($('#navbarToggle').is(":visible") && $('#liHomeNav').is(':visible')) { - $('#navbarToggle').click(); - } - }); - $('#formTitleSearch').on('submit', function(e) { - document.getElementById("searchTitles").click(); - return false; - }); - $('#prefix').on('keyup', function(e) { - if (localArchive !== null && localArchive.titleFile !== null) { - onKeyUpPrefix(e); - } - }); - $("#btnArticlesNearby").on("click", function(e) { - searchTitlesNearby(); - $("#welcomeText").hide(); - $("#readingArticle").hide(); - if ($('#navbarToggle').is(":visible") && $('#liHomeNav').is(':visible')) { - $('#navbarToggle').click(); - } - }); - $("#btnRandomArticle").on("click", function(e) { - goToRandomArticle(); - $("#welcomeText").hide(); - $("#readingArticle").hide(); - if ($('#navbarToggle').is(":visible") && $('#liHomeNav').is(':visible')) { - $('#navbarToggle').click(); - } - }); - // Bottom bar : - $('#btnBack').on('click', function(e) { - history.back(); - return false; - }); - $('#btnForward').on('click', function(e) { - history.forward(); - return false; - }); - $('#btnHomeBottom').on('click', function(e) { - $('#btnHome').click(); - return false; - }); - // Top menu : - $('#btnHome').on('click', function(e) { - // Highlight the selected section in the navbar - $('#liHomeNav').attr("class","active"); - $('#liConfigureNav').attr("class",""); - $('#liAboutNav').attr("class",""); - if ($('#navbarToggle').is(":visible") && $('#liHomeNav').is(':visible')) { - $('#navbarToggle').click(); - } - // Show the selected content in the page - $('#about').hide(); - $('#configuration').hide(); - $('#formTitleSearch').show(); - $("#welcomeText").show(); - $('#titleList').show(); - $('#articleContent').show(); - // Give the focus to the search field, and clean up the page contents - $("#prefix").val(""); - $('#prefix').focus(); - $("#titleList").html(""); - $("#readingArticle").hide(); - $("#articleContent").html(""); - return false; - }); - $('#btnConfigure').on('click', function(e) { - // Highlight the selected section in the navbar - $('#liHomeNav').attr("class",""); - $('#liConfigureNav').attr("class","active"); - $('#liAboutNav').attr("class",""); - if ($('#navbarToggle').is(":visible") && $('#liHomeNav').is(':visible')) { - $('#navbarToggle').click(); - } - // Show the selected content in the page - $('#about').hide(); - $('#configuration').show(); - $('#formTitleSearch').hide(); - $("#welcomeText").hide(); - $('#titleList').hide(); - $("#readingArticle").hide(); - $('#articleContent').hide(); - return false; - }); - $('#btnAbout').on('click', function(e) { - // Highlight the selected section in the navbar - $('#liHomeNav').attr("class",""); - $('#liConfigureNav').attr("class",""); - $('#liAboutNav').attr("class","active"); - if ($('#navbarToggle').is(":visible") && $('#liHomeNav').is(':visible')) { - $('#navbarToggle').click(); - } - // Show the selected content in the page - $('#about').show(); - $('#configuration').hide(); - $('#formTitleSearch').hide(); - $("#welcomeText").hide(); - $('#titleList').hide(); - $("#readingArticle").hide(); - $('#articleContent').hide(); - return false; - }); - - // Detect if DeviceStorage is available - var storages = []; - function searchForArchives() { - // If DeviceStorage is available, we look for archives in it - $("#btnConfigure").click(); - $('#scanningForArchives').show(); - evopediaArchive.LocalArchive.scanForArchives(storages, populateDropDownListOfArchives); - } - - if ($.isFunction(navigator.getDeviceStorage)) { - if ($.isFunction(navigator.getDeviceStorages)) { - // The method getDeviceStorages is available (FxOS>=1.1) - // We have to scan all the DeviceStorages, because getDeviceStorage - // only returns the default Device Storage. - // See https://bugzilla.mozilla.org/show_bug.cgi?id=885753 - storages = $.map(navigator.getDeviceStorages("sdcard"), function(s) { - return new osabstraction.StorageFirefoxOS(s); - }); - } - else { - // The method getDeviceStorages is not available (FxOS 1.0) - // The fallback is to use getDeviceStorage - storages[0] = new osabstraction.StorageFirefoxOS( - navigator.getDeviceStorage("sdcard")); - } - } else if ($.isFunction(window.requestFileSystem)) { // cordova - window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function(fs) { - storages[0] = new osabstraction.StoragePhoneGap(fs); - searchForArchives(); - }); - } - - if (storages !== null && storages.length > 0) { - searchForArchives(); - } - else { - // If DeviceStorage is not available, we display the file select components - displayFileSelect(); - if (document.getElementById('archiveFiles').files && document.getElementById('archiveFiles').files.length>0) { - // Archive files are already selected, - setLocalArchiveFromFileSelect(); - } - else { - $("#btnConfigure").click(); - } - } - - - // Display the article when the user goes back in the browser history - window.onpopstate = function(event) { - if (event.state) { - var titleName = event.state.titleName; - goToArticle(titleName); - } - }; - - /** - * Populate the drop-down list of titles with the given list - * @param {type} archiveDirectories - */ - function populateDropDownListOfArchives(archiveDirectories) { - $('#scanningForArchives').hide(); - $('#chooseArchiveFromLocalStorage').show(); - var comboArchiveList = document.getElementById('archiveList'); - comboArchiveList.options.length = 0; - for (var i = 0; i < archiveDirectories.length; i++) { - var archiveDirectory = archiveDirectories[i]; - if (archiveDirectory === "/") { - alert("It looks like you have put some archive files at the root of your sdcard. Please move them in a subdirectory"); - } - else { - comboArchiveList.options[i] = new Option(archiveDirectory, archiveDirectory); - } - } - $('#archiveList').on('change', setLocalArchiveFromArchiveList); - if (comboArchiveList.options.length > 0) { - var lastSelectedArchive = cookies.getItem("lastSelectedArchive"); - if (lastSelectedArchive !== null && lastSelectedArchive !== undefined && lastSelectedArchive !== "") { - // Attempt to select the corresponding item in the list, if it exists - if ($("#archiveList option[value='"+lastSelectedArchive+"']").length > 0) { - $("#archiveList").val(lastSelectedArchive); - } - } - // Set the localArchive as the last selected (or the first one if it has never been selected) - setLocalArchiveFromArchiveList(); - } - else { - alert("Welcome to Evopedia! This application needs a wikipedia archive in your SD-card. Please download one and put it on the SD-card (see About section). Also check that your device is not connected to a computer through USB device storage (which locks the SD-card content)"); - $("#btnAbout").click(); - } - } - - /** - * Sets the localArchive from the selected archive in the drop-down list - */ - function setLocalArchiveFromArchiveList() { - var archiveDirectory = $('#archiveList').val(); - localArchive = new evopediaArchive.LocalArchive(); - localArchive.initializeFromDeviceStorage(storages, archiveDirectory); - cookies.setItem("lastSelectedArchive",archiveDirectory,Infinity); - // The archive is set : go back to home page to start searching - $("#btnHome").click(); - } - - /** - * Displays the zone to select files from the archive - */ - function displayFileSelect() { - $('#openLocalFiles').show(); - $('#archiveFiles').on('change', setLocalArchiveFromFileSelect); - } - - /** - * Sets the localArchive from the File selects populated by user - */ - function setLocalArchiveFromFileSelect() { - localArchive = new evopediaArchive.LocalArchive(); - localArchive.initializeFromArchiveFiles(document.getElementById('archiveFiles').files); - // The archive is set : go back to home page to start searching - $("#btnHome").click(); - } - - /** - * Handle key input in the prefix input zone - * @param {type} evt - */ - function onKeyUpPrefix(evt) { - // Use a timeout, so that very quick typing does not cause a lot of overhead - // It is also necessary for the words suggestions to work inside Firefox OS - if(window.timeoutKeyUpPrefix) { - window.clearTimeout(window.timeoutKeyUpPrefix); - } - window.timeoutKeyUpPrefix = window.setTimeout(function() { - var prefix = $("#prefix").val(); - if (prefix && prefix.length>0) { - $('#searchTitles').click(); - } - } - ,500); - } - - - /** - * Search the index for titles that start with the given prefix (implemented - * with a binary search inside the index file) - * @param {type} prefix - */ - function searchTitlesFromPrefix(prefix) { - $('#searchingForTitles').show(); - $('#configuration').hide(); - $('#articleContent').empty(); - if (localArchive !== null && localArchive.titleFile !== null) { - localArchive.findTitlesWithPrefix(prefix.trim(), MAX_SEARCH_RESULT_SIZE, populateListOfTitles); - } else { - $('#searchingForTitles').hide(); - // We have to remove the focus from the search field, - // so that the keyboard does not stay above the message - $("#searchTitles").focus(); - alert("Archive not set : please select an archive"); - $("#btnConfigure").click(); - } - } - - - /** - * Display the list of titles with the given array of titles - * @param {type} titleArray - */ - function populateListOfTitles(titleArray) { - var titleListDiv = $('#titleList'); - var titleListDivHtml = ""; - for (var i = 0; i < titleArray.length; i++) { - var title = titleArray[i]; - titleListDivHtml += "" + title.getReadableName() + ""; - } - titleListDiv.html(titleListDivHtml); - $("#titleList a").on("click",handleTitleClick); - $('#searchingForTitles').hide(); - } - - - /** - * Checks if the small archive is in use - * If it is, display a warning message about the hyperlinks not working - */ - function checkSmallArchive() { - if (localArchive.language === "small" && !cookies.hasItem("warnedSmallArchive")) { - // The user selected the "small" archive, which is quite incomplete - // 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")) { - $("searchTitles").focus(); - } - alert("You selected the 'small' archive. This archive is OK for testing, but be aware that very few hyperlinks in the articles will work because it's only a very small subset of the English dump."); - // We will not display this warning again for one day - cookies.setItem("warnedSmallArchive",true,86400); - } - } - - - /** - * Handles the click on a title - * @param {type} event - * @returns {undefined} - */ - function handleTitleClick(event) { - // If we use the small archive, a warning should be displayed to the user - checkSmallArchive(); - - var titleId = event.target.getAttribute("titleId"); - $("#titleList").empty(); - findTitleFromTitleIdAndLaunchArticleRead(titleId); - var title = evopediaTitle.Title.parseTitleId(localArchive, titleId); - pushBrowserHistoryState(title.name); - $("#prefix").val(""); - return false; - } - - - /** - * Creates an instance of title from given titleId (including resolving redirects), - * and call the function to read the corresponding article - * @param {type} titleId - */ - function findTitleFromTitleIdAndLaunchArticleRead(titleId) { - if (localArchive.dataFiles && localArchive.dataFiles.length > 0) { - var title = evopediaTitle.Title.parseTitleId(localArchive, titleId); - $("#articleName").html(title.name); - $("#readingArticle").show(); - $("#articleContent").html(""); - if (title.fileNr === 255) { - localArchive.resolveRedirect(title, readArticle); - } - else { - readArticle(title); - } - } - else { - alert("Data files not set"); - } - } - - /** - * Read the article corresponding to the given title - * @param {type} title - */ - function readArticle(title) { - if (title.fileNr === 255) { - localArchive.resolveRedirect(title, readArticle); - } - else { - localArchive.readArticle(title, displayArticleInForm); - } - } - - /** - * Display the the given HTML article in the web page, - * and convert links to javascript calls - * @param {type} title - * @param {type} htmlArticle - */ - function displayArticleInForm(title, htmlArticle) { - $("#readingArticle").hide(); - - // Display the article inside the web page. - $('#articleContent').html(htmlArticle); - - // Compile the regular expressions needed to modify links - var regexOtherLanguage = /^\.?\/?\.\.\/([^\/]+)\/(.*)/; - var regexImageLink = /^.?\/?[^:]+:(.*)/; - - // Convert links into javascript calls - $('#articleContent').find('a').each(function() { - // Store current link's url - var url = $(this).attr("href"); - if (url === null || url === undefined) { - return; - } - var lowerCaseUrl = url.toLowerCase(); - var cssClass = $(this).attr("class"); - - if (cssClass === "new") { - // It's a link to a missing article : display a message - $(this).on('click', function(e) { - alert("Missing article in Wikipedia"); - return false; - }); - } - else if (url.slice(0, 1) === "#") { - // It's an anchor link : do nothing - } - else if (url.substring(0, 4) === "http") { - // It's an external link : open in a new tab - $(this).attr("target", "_blank"); - } - else if (url.match(regexOtherLanguage)) { - // It's a link to another language : change the URL to the online version of wikipedia - // The regular expression extracts $1 as the language, and $2 as the title name - var onlineWikipediaUrl = url.replace(regexOtherLanguage, "https://$1.wikipedia.org/wiki/$2"); - $(this).attr("href", onlineWikipediaUrl); - // Open in a new tab - $(this).attr("target", "_blank"); - } - else if (url.match(regexImageLink) - && (util.endsWith(lowerCaseUrl, ".png") - || util.endsWith(lowerCaseUrl, ".svg") - || util.endsWith(lowerCaseUrl, ".jpg") - || util.endsWith(lowerCaseUrl, ".jpeg"))) { - // It's a link to a file of wikipedia : change the URL to the online version and open in a new tab - var onlineWikipediaUrl = url.replace(regexImageLink, "https://"+localArchive.language+".wikipedia.org/wiki/File:$1"); - $(this).attr("href", onlineWikipediaUrl); - $(this).attr("target", "_blank"); - } - else { - // It's a link to another article : add an onclick event to go to this article - // instead of following the link - if (url.length>=2 && url.substring(0, 2) === "./") { - url = url.substring(2); - } - $(this).on('click', function(e) { - var titleName = decodeURIComponent(url); - pushBrowserHistoryState(titleName); - goToArticle(titleName); - return false; - }); - } - }); - - // Load math images - $('#articleContent').find('img').each(function() { - var image = $(this); - var m = image.attr("src").match(/^\/math.*\/([0-9a-f]{32})\.png$/); - if (m) { - localArchive.loadMathImage(m[1], function(data) { - image.attr("src", 'data:image/png;base64,' + data); - }); - } - }); - } - - /** - * Changes the URL of the browser page - * @param {type} titleName - */ - function pushBrowserHistoryState(titleName) { - if (titleName) { - var stateObj = {titleName: titleName}; - window.history.pushState(stateObj, "Wikipedia Article : " + titleName, "?title=" + titleName); - } - } - - - /** - * Replace article content with the one of the given title - * @param {type} titleName - * @returns {undefined} - */ - function goToArticle(titleName) { - localArchive.getTitleByName(titleName, function(title) { - if (title === null || title === undefined) { - $("#readingArticle").hide(); - alert("Article with title " + titleName + " not found in the archive"); - } - else { - $("#articleName").html(titleName); - $("#readingArticle").show(); - $("#articleContent").html(""); - readArticle(title); - } - }); - } - - /** - * Looks for titles located around where the device is geolocated - */ - function searchTitlesNearby() { - $('#searchingForTitles').show(); - $('#configuration').hide(); - $('#articleContent').empty(); - if (localArchive !== null && localArchive.titleFile !== null) { - //var rectangle = new geometry.rect(0,40,10,10); - //localArchive.getTitlesInCoords(rectangle, MAX_SEARCH_RESULT_SIZE, populateListOfTitles); - if (navigator.geolocation) { - var geo_options = { - enableHighAccuracy: false, - maximumAge: 1800000, // 30 minutes - timeout : 10000 // 10 seconds - }; - - function geo_success(pos) { - var crd = pos.coords; - - var rectangle = new geometry.rect( - crd.longitude - MAX_DISTANCE_ARTICLES_NEARBY, - crd.latitude - MAX_DISTANCE_ARTICLES_NEARBY, - MAX_DISTANCE_ARTICLES_NEARBY * 2, - MAX_DISTANCE_ARTICLES_NEARBY * 2); - - localArchive.getTitlesInCoords(rectangle, MAX_SEARCH_RESULT_SIZE, populateListOfTitles); - }; - - function geo_error(err) { - alert("Unable to geolocate your device : " + err.code + " : " + err.message); - }; - - // TODO : for testing purpose - //navigator.geolocation.getCurrentPosition(geo_success, geo_error, geo_options); - var longitude = $('#longitude').val(); - var latitude = $('#latitude').val(); - var rectangle = new geometry.rect( - longitude - MAX_DISTANCE_ARTICLES_NEARBY, - latitude - MAX_DISTANCE_ARTICLES_NEARBY, - MAX_DISTANCE_ARTICLES_NEARBY * 2, - MAX_DISTANCE_ARTICLES_NEARBY * 2); - - localArchive.getTitlesInCoords(rectangle, MAX_SEARCH_RESULT_SIZE, populateListOfTitles); - } - else { - alert("Geolocation is not supported (or disabled) on your device, or by your browser"); - } - } else { - $('#searchingForTitles').hide(); - // We have to remove the focus from the search field, - // so that the keyboard does not stay above the message - $("#searchTitles").focus(); - alert("Archive not set : please select an archive"); - $("#btnConfigure").click(); - } - } - - function goToRandomArticle() { - localArchive.getRandomTitle(function(title) { - if (title === null || title === undefined) { - alert("Error finding random article."); - } - else { - $("#articleName").html(title.name); - $("#readingArticle").show(); - $("#articleContent").html(""); - readArticle(title); - } - }); - } - -}); +/** + * app.js : User Interface implementation + * This file handles the interaction between the application and the user + * + * Copyright 2013 Mossroy + * License GPL v3: + * + * This file is part of Evopedia. + * + * Evopedia 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. + * + * Evopedia 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 Evopedia (file LICENSE-GPLv3.txt). If not, see + */ + +// This uses require.js to structure javascript: +// http://requirejs.org/docs/api.html#define + +define(function(require) { + + var $ = require('jquery'); + + // Evopedia javascript dependencies + var evopediaTitle = require('title'); + var evopediaArchive = require('archive'); + var util = require('util'); + var cookies = require('cookies'); + var geometry = require('geometry'); + var osabstraction = require('osabstraction'); + + // Maximum number of titles to display in a search + var MAX_SEARCH_RESULT_SIZE = 50; + + // Maximum distance (in degrees) where to search for articles around me + // In fact, we use a square around the user, not a circle + // This square has a length of twice the value of this constant + // One degree is ~111 km at the equator + var MAX_DISTANCE_ARTICLES_NEARBY = 0.1; + + var localArchive = null; + + // Define behavior of HTML elements + $('#searchTitles').on('click', function(e) { + searchTitlesFromPrefix($('#prefix').val()); + $("#welcomeText").hide(); + $("#readingArticle").hide(); + if ($('#navbarToggle').is(":visible") && $('#liHomeNav').is(':visible')) { + $('#navbarToggle').click(); + } + }); + $('#formTitleSearch').on('submit', function(e) { + document.getElementById("searchTitles").click(); + return false; + }); + $('#prefix').on('keyup', function(e) { + if (localArchive !== null && localArchive.titleFile !== null) { + onKeyUpPrefix(e); + } + }); + $("#btnArticlesNearby").on("click", function(e) { + searchTitlesNearby(); + $("#welcomeText").hide(); + $("#readingArticle").hide(); + if ($('#navbarToggle').is(":visible") && $('#liHomeNav').is(':visible')) { + $('#navbarToggle').click(); + } + }); + $("#btnRandomArticle").on("click", function(e) { + goToRandomArticle(); + $("#welcomeText").hide(); + $('#titleList').hide(); + $("#readingArticle").hide(); + if ($('#navbarToggle').is(":visible") && $('#liHomeNav').is(':visible')) { + $('#navbarToggle').click(); + } + }); + // Bottom bar : + $('#btnBack').on('click', function(e) { + history.back(); + return false; + }); + $('#btnForward').on('click', function(e) { + history.forward(); + return false; + }); + $('#btnHomeBottom').on('click', function(e) { + $('#btnHome').click(); + return false; + }); + // Top menu : + $('#btnHome').on('click', function(e) { + // Highlight the selected section in the navbar + $('#liHomeNav').attr("class","active"); + $('#liConfigureNav').attr("class",""); + $('#liAboutNav').attr("class",""); + if ($('#navbarToggle').is(":visible") && $('#liHomeNav').is(':visible')) { + $('#navbarToggle').click(); + } + // Show the selected content in the page + $('#about').hide(); + $('#configuration').hide(); + $('#formTitleSearch').show(); + $("#welcomeText").show(); + $('#titleList').show(); + $('#articleContent').show(); + // Give the focus to the search field, and clean up the page contents + $("#prefix").val(""); + $('#prefix').focus(); + $("#titleList").html(""); + $("#readingArticle").hide(); + $("#articleContent").html(""); + return false; + }); + $('#btnConfigure').on('click', function(e) { + // Highlight the selected section in the navbar + $('#liHomeNav').attr("class",""); + $('#liConfigureNav').attr("class","active"); + $('#liAboutNav').attr("class",""); + if ($('#navbarToggle').is(":visible") && $('#liHomeNav').is(':visible')) { + $('#navbarToggle').click(); + } + // Show the selected content in the page + $('#about').hide(); + $('#configuration').show(); + $('#formTitleSearch').hide(); + $("#welcomeText").hide(); + $('#titleList').hide(); + $("#readingArticle").hide(); + $('#articleContent').hide(); + return false; + }); + $('#btnAbout').on('click', function(e) { + // Highlight the selected section in the navbar + $('#liHomeNav').attr("class",""); + $('#liConfigureNav').attr("class",""); + $('#liAboutNav').attr("class","active"); + if ($('#navbarToggle').is(":visible") && $('#liHomeNav').is(':visible')) { + $('#navbarToggle').click(); + } + // Show the selected content in the page + $('#about').show(); + $('#configuration').hide(); + $('#formTitleSearch').hide(); + $("#welcomeText").hide(); + $('#titleList').hide(); + $("#readingArticle").hide(); + $('#articleContent').hide(); + return false; + }); + + // Detect if DeviceStorage is available + var storages = []; + function searchForArchives() { + // If DeviceStorage is available, we look for archives in it + $("#btnConfigure").click(); + $('#scanningForArchives').show(); + evopediaArchive.LocalArchive.scanForArchives(storages, populateDropDownListOfArchives); + } + + if ($.isFunction(navigator.getDeviceStorage)) { + if ($.isFunction(navigator.getDeviceStorages)) { + // The method getDeviceStorages is available (FxOS>=1.1) + // We have to scan all the DeviceStorages, because getDeviceStorage + // only returns the default Device Storage. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=885753 + storages = $.map(navigator.getDeviceStorages("sdcard"), function(s) { + return new osabstraction.StorageFirefoxOS(s); + }); + } + else { + // The method getDeviceStorages is not available (FxOS 1.0) + // The fallback is to use getDeviceStorage + storages[0] = new osabstraction.StorageFirefoxOS( + navigator.getDeviceStorage("sdcard")); + } + } else if ($.isFunction(window.requestFileSystem)) { // cordova + window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function(fs) { + storages[0] = new osabstraction.StoragePhoneGap(fs); + searchForArchives(); + }); + } + + if (storages !== null && storages.length > 0) { + searchForArchives(); + } + else { + // If DeviceStorage is not available, we display the file select components + displayFileSelect(); + if (document.getElementById('archiveFiles').files && document.getElementById('archiveFiles').files.length>0) { + // Archive files are already selected, + setLocalArchiveFromFileSelect(); + } + else { + $("#btnConfigure").click(); + } + } + + + // Display the article when the user goes back in the browser history + window.onpopstate = function(event) { + if (event.state) { + var titleName = event.state.titleName; + goToArticle(titleName); + } + }; + + /** + * Populate the drop-down list of titles with the given list + * @param {type} archiveDirectories + */ + function populateDropDownListOfArchives(archiveDirectories) { + $('#scanningForArchives').hide(); + $('#chooseArchiveFromLocalStorage').show(); + var comboArchiveList = document.getElementById('archiveList'); + comboArchiveList.options.length = 0; + for (var i = 0; i < archiveDirectories.length; i++) { + var archiveDirectory = archiveDirectories[i]; + if (archiveDirectory === "/") { + alert("It looks like you have put some archive files at the root of your sdcard. Please move them in a subdirectory"); + } + else { + comboArchiveList.options[i] = new Option(archiveDirectory, archiveDirectory); + } + } + $('#archiveList').on('change', setLocalArchiveFromArchiveList); + if (comboArchiveList.options.length > 0) { + var lastSelectedArchive = cookies.getItem("lastSelectedArchive"); + if (lastSelectedArchive !== null && lastSelectedArchive !== undefined && lastSelectedArchive !== "") { + // Attempt to select the corresponding item in the list, if it exists + if ($("#archiveList option[value='"+lastSelectedArchive+"']").length > 0) { + $("#archiveList").val(lastSelectedArchive); + } + } + // Set the localArchive as the last selected (or the first one if it has never been selected) + setLocalArchiveFromArchiveList(); + } + else { + alert("Welcome to Evopedia! This application needs a wikipedia archive in your SD-card. Please download one and put it on the SD-card (see About section). Also check that your device is not connected to a computer through USB device storage (which locks the SD-card content)"); + $("#btnAbout").click(); + } + } + + /** + * Sets the localArchive from the selected archive in the drop-down list + */ + function setLocalArchiveFromArchiveList() { + var archiveDirectory = $('#archiveList').val(); + localArchive = new evopediaArchive.LocalArchive(); + localArchive.initializeFromDeviceStorage(storages, archiveDirectory); + cookies.setItem("lastSelectedArchive",archiveDirectory,Infinity); + // The archive is set : go back to home page to start searching + $("#btnHome").click(); + } + + /** + * Displays the zone to select files from the archive + */ + function displayFileSelect() { + $('#openLocalFiles').show(); + $('#archiveFiles').on('change', setLocalArchiveFromFileSelect); + } + + /** + * Sets the localArchive from the File selects populated by user + */ + function setLocalArchiveFromFileSelect() { + localArchive = new evopediaArchive.LocalArchive(); + localArchive.initializeFromArchiveFiles(document.getElementById('archiveFiles').files); + // The archive is set : go back to home page to start searching + $("#btnHome").click(); + } + + /** + * Handle key input in the prefix input zone + * @param {type} evt + */ + function onKeyUpPrefix(evt) { + // Use a timeout, so that very quick typing does not cause a lot of overhead + // It is also necessary for the words suggestions to work inside Firefox OS + if(window.timeoutKeyUpPrefix) { + window.clearTimeout(window.timeoutKeyUpPrefix); + } + window.timeoutKeyUpPrefix = window.setTimeout(function() { + var prefix = $("#prefix").val(); + if (prefix && prefix.length>0) { + $('#searchTitles').click(); + } + } + ,500); + } + + + /** + * Search the index for titles that start with the given prefix (implemented + * with a binary search inside the index file) + * @param {type} prefix + */ + function searchTitlesFromPrefix(prefix) { + $('#searchingForTitles').show(); + $('#configuration').hide(); + $('#articleContent').empty(); + if (localArchive !== null && localArchive.titleFile !== null) { + localArchive.findTitlesWithPrefix(prefix.trim(), MAX_SEARCH_RESULT_SIZE, populateListOfTitles); + } else { + $('#searchingForTitles').hide(); + // We have to remove the focus from the search field, + // so that the keyboard does not stay above the message + $("#searchTitles").focus(); + alert("Archive not set : please select an archive"); + $("#btnConfigure").click(); + } + } + + + /** + * Display the list of titles with the given array of titles + * @param {type} titleArray + */ + function populateListOfTitles(titleArray) { + var titleListDiv = $('#titleList'); + var titleListDivHtml = ""; + for (var i = 0; i < titleArray.length; i++) { + var title = titleArray[i]; + titleListDivHtml += "" + title.getReadableName() + ""; + } + titleListDiv.html(titleListDivHtml); + $("#titleList a").on("click",handleTitleClick); + $('#searchingForTitles').hide(); + } + + + /** + * Checks if the small archive is in use + * If it is, display a warning message about the hyperlinks not working + */ + function checkSmallArchive() { + if (localArchive.language === "small" && !cookies.hasItem("warnedSmallArchive")) { + // The user selected the "small" archive, which is quite incomplete + // 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")) { + $("searchTitles").focus(); + } + alert("You selected the 'small' archive. This archive is OK for testing, but be aware that very few hyperlinks in the articles will work because it's only a very small subset of the English dump."); + // We will not display this warning again for one day + cookies.setItem("warnedSmallArchive",true,86400); + } + } + + + /** + * Handles the click on a title + * @param {type} event + * @returns {undefined} + */ + function handleTitleClick(event) { + // If we use the small archive, a warning should be displayed to the user + checkSmallArchive(); + + var titleId = event.target.getAttribute("titleId"); + $("#titleList").empty(); + findTitleFromTitleIdAndLaunchArticleRead(titleId); + var title = evopediaTitle.Title.parseTitleId(localArchive, titleId); + pushBrowserHistoryState(title.name); + $("#prefix").val(""); + return false; + } + + + /** + * Creates an instance of title from given titleId (including resolving redirects), + * and call the function to read the corresponding article + * @param {type} titleId + */ + function findTitleFromTitleIdAndLaunchArticleRead(titleId) { + if (localArchive.dataFiles && localArchive.dataFiles.length > 0) { + var title = evopediaTitle.Title.parseTitleId(localArchive, titleId); + $("#articleName").html(title.name); + $("#readingArticle").show(); + $("#articleContent").html(""); + if (title.fileNr === 255) { + localArchive.resolveRedirect(title, readArticle); + } + else { + readArticle(title); + } + } + else { + alert("Data files not set"); + } + } + + /** + * Read the article corresponding to the given title + * @param {type} title + */ + function readArticle(title) { + if (title.fileNr === 255) { + localArchive.resolveRedirect(title, readArticle); + } + else { + localArchive.readArticle(title, displayArticleInForm); + } + } + + /** + * Display the the given HTML article in the web page, + * and convert links to javascript calls + * @param {type} title + * @param {type} htmlArticle + */ + function displayArticleInForm(title, htmlArticle) { + $("#readingArticle").hide(); + + // Display the article inside the web page. + $('#articleContent').html(htmlArticle); + + // Compile the regular expressions needed to modify links + var regexOtherLanguage = /^\.?\/?\.\.\/([^\/]+)\/(.*)/; + var regexImageLink = /^.?\/?[^:]+:(.*)/; + + // Convert links into javascript calls + $('#articleContent').find('a').each(function() { + // Store current link's url + var url = $(this).attr("href"); + if (url === null || url === undefined) { + return; + } + var lowerCaseUrl = url.toLowerCase(); + var cssClass = $(this).attr("class"); + + if (cssClass === "new") { + // It's a link to a missing article : display a message + $(this).on('click', function(e) { + alert("Missing article in Wikipedia"); + return false; + }); + } + else if (url.slice(0, 1) === "#") { + // It's an anchor link : do nothing + } + else if (url.substring(0, 4) === "http") { + // It's an external link : open in a new tab + $(this).attr("target", "_blank"); + } + else if (url.match(regexOtherLanguage)) { + // It's a link to another language : change the URL to the online version of wikipedia + // The regular expression extracts $1 as the language, and $2 as the title name + var onlineWikipediaUrl = url.replace(regexOtherLanguage, "https://$1.wikipedia.org/wiki/$2"); + $(this).attr("href", onlineWikipediaUrl); + // Open in a new tab + $(this).attr("target", "_blank"); + } + else if (url.match(regexImageLink) + && (util.endsWith(lowerCaseUrl, ".png") + || util.endsWith(lowerCaseUrl, ".svg") + || util.endsWith(lowerCaseUrl, ".jpg") + || util.endsWith(lowerCaseUrl, ".jpeg"))) { + // It's a link to a file of wikipedia : change the URL to the online version and open in a new tab + var onlineWikipediaUrl = url.replace(regexImageLink, "https://"+localArchive.language+".wikipedia.org/wiki/File:$1"); + $(this).attr("href", onlineWikipediaUrl); + $(this).attr("target", "_blank"); + } + else { + // It's a link to another article : add an onclick event to go to this article + // instead of following the link + if (url.length>=2 && url.substring(0, 2) === "./") { + url = url.substring(2); + } + $(this).on('click', function(e) { + var titleName = decodeURIComponent(url); + pushBrowserHistoryState(titleName); + goToArticle(titleName); + return false; + }); + } + }); + + // Load math images + $('#articleContent').find('img').each(function() { + var image = $(this); + var m = image.attr("src").match(/^\/math.*\/([0-9a-f]{32})\.png$/); + if (m) { + localArchive.loadMathImage(m[1], function(data) { + image.attr("src", 'data:image/png;base64,' + data); + }); + } + }); + } + + /** + * Changes the URL of the browser page + * @param {type} titleName + */ + function pushBrowserHistoryState(titleName) { + if (titleName) { + var stateObj = {titleName: titleName}; + window.history.pushState(stateObj, "Wikipedia Article : " + titleName, "?title=" + titleName); + } + } + + + /** + * Replace article content with the one of the given title + * @param {type} titleName + * @returns {undefined} + */ + function goToArticle(titleName) { + localArchive.getTitleByName(titleName, function(title) { + if (title === null || title === undefined) { + $("#readingArticle").hide(); + alert("Article with title " + titleName + " not found in the archive"); + } + else { + $("#articleName").html(titleName); + $("#readingArticle").show(); + $("#articleContent").html(""); + readArticle(title); + } + }); + } + + /** + * Looks for titles located around where the device is geolocated + */ + function searchTitlesNearby() { + $('#searchingForTitles').show(); + $('#configuration').hide(); + $('#articleContent').empty(); + if (localArchive !== null && localArchive.titleFile !== null) { + //var rectangle = new geometry.rect(0,40,10,10); + //localArchive.getTitlesInCoords(rectangle, MAX_SEARCH_RESULT_SIZE, populateListOfTitles); + if (navigator.geolocation) { + var geo_options = { + enableHighAccuracy: false, + maximumAge: 1800000, // 30 minutes + timeout : 10000 // 10 seconds + }; + + function geo_success(pos) { + var crd = pos.coords; + + var rectangle = new geometry.rect( + crd.longitude - MAX_DISTANCE_ARTICLES_NEARBY, + crd.latitude - MAX_DISTANCE_ARTICLES_NEARBY, + MAX_DISTANCE_ARTICLES_NEARBY * 2, + MAX_DISTANCE_ARTICLES_NEARBY * 2); + + localArchive.getTitlesInCoords(rectangle, MAX_SEARCH_RESULT_SIZE, populateListOfTitles); + }; + + function geo_error(err) { + alert("Unable to geolocate your device : " + err.code + " : " + err.message); + }; + + // TODO : for testing purpose + //navigator.geolocation.getCurrentPosition(geo_success, geo_error, geo_options); + var longitude = $('#longitude').val(); + var latitude = $('#latitude').val(); + var rectangle = new geometry.rect( + longitude - MAX_DISTANCE_ARTICLES_NEARBY, + latitude - MAX_DISTANCE_ARTICLES_NEARBY, + MAX_DISTANCE_ARTICLES_NEARBY * 2, + MAX_DISTANCE_ARTICLES_NEARBY * 2); + + localArchive.getTitlesInCoords(rectangle, MAX_SEARCH_RESULT_SIZE, populateListOfTitles); + } + else { + alert("Geolocation is not supported (or disabled) on your device, or by your browser"); + } + } else { + $('#searchingForTitles').hide(); + // We have to remove the focus from the search field, + // so that the keyboard does not stay above the message + $("#searchTitles").focus(); + alert("Archive not set : please select an archive"); + $("#btnConfigure").click(); + } + } + + function goToRandomArticle() { + localArchive.getRandomTitle(function(title) { + if (title === null || title === undefined) { + alert("Error finding random article."); + } + else { + $("#articleName").html(title.name); + $("#readingArticle").show(); + $("#articleContent").html(""); + readArticle(title); + } + }); + } + +}); diff --git a/www/js/lib/archive.js b/www/js/lib/archive.js index b37b1d69..b52a9f22 100644 --- a/www/js/lib/archive.js +++ b/www/js/lib/archive.js @@ -24,8 +24,6 @@ define(function(require) { // Module dependencies var normalize_string = require('normalize_string'); - var utf8 = require('utf8'); - var evopediaTitle = require('title'); var util = require('util'); var geometry = require('geometry'); var jQuery = require('jquery'); @@ -110,7 +108,7 @@ define(function(require) { currentLocalArchiveInstance.readDataFilesFromStorage(storage, directory, index + 1); }, function(error) { - // TODO there must be a better to way to detect a FileNotFound + // TODO there must be a better way to detect a FileNotFound if (error != "NotFoundError") { alert("Error reading data file " + index + " in directory " + directory + " : " + error); @@ -141,7 +139,7 @@ define(function(require) { currentLocalArchiveInstance.readCoordinateFilesFromStorage(storage, directory, index + 1); }, function(error) { - // TODO there must be a better to way to detect a FileNotFound + // TODO there must be a better way to detect a FileNotFound if (error != "NotFoundError") { alert("Error reading coordinates file " + index + " in directory " + directory + " : " + error); @@ -372,6 +370,8 @@ define(function(require) { var iterator = new titleIterators.SequentialTitleIterator(that, offset); // call advance twice because we are probably not at the beginning // of a title + // TODO : we need to find a better way to reach the beginning of the title + // As it is now, the first advance() can fail on utf8 decoding return iterator.advance().then(function() { return iterator.advance(); }); @@ -842,7 +842,7 @@ define(function(require) { return storage.scanForDirectoriesContainingFile('titles.idx') .then(function(dirs) { jQuery.merge(directories, dirs); - return true + return true; }); }); jQuery.when.apply(null, promises).then(function() { @@ -880,7 +880,7 @@ define(function(require) { if (this.normalizedTitles === true) { return normalize_string.normalizeString; } else { - return function(string) { return string; } + return function(string) { return string; }; } }; diff --git a/www/js/lib/titleIterators.js b/www/js/lib/titleIterators.js index 3ee55192..c5ce6ffe 100644 --- a/www/js/lib/titleIterators.js +++ b/www/js/lib/titleIterators.js @@ -53,7 +53,7 @@ define(['utf8', 'title', 'util', 'jquery'], function(utf8, evopediaTitle, util, return util.readFileSlice(this._titleFile, this._offset, this._offset + MAX_TITLE_LENGTH).then(function(byteArray) { var newLineIndex = 15; - while (newLineIndex < byteArray.length && byteArray[newLineIndex] != 10) { + while (newLineIndex < byteArray.length && byteArray[newLineIndex] !== 10) { newLineIndex++; } var encodedTitle = byteArray.subarray(0, newLineIndex);