314 lines
10 KiB
JavaScript
314 lines
10 KiB
JavaScript
// -*- 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");
|
|
}),
|
|
};
|