// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm"); let Reader = { // These values should match those defined in BrowserContract.java. STATUS_UNFETCHED: 0, STATUS_FETCH_FAILED_TEMPORARY: 1, STATUS_FETCH_FAILED_PERMANENT: 2, STATUS_FETCH_FAILED_UNSUPPORTED_FORMAT: 3, STATUS_FETCHED_ARTICLE: 4, observe: function Reader_observe(aMessage, aTopic, aData) { switch (aTopic) { case "Reader:Added": { let mm = window.getGroupMessageManager("browsers"); mm.broadcastAsyncMessage("Reader:Added", { url: aData }); break; } case "Reader:Removed": { let uri = Services.io.newURI(aData, null, null); ReaderMode.removeArticleFromCache(uri).catch(e => Cu.reportError("Error removing article from cache: " + e)); let mm = window.getGroupMessageManager("browsers"); mm.broadcastAsyncMessage("Reader:Removed", { url: aData }); break; } case "Gesture:DoubleTap": { // Ideally, we would just do this all with web APIs in AboutReader.jsm (bug 1118487) if (!BrowserApp.selectedBrowser.currentURI.spec.startsWith("about:reader")) { return; } let win = BrowserApp.selectedBrowser.contentWindow; let scrollBy; // Arbitrary choice of innerHeight (50) to give some context after scroll. if (JSON.parse(aData).y < (win.innerHeight / 2)) { scrollBy = - win.innerHeight + 50; } else { scrollBy = win.innerHeight - 50; } let viewport = BrowserApp.selectedTab.getViewport(); let newY = Math.min(Math.max(viewport.cssY + scrollBy, viewport.cssPageTop), viewport.cssPageBottom); let newRect = new Rect(viewport.cssX, newY, viewport.cssWidth, viewport.cssHeight); ZoomHelper.zoomToRect(newRect, -1); break; } } }, receiveMessage: function(message) { switch (message.name) { case "Reader:AddToList": this.addArticleToReadingList(message.data.article); break; case "Reader:ArticleGet": this._getArticle(message.data.url, message.target).then((article) => { message.target.messageManager.sendAsyncMessage("Reader:ArticleData", { article: article }); }); break; case "Reader:FaviconRequest": { let observer = (s, t, d) => { Services.obs.removeObserver(observer, "Reader:FaviconReturn", false); message.target.messageManager.sendAsyncMessage("Reader:FaviconReturn", JSON.parse(d)); }; Services.obs.addObserver(observer, "Reader:FaviconReturn", false); Messaging.sendRequest({ type: "Reader:FaviconRequest", url: message.data.url }); break; } case "Reader:ListStatusRequest": Messaging.sendRequestForResult({ type: "Reader:ListStatusRequest", url: message.data.url }).then((data) => { message.target.messageManager.sendAsyncMessage("Reader:ListStatusData", JSON.parse(data)); }); break; case "Reader:RemoveFromList": Messaging.sendRequest({ type: "Reader:RemoveFromList", url: message.data.url }); break; case "Reader:Share": Messaging.sendRequest({ type: "Reader:Share", url: message.data.url, title: message.data.title }); break; case "Reader:ShowToast": NativeWindow.toast.show(message.data.toast, "short"); break; case "Reader:SystemUIVisibility": Messaging.sendRequest({ type: "SystemUI:Visibility", visible: message.data.visible }); break; case "Reader:ToolbarVisibility": Messaging.sendRequest({ type: "BrowserToolbar:Visibility", visible: message.data.visible }); break; case "Reader:UpdateReaderButton": { let tab = BrowserApp.getTabForBrowser(message.target); tab.browser.isArticle = message.data.isArticle; this.updatePageAction(tab); break; } } }, pageAction: { readerModeCallback: function(tabID) { Messaging.sendRequest({ type: "Reader:Toggle", tabID: tabID }); }, readerModeActiveCallback: function(tabID) { Reader._addTabToReadingList(tabID).catch(e => Cu.reportError("Error adding tab to reading list: " + e)); UITelemetry.addEvent("save.1", "pageaction", null, "reader"); }, }, updatePageAction: function(tab) { if (!tab.getActive()) { return; } if (this.pageAction.id) { PageActions.remove(this.pageAction.id); delete this.pageAction.id; } let browser = tab.browser; if (browser.currentURI.spec.startsWith("about:reader")) { this.pageAction.id = PageActions.add({ title: Strings.browser.GetStringFromName("readerMode.exit"), icon: "drawable://reader_active", clickCallback: () => this.pageAction.readerModeCallback(tab.id), important: true }); // Only start a reader session if the viewer is in the foreground. We do // not track background reader viewers. UITelemetry.startSession("reader.1", null); return; } // Only stop a reader session if the foreground viewer is not visible. UITelemetry.stopSession("reader.1", "", null); if (browser.isArticle) { this.pageAction.id = PageActions.add({ title: Strings.browser.GetStringFromName("readerMode.enter"), icon: "drawable://reader", clickCallback: () => this.pageAction.readerModeCallback(tab.id), longClickCallback: () => this.pageAction.readerModeActiveCallback(tab.id), important: true }); } }, _addTabToReadingList: Task.async(function* (tabID) { let tab = BrowserApp.getTabForId(tabID); if (!tab) { throw new Error("Can't add tab to reading list because no tab found for ID: " + tabID); } let urlWithoutRef = tab.browser.currentURI.specIgnoringRef; let article = yield this._getArticle(urlWithoutRef, tab.browser).catch(e => { Cu.reportError("Error getting article for tab: " + e); return null; }); if (!article) { // If there was a problem getting the article, just store the // URL and title from the tab. article = { url: urlWithoutRef, title: tab.browser.contentDocument.title, status: this.STATUS_FETCH_FAILED_UNSUPPORTED_FORMAT, }; } else { article.status = this.STATUS_FETCHED_ARTICLE; } this.addArticleToReadingList(article); }), addArticleToReadingList: function(article) { if (!article || !article.url) { Cu.reportError("addArticleToReadingList requires article with valid URL"); return; } Messaging.sendRequest({ type: "Reader:AddToList", url: truncate(article.url, MAX_URI_LENGTH), title: truncate(article.title || "", MAX_TITLE_LENGTH), length: article.length || 0, excerpt: article.excerpt || "", status: article.status, }); ReaderMode.storeArticleInCache(article).catch(e => Cu.reportError("Error storing article in cache: " + e)); }, /** * Gets an article for a given URL. This method will download and parse a document * if it does not find the article in the tab data or the cache. * * @param url The article URL. * @param browser The browser where the article is currently loaded. * @return {Promise} * @resolves JS object representing the article, or null if no article is found. */ _getArticle: Task.async(function* (url, browser) { // First, look for a saved article. let article = yield this._getSavedArticle(browser); if (article && article.url == url) { return article; } // Next, try to find a parsed article in the cache. let uri = Services.io.newURI(url, null, null); article = yield ReaderMode.getArticleFromCache(uri); if (article) { return article; } // Article hasn't been found in the cache, we need to // download the page and parse the article out of it. return yield ReaderMode.downloadAndParseDocument(url); }), _getSavedArticle: function(browser) { return new Promise((resolve, reject) => { let mm = browser.messageManager; let listener = (message) => { mm.removeMessageListener("Reader:SavedArticleData", listener); resolve(message.data.article); }; mm.addMessageListener("Reader:SavedArticleData", listener); mm.sendAsyncMessage("Reader:SavedArticleGet"); }); }, /** * Migrates old indexedDB reader mode cache to new JSON cache. */ migrateCache: Task.async(function* () { let cacheDB = yield new Promise((resolve, reject) => { let request = window.indexedDB.open("about:reader", 1); request.onsuccess = event => resolve(event.target.result); request.onerror = event => reject(request.error); // If there is no DB to migrate, don't do anything. request.onupgradeneeded = event => resolve(null); }); if (!cacheDB) { return; } let articles = yield new Promise((resolve, reject) => { let articles = []; let transaction = cacheDB.transaction(cacheDB.objectStoreNames); let store = transaction.objectStore(cacheDB.objectStoreNames[0]); let request = store.openCursor(); request.onsuccess = event => { let cursor = event.target.result; if (!cursor) { resolve(articles); } else { articles.push(cursor.value); cursor.continue(); } }; request.onerror = event => reject(request.error); }); for (let article of articles) { yield ReaderMode.storeArticleInCache(article); } // Delete the database. window.indexedDB.deleteDatabase("about:reader"); }), };