Initial updates for Reader View.
|
|
@ -285,7 +285,7 @@
|
|||
<key id="key_fullScreen_old" key="&fullScreenCmd.macCommandKey;" command="View:FullScreen" modifiers="accel,shift"/>
|
||||
<key keycode="VK_F11" command="View:FullScreen"/>
|
||||
#endif
|
||||
<key id="toggleReaderMode" key="&toggleReaderMode.key;" command="View:ReaderView" modifiers="accel,alt" disabled="true"/>
|
||||
<key id="key_toggleReaderMode" key="&toggleReaderMode.key;" command="View:ReaderView" modifiers="accel,alt" disabled="true"/>
|
||||
<key key="&reloadCmd.commandkey;" command="Browser:Reload" modifiers="accel" id="key_reload"/>
|
||||
<key key="&reloadCmd.commandkey;" command="Browser:ReloadSkipCache" modifiers="accel,shift"/>
|
||||
<key id="key_viewSource" key="&pageSourceCmd.commandkey;" command="View:PageSource" modifiers="accel"/>
|
||||
|
|
|
|||
|
|
@ -11,31 +11,27 @@ this.EXPORTED_SYMBOLS = [ "ReaderParent" ];
|
|||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "UITour", "resource:///modules/UITour.jsm");
|
||||
|
||||
const gStringBundle = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
|
||||
|
||||
var ReaderParent = {
|
||||
_readerModeInfoPanelOpen: false,
|
||||
|
||||
MESSAGES: [
|
||||
"Reader:ArticleGet",
|
||||
"Reader:FaviconRequest",
|
||||
"Reader:UpdateReaderButton",
|
||||
],
|
||||
|
||||
init: function() {
|
||||
init() {
|
||||
let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
|
||||
for (let msg of this.MESSAGES) {
|
||||
mm.addMessageListener(msg, this);
|
||||
}
|
||||
},
|
||||
|
||||
receiveMessage: function(message) {
|
||||
receiveMessage(message) {
|
||||
switch (message.name) {
|
||||
case "Reader:ArticleGet":
|
||||
this._getArticle(message.data.url, message.target).then((article) => {
|
||||
|
|
@ -60,7 +56,7 @@ var ReaderParent = {
|
|||
message.target.messageManager.sendAsyncMessage("Reader:FaviconReturn", {
|
||||
url: message.data.url,
|
||||
faviconUrl: favicon.path.replace(/^favicon:/, "")
|
||||
})
|
||||
});
|
||||
},
|
||||
function onRejection(reason) {
|
||||
Cu.reportError("Error requesting favicon URL for about:reader content: " + reason);
|
||||
|
|
@ -80,7 +76,7 @@ var ReaderParent = {
|
|||
}
|
||||
},
|
||||
|
||||
updateReaderButton: function(browser) {
|
||||
updateReaderButton(browser) {
|
||||
let win = browser.ownerGlobal;
|
||||
if (browser != win.gBrowser.selectedBrowser) {
|
||||
return;
|
||||
|
|
@ -88,7 +84,11 @@ var ReaderParent = {
|
|||
|
||||
let button = win.document.getElementById("reader-mode-button");
|
||||
let command = win.document.getElementById("View:ReaderView");
|
||||
let key = win.document.getElementById("toggleReaderMode");
|
||||
let key = win.document.getElementById("key_toggleReaderMode");
|
||||
// aria-reader is not a real ARIA attribute. However, this will cause
|
||||
// Gecko accessibility to expose the "reader" object attribute. We do this
|
||||
// so that the reader state is easy for accessibility clients to access
|
||||
// programmatically.
|
||||
if (browser.currentURI.spec.startsWith("about:reader")) {
|
||||
button.setAttribute("readeractive", true);
|
||||
button.hidden = false;
|
||||
|
|
@ -98,6 +98,7 @@ var ReaderParent = {
|
|||
command.setAttribute("hidden", false);
|
||||
command.setAttribute("accesskey", gStringBundle.GetStringFromName("readerView.close.accesskey"));
|
||||
key.setAttribute("disabled", false);
|
||||
browser.setAttribute("aria-reader", "active");
|
||||
} else {
|
||||
button.removeAttribute("readeractive");
|
||||
button.hidden = !browser.isArticle;
|
||||
|
|
@ -107,24 +108,15 @@ var ReaderParent = {
|
|||
command.setAttribute("hidden", !browser.isArticle);
|
||||
command.setAttribute("accesskey", gStringBundle.GetStringFromName("readerView.enter.accesskey"));
|
||||
key.setAttribute("disabled", !browser.isArticle);
|
||||
}
|
||||
|
||||
let currentUriHost = browser.currentURI && browser.currentURI.asciiHost;
|
||||
if (browser.isArticle &&
|
||||
!Services.prefs.getBoolPref("browser.reader.detectedFirstArticle") &&
|
||||
currentUriHost && !currentUriHost.endsWith("mozilla.org")) {
|
||||
this.showReaderModeInfoPanel(browser);
|
||||
Services.prefs.setBoolPref("browser.reader.detectedFirstArticle", true);
|
||||
this._readerModeInfoPanelOpen = true;
|
||||
} else if (this._readerModeInfoPanelOpen) {
|
||||
if (UITour.isInfoOnTarget(win, "readerMode-urlBar")) {
|
||||
UITour.hideInfo(win);
|
||||
if (browser.isArticle) {
|
||||
browser.setAttribute("aria-reader", "available");
|
||||
} else {
|
||||
browser.removeAttribute("aria-reader");
|
||||
}
|
||||
this._readerModeInfoPanelOpen = false;
|
||||
}
|
||||
},
|
||||
|
||||
forceShowReaderIcon: function(browser) {
|
||||
forceShowReaderIcon(browser) {
|
||||
browser.isArticle = true;
|
||||
this.updateReaderButton(browser);
|
||||
},
|
||||
|
|
@ -136,35 +128,12 @@ var ReaderParent = {
|
|||
this.toggleReaderMode(event);
|
||||
},
|
||||
|
||||
toggleReaderMode: function(event) {
|
||||
toggleReaderMode(event) {
|
||||
let win = event.target.ownerGlobal;
|
||||
let browser = win.gBrowser.selectedBrowser;
|
||||
browser.messageManager.sendAsyncMessage("Reader:ToggleReaderMode");
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows an info panel from the UITour for Reader Mode.
|
||||
*
|
||||
* @param browser The <browser> that the tour should be started for.
|
||||
*/
|
||||
showReaderModeInfoPanel(browser) {
|
||||
let win = browser.ownerGlobal;
|
||||
let targetPromise = UITour.getTarget(win, "readerMode-urlBar");
|
||||
targetPromise.then(target => {
|
||||
let browserBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
|
||||
let icon = "chrome://browser/skin/";
|
||||
if (win.devicePixelRatio > 1) {
|
||||
icon += "reader-tour@2x.png";
|
||||
} else {
|
||||
icon += "reader-tour.png";
|
||||
}
|
||||
UITour.showInfo(win, target,
|
||||
browserBundle.GetStringFromName("readingList.promo.firstUse.readerView.title"),
|
||||
browserBundle.GetStringFromName("readingList.promo.firstUse.readerView.body"),
|
||||
icon);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets an article for a given URL. This method will download and parse a document.
|
||||
*
|
||||
|
|
@ -173,8 +142,8 @@ var ReaderParent = {
|
|||
* @return {Promise}
|
||||
* @resolves JS object representing the article, or null if no article is found.
|
||||
*/
|
||||
_getArticle: Task.async(function* (url, browser) {
|
||||
return yield ReaderMode.downloadAndParseDocument(url).catch(e => {
|
||||
async _getArticle(url, browser) {
|
||||
return await ReaderMode.downloadAndParseDocument(url).catch(e => {
|
||||
if (e && e.newURL) {
|
||||
// Pass up the error so we can navigate the browser in question to the new URL:
|
||||
throw e;
|
||||
|
|
@ -182,5 +151,5 @@ var ReaderParent = {
|
|||
Cu.reportError("Error downloading and parsing document: " + e);
|
||||
return null;
|
||||
});
|
||||
})
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ chrome.jar:
|
|||
skin/aboutMemory.css (aboutMemory.css)
|
||||
skin/aboutPrivateBrowsing.css (aboutPrivateBrowsing.css)
|
||||
skin/aboutReader.css (aboutReader.css)
|
||||
skin/aboutReaderContent.css (aboutReaderContent.css)
|
||||
skin/aboutReaderControls.css (aboutReaderControls.css)
|
||||
skin/aboutSupport.css (aboutSupport.css)
|
||||
skin/content.css (content.css)
|
||||
skin/scrollbar.css (scrollbar-apz.css)
|
||||
|
|
@ -31,8 +29,6 @@ chrome.jar:
|
|||
% override chrome://global/skin/about.css chrome://browser/skin/about.css
|
||||
% override chrome://global/skin/aboutMemory.css chrome://browser/skin/aboutMemory.css
|
||||
% override chrome://global/skin/aboutReader.css chrome://browser/skin/aboutReader.css
|
||||
% override chrome://global/skin/aboutReaderContent.css chrome://browser/skin/aboutReaderContent.css
|
||||
% override chrome://global/skin/aboutReaderControls.css chrome://browser/skin/aboutReaderControls.css
|
||||
% override chrome://global/skin/aboutSupport.css chrome://browser/skin/aboutSupport.css
|
||||
% override chrome://global/skin/media/videocontrols.css chrome://browser/skin/touchcontrols.css
|
||||
% override chrome://global/skin/netError.css chrome://browser/skin/netError.css
|
||||
|
|
|
|||
|
|
@ -16,9 +16,10 @@ this.EXPORTED_SYMBOLS = ["NarrateControls"];
|
|||
|
||||
var gStrings = Services.strings.createBundle("chrome://global/locale/narrate.properties");
|
||||
|
||||
function NarrateControls(mm, win) {
|
||||
function NarrateControls(mm, win, languagePromise) {
|
||||
this._mm = mm;
|
||||
this._winRef = Cu.getWeakReference(win);
|
||||
this._languagePromise = languagePromise;
|
||||
|
||||
win.addEventListener("unload", this);
|
||||
|
||||
|
|
@ -37,16 +38,12 @@ function NarrateControls(mm, win) {
|
|||
}
|
||||
|
||||
let dropdown = win.document.createElement("ul");
|
||||
dropdown.className = "dropdown";
|
||||
dropdown.id = "narrate-dropdown";
|
||||
dropdown.className = "dropdown narrate-dropdown";
|
||||
// We need inline svg here for the animation to work (bug 908634 & 1190881).
|
||||
// The style animation can't be scoped (bug 830056).
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
dropdown.innerHTML =
|
||||
localize`<style scoped>
|
||||
@import url("chrome://global/skin/narrateControls.css");
|
||||
</style>
|
||||
<li>
|
||||
<button class="dropdown-toggle button" id="narrate-toggle"
|
||||
localize`<li>
|
||||
<button class="dropdown-toggle button narrate-toggle"
|
||||
title="${"narrate"}" hidden>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
|
|
@ -60,11 +57,11 @@ function NarrateControls(mm, win) {
|
|||
100% { transform: scaleY(1); }
|
||||
}
|
||||
|
||||
#waveform > rect {
|
||||
.waveform > rect {
|
||||
fill: #808080;
|
||||
}
|
||||
|
||||
.speaking #waveform > rect {
|
||||
.speaking .waveform > rect {
|
||||
fill: #58bf43;
|
||||
transform-box: fill-box;
|
||||
transform-origin: 50% 50%;
|
||||
|
|
@ -74,15 +71,15 @@ function NarrateControls(mm, win) {
|
|||
animation-timing-function: linear;
|
||||
}
|
||||
|
||||
#waveform > rect:nth-child(2) { animation-delay: 250ms; }
|
||||
#waveform > rect:nth-child(3) { animation-delay: 500ms; }
|
||||
#waveform > rect:nth-child(4) { animation-delay: 750ms; }
|
||||
#waveform > rect:nth-child(5) { animation-delay: 1000ms; }
|
||||
#waveform > rect:nth-child(6) { animation-delay: 1250ms; }
|
||||
#waveform > rect:nth-child(7) { animation-delay: 1500ms; }
|
||||
.waveform > rect:nth-child(2) { animation-delay: 250ms; }
|
||||
.waveform > rect:nth-child(3) { animation-delay: 500ms; }
|
||||
.waveform > rect:nth-child(4) { animation-delay: 750ms; }
|
||||
.waveform > rect:nth-child(5) { animation-delay: 1000ms; }
|
||||
.waveform > rect:nth-child(6) { animation-delay: 1250ms; }
|
||||
.waveform > rect:nth-child(7) { animation-delay: 1500ms; }
|
||||
|
||||
</style>
|
||||
<g id="waveform">
|
||||
<g class="waveform">
|
||||
<rect x="1" y="8" width="2" height="8" rx=".5" ry=".5" />
|
||||
<rect x="4" y="5" width="2" height="14" rx=".5" ry=".5" />
|
||||
<rect x="7" y="8" width="2" height="8" rx=".5" ry=".5" />
|
||||
|
|
@ -95,35 +92,35 @@ function NarrateControls(mm, win) {
|
|||
</button>
|
||||
</li>
|
||||
<li class="dropdown-popup">
|
||||
<div id="narrate-control" class="narrate-row">
|
||||
<button disabled id="narrate-skip-previous"
|
||||
<div class="narrate-row narrate-control">
|
||||
<button disabled class="narrate-skip-previous"
|
||||
title="${"back"}"></button>
|
||||
<button id="narrate-start-stop" title="${"start"}"></button>
|
||||
<button disabled id="narrate-skip-next"
|
||||
<button class="narrate-start-stop" title="${"start"}"></button>
|
||||
<button disabled class="narrate-skip-next"
|
||||
title="${"forward"}"></button>
|
||||
</div>
|
||||
<div id="narrate-rate" class="narrate-row">
|
||||
<input id="narrate-rate-input" value="0" title="${"speed"}"
|
||||
<div class="narrate-row narrate-rate">
|
||||
<input class="narrate-rate-input" value="0" title="${"speed"}"
|
||||
step="5" max="100" min="-100" type="range">
|
||||
</div>
|
||||
<div id="narrate-voices" class="narrate-row"></div>
|
||||
<div class="narrate-row narrate-voices"></div>
|
||||
<div class="dropdown-arrow"></div>
|
||||
</li>`;
|
||||
|
||||
this.narrator = new Narrator(win);
|
||||
this.narrator = new Narrator(win, languagePromise);
|
||||
|
||||
let branch = Services.prefs.getBranch("narrate.");
|
||||
let selectLabel = gStrings.GetStringFromName("selectvoicelabel");
|
||||
this.voiceSelect = new VoiceSelect(win, selectLabel);
|
||||
this.voiceSelect.element.addEventListener("change", this);
|
||||
this.voiceSelect.element.id = "voice-select";
|
||||
this.voiceSelect.element.classList.add("voice-select");
|
||||
win.speechSynthesis.addEventListener("voiceschanged", this);
|
||||
dropdown.querySelector("#narrate-voices").appendChild(
|
||||
dropdown.querySelector(".narrate-voices").appendChild(
|
||||
this.voiceSelect.element);
|
||||
|
||||
dropdown.addEventListener("click", this, true);
|
||||
|
||||
let rateRange = dropdown.querySelector("#narrate-rate > input");
|
||||
let rateRange = dropdown.querySelector(".narrate-rate > input");
|
||||
rateRange.addEventListener("change", this);
|
||||
|
||||
// The rate is stored as an integer.
|
||||
|
|
@ -131,15 +128,15 @@ function NarrateControls(mm, win) {
|
|||
|
||||
this._setupVoices();
|
||||
|
||||
let tb = win.document.getElementById("reader-toolbar");
|
||||
let tb = win.document.querySelector(".reader-toolbar");
|
||||
tb.appendChild(dropdown);
|
||||
}
|
||||
|
||||
NarrateControls.prototype = {
|
||||
handleEvent: function(evt) {
|
||||
handleEvent(evt) {
|
||||
switch (evt.type) {
|
||||
case "change":
|
||||
if (evt.target.id == "narrate-rate-input") {
|
||||
if (evt.target.classList.contains("narrate-rate-input")) {
|
||||
this._onRateInput(evt);
|
||||
} else {
|
||||
this._onVoiceChange();
|
||||
|
|
@ -162,8 +159,8 @@ NarrateControls.prototype = {
|
|||
/**
|
||||
* Returns true if synth voices are available.
|
||||
*/
|
||||
_setupVoices: function() {
|
||||
return this.narrator.languagePromise.then(language => {
|
||||
_setupVoices() {
|
||||
return this._languagePromise.then(language => {
|
||||
this.voiceSelect.clear();
|
||||
let win = this._win;
|
||||
let voicePrefs = this._getVoicePref();
|
||||
|
|
@ -190,7 +187,7 @@ NarrateControls.prototype = {
|
|||
this.voiceSelect.addOptions(options);
|
||||
}
|
||||
|
||||
let narrateToggle = win.document.getElementById("narrate-toggle");
|
||||
let narrateToggle = win.document.querySelector(".narrate-toggle");
|
||||
let histogram = Services.telemetry.getKeyedHistogramById(
|
||||
"NARRATE_CONTENT_BY_LANGUAGE_2");
|
||||
let initial = !this._voicesInitialized;
|
||||
|
|
@ -210,7 +207,7 @@ NarrateControls.prototype = {
|
|||
});
|
||||
},
|
||||
|
||||
_getVoicePref: function() {
|
||||
_getVoicePref() {
|
||||
let voicePref = Services.prefs.getCharPref("narrate.voice");
|
||||
try {
|
||||
return JSON.parse(voicePref);
|
||||
|
|
@ -219,15 +216,15 @@ NarrateControls.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
_onRateInput: function(evt) {
|
||||
_onRateInput(evt) {
|
||||
AsyncPrefs.set("narrate.rate", parseInt(evt.target.value, 10));
|
||||
this.narrator.setRate(this._convertRate(evt.target.value));
|
||||
},
|
||||
|
||||
_onVoiceChange: function() {
|
||||
_onVoiceChange() {
|
||||
let voice = this.voice;
|
||||
this.narrator.setVoice(voice);
|
||||
this.narrator.languagePromise.then(language => {
|
||||
this._languagePromise.then(language => {
|
||||
if (language) {
|
||||
let voicePref = this._getVoicePref();
|
||||
voicePref[language || "default"] = voice;
|
||||
|
|
@ -236,42 +233,39 @@ NarrateControls.prototype = {
|
|||
});
|
||||
},
|
||||
|
||||
_onButtonClick: function(evt) {
|
||||
switch (evt.target.id) {
|
||||
case "narrate-skip-previous":
|
||||
this.narrator.skipPrevious();
|
||||
break;
|
||||
case "narrate-skip-next":
|
||||
this.narrator.skipNext();
|
||||
break;
|
||||
case "narrate-start-stop":
|
||||
if (this.narrator.speaking) {
|
||||
this.narrator.stop();
|
||||
} else {
|
||||
this._updateSpeechControls(true);
|
||||
let options = { rate: this.rate, voice: this.voice };
|
||||
this.narrator.start(options).then(() => {
|
||||
this._updateSpeechControls(false);
|
||||
}, err => {
|
||||
Cu.reportError(`Narrate failed: ${err}.`);
|
||||
this._updateSpeechControls(false);
|
||||
});
|
||||
}
|
||||
break;
|
||||
_onButtonClick(evt) {
|
||||
let classList = evt.target.classList;
|
||||
if (classList.contains("narrate-skip-previous")) {
|
||||
this.narrator.skipPrevious();
|
||||
} else if (classList.contains("narrate-skip-next")) {
|
||||
this.narrator.skipNext();
|
||||
} else if (classList.contains("narrate-start-stop")) {
|
||||
if (this.narrator.speaking) {
|
||||
this.narrator.stop();
|
||||
} else {
|
||||
this._updateSpeechControls(true);
|
||||
let options = { rate: this.rate, voice: this.voice };
|
||||
this.narrator.start(options).then(() => {
|
||||
this._updateSpeechControls(false);
|
||||
}, err => {
|
||||
Cu.reportError(`Narrate failed: ${err}.`);
|
||||
this._updateSpeechControls(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_updateSpeechControls: function(speaking) {
|
||||
let dropdown = this._doc.getElementById("narrate-dropdown");
|
||||
_updateSpeechControls(speaking) {
|
||||
let dropdown = this._doc.querySelector(".narrate-dropdown");
|
||||
dropdown.classList.toggle("keep-open", speaking);
|
||||
dropdown.classList.toggle("speaking", speaking);
|
||||
|
||||
let startStopButton = this._doc.getElementById("narrate-start-stop");
|
||||
let startStopButton = this._doc.querySelector(".narrate-start-stop");
|
||||
startStopButton.title =
|
||||
gStrings.GetStringFromName(speaking ? "stop" : "start");
|
||||
|
||||
this._doc.getElementById("narrate-skip-previous").disabled = !speaking;
|
||||
this._doc.getElementById("narrate-skip-next").disabled = !speaking;
|
||||
this._doc.querySelector(".narrate-skip-previous").disabled = !speaking;
|
||||
this._doc.querySelector(".narrate-skip-next").disabled = !speaking;
|
||||
|
||||
if (speaking) {
|
||||
TelemetryStopwatch.start("NARRATE_CONTENT_SPEAKTIME_MS", this);
|
||||
|
|
@ -280,7 +274,7 @@ NarrateControls.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
_createVoiceLabel: function(voice) {
|
||||
_createVoiceLabel(voice) {
|
||||
// This is a highly imperfect method of making human-readable labels
|
||||
// for system voices. Because each platform has a different naming scheme
|
||||
// for voices, we use a different method for each platform.
|
||||
|
|
@ -303,7 +297,7 @@ NarrateControls.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
_getLanguageName: function(lang) {
|
||||
_getLanguageName(lang) {
|
||||
if (!this._langStrings) {
|
||||
this._langStrings = Services.strings.createBundle(
|
||||
"chrome://global/locale/languageNames.properties ");
|
||||
|
|
@ -317,7 +311,7 @@ NarrateControls.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
_convertRate: function(rate) {
|
||||
_convertRate(rate) {
|
||||
// We need to convert a relative percentage value to a fraction rate value.
|
||||
// eg. -100 is half the speed, 100 is twice the speed in percentage,
|
||||
// 0.5 is half the speed and 2 is twice the speed in fractions.
|
||||
|
|
@ -334,7 +328,7 @@ NarrateControls.prototype = {
|
|||
|
||||
get rate() {
|
||||
return this._convertRate(
|
||||
this._doc.getElementById("narrate-rate-input").value);
|
||||
this._doc.querySelector(".narrate-rate-input").value);
|
||||
},
|
||||
|
||||
get voice() {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ const { interfaces: Ci, utils: Cu } = Components;
|
|||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
|
||||
"resource:///modules/translation/LanguageDetector.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Services",
|
||||
"resource://gre/modules/Services.jsm");
|
||||
|
||||
|
|
@ -24,29 +22,13 @@ const kTextStylesRules = ["font-family", "font-kerning", "font-size",
|
|||
"line-height", "letter-spacing", "text-orientation",
|
||||
"text-transform", "word-spacing"];
|
||||
|
||||
function Narrator(win) {
|
||||
function Narrator(win, languagePromise) {
|
||||
this._winRef = Cu.getWeakReference(win);
|
||||
this._languagePromise = languagePromise;
|
||||
this._inTest = Services.prefs.getBoolPref("narrate.test");
|
||||
this._speechOptions = {};
|
||||
this._startTime = 0;
|
||||
this._stopped = false;
|
||||
|
||||
this.languagePromise = new Promise(resolve => {
|
||||
let detect = () => {
|
||||
win.document.removeEventListener("AboutReaderContentReady", detect);
|
||||
let sampleText = this._doc.getElementById(
|
||||
"moz-reader-content").textContent.substring(0, 60 * 1024);
|
||||
LanguageDetector.detectLanguage(sampleText).then(result => {
|
||||
resolve(result.confident ? result.language : null);
|
||||
});
|
||||
};
|
||||
|
||||
if (win.document.body.classList.contains("loaded")) {
|
||||
detect();
|
||||
} else {
|
||||
win.document.addEventListener("AboutReaderContentReady", detect);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Narrator.prototype = {
|
||||
|
|
@ -71,7 +53,7 @@ Narrator.prototype = {
|
|||
// For example, paragraphs. But nested anchors and other elements
|
||||
// are not interesting since their text already appears in their
|
||||
// parent's textContent.
|
||||
acceptNode: function(node) {
|
||||
acceptNode(node) {
|
||||
if (this._matches.has(node.parentNode)) {
|
||||
// Reject sub-trees of accepted nodes.
|
||||
return nf.FILTER_REJECT;
|
||||
|
|
@ -107,7 +89,7 @@ Narrator.prototype = {
|
|||
// are no other strong references, and it will be GC'ed. Instead,
|
||||
// we rely on the window's lifetime and use it as a weak reference.
|
||||
this._treeWalkerRef.set(this._win,
|
||||
this._doc.createTreeWalker(this._doc.getElementById("container"),
|
||||
this._doc.createTreeWalker(this._doc.querySelector(".container"),
|
||||
nf.SHOW_ELEMENT, filter, false));
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +106,7 @@ Narrator.prototype = {
|
|||
this._win.speechSynthesis.pending;
|
||||
},
|
||||
|
||||
_getVoice: function(voiceURI) {
|
||||
_getVoice(voiceURI) {
|
||||
if (!this._voiceMap || !this._voiceMap.has(voiceURI)) {
|
||||
this._voiceMap = new Map(
|
||||
this._win.speechSynthesis.getVoices().map(v => [v.voiceURI, v]));
|
||||
|
|
@ -133,7 +115,7 @@ Narrator.prototype = {
|
|||
return this._voiceMap.get(voiceURI);
|
||||
},
|
||||
|
||||
_isParagraphInView: function(paragraph) {
|
||||
_isParagraphInView(paragraph) {
|
||||
if (!paragraph) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -142,13 +124,13 @@ Narrator.prototype = {
|
|||
return bb.top >= 0 && bb.top < this._win.innerHeight;
|
||||
},
|
||||
|
||||
_sendTestEvent: function(eventType, detail) {
|
||||
_sendTestEvent(eventType, detail) {
|
||||
let win = this._win;
|
||||
win.dispatchEvent(new win.CustomEvent(eventType,
|
||||
{ detail: Cu.cloneInto(detail, win.document) }));
|
||||
},
|
||||
|
||||
_speakInner: function() {
|
||||
_speakInner() {
|
||||
this._win.speechSynthesis.cancel();
|
||||
let tw = this._treeWalker;
|
||||
let paragraph = tw.currentNode;
|
||||
|
|
@ -238,18 +220,12 @@ Narrator.prototype = {
|
|||
return;
|
||||
}
|
||||
|
||||
// Match non-whitespace. This isn't perfect, but the most universal
|
||||
// solution for now.
|
||||
let reWordBoundary = /\S+/g;
|
||||
// Match the first word from the boundary event offset.
|
||||
reWordBoundary.lastIndex = e.charIndex;
|
||||
let firstIndex = reWordBoundary.exec(paragraph.textContent);
|
||||
if (firstIndex) {
|
||||
highlighter.highlight(firstIndex.index, reWordBoundary.lastIndex);
|
||||
if (e.charLength) {
|
||||
highlighter.highlight(e.charIndex, e.charLength);
|
||||
if (this._inTest) {
|
||||
this._sendTestEvent("wordhighlight", {
|
||||
start: firstIndex.index,
|
||||
end: reWordBoundary.lastIndex
|
||||
start: e.charIndex,
|
||||
end: e.charIndex + e.charLength
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -259,14 +235,14 @@ Narrator.prototype = {
|
|||
});
|
||||
},
|
||||
|
||||
start: function(speechOptions) {
|
||||
start(speechOptions) {
|
||||
this._speechOptions = {
|
||||
rate: speechOptions.rate,
|
||||
voice: this._getVoice(speechOptions.voice)
|
||||
};
|
||||
|
||||
this._stopped = false;
|
||||
return this.languagePromise.then(language => {
|
||||
return this._languagePromise.then(language => {
|
||||
if (!this._speechOptions.voice) {
|
||||
this._speechOptions.lang = language;
|
||||
}
|
||||
|
|
@ -288,32 +264,32 @@ Narrator.prototype = {
|
|||
});
|
||||
},
|
||||
|
||||
stop: function() {
|
||||
stop() {
|
||||
this._stopped = true;
|
||||
this._win.speechSynthesis.cancel();
|
||||
},
|
||||
|
||||
skipNext: function() {
|
||||
skipNext() {
|
||||
this._win.speechSynthesis.cancel();
|
||||
},
|
||||
|
||||
skipPrevious: function() {
|
||||
skipPrevious() {
|
||||
this._goBackParagraphs(this._timeIntoParagraph < PREV_THRESHOLD ? 2 : 1);
|
||||
},
|
||||
|
||||
setRate: function(rate) {
|
||||
setRate(rate) {
|
||||
this._speechOptions.rate = rate;
|
||||
/* repeat current paragraph */
|
||||
this._goBackParagraphs(1);
|
||||
},
|
||||
|
||||
setVoice: function(voice) {
|
||||
setVoice(voice) {
|
||||
this._speechOptions.voice = this._getVoice(voice);
|
||||
/* repeat current paragraph */
|
||||
this._goBackParagraphs(1);
|
||||
},
|
||||
|
||||
_goBackParagraphs: function(count) {
|
||||
_goBackParagraphs(count) {
|
||||
let tw = this._treeWalker;
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (!tw.previousNode()) {
|
||||
|
|
@ -338,13 +314,13 @@ Highlighter.prototype = {
|
|||
* Highlight the range within offsets relative to the container.
|
||||
*
|
||||
* @param {Number} startOffset the start offset
|
||||
* @param {Number} endOffset the end offset
|
||||
* @param {Number} length the length in characters of the range
|
||||
*/
|
||||
highlight: function(startOffset, endOffset) {
|
||||
highlight(startOffset, length) {
|
||||
let containerRect = this.container.getBoundingClientRect();
|
||||
let range = this._getRange(startOffset, endOffset);
|
||||
let range = this._getRange(startOffset, startOffset + length);
|
||||
let rangeRects = range.getClientRects();
|
||||
let win = this.container.ownerDocument.defaultView;
|
||||
let win = this.container.ownerGlobal;
|
||||
let computedStyle = win.getComputedStyle(range.endContainer.parentNode);
|
||||
let nodes = this._getFreshHighlightNodes(rangeRects.length);
|
||||
|
||||
|
|
@ -386,7 +362,7 @@ Highlighter.prototype = {
|
|||
/**
|
||||
* Releases reference to container and removes all highlight nodes.
|
||||
*/
|
||||
remove: function() {
|
||||
remove() {
|
||||
for (let node of this._nodes) {
|
||||
node.remove();
|
||||
}
|
||||
|
|
@ -400,7 +376,7 @@ Highlighter.prototype = {
|
|||
*
|
||||
* @param {Number} count number of nodes needed
|
||||
*/
|
||||
_getFreshHighlightNodes: function(count) {
|
||||
_getFreshHighlightNodes(count) {
|
||||
let doc = this.container.ownerDocument;
|
||||
let nodes = Array.from(this._nodes);
|
||||
|
||||
|
|
@ -427,7 +403,7 @@ Highlighter.prototype = {
|
|||
* @param {Number} startOffset the start offset
|
||||
* @param {Number} endOffset the end offset
|
||||
*/
|
||||
_getRange: function(startOffset, endOffset) {
|
||||
_getRange(startOffset, endOffset) {
|
||||
let doc = this.container.ownerDocument;
|
||||
let i = 0;
|
||||
let treeWalker = doc.createTreeWalker(
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ function VoiceSelect(win, label) {
|
|||
|
||||
let element = win.document.createElement("div");
|
||||
element.classList.add("voiceselect");
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
element.innerHTML =
|
||||
`<button class="select-toggle" aria-controls="voice-options">
|
||||
<span class="label">${label}</span> <span class="current-voice"></span>
|
||||
|
|
@ -37,7 +38,7 @@ function VoiceSelect(win, label) {
|
|||
}
|
||||
|
||||
VoiceSelect.prototype = {
|
||||
add: function(label, value) {
|
||||
add(label, value) {
|
||||
let option = this._doc.createElement("button");
|
||||
option.dataset.value = value;
|
||||
option.classList.add("option");
|
||||
|
|
@ -48,7 +49,7 @@ VoiceSelect.prototype = {
|
|||
return option;
|
||||
},
|
||||
|
||||
addOptions: function(options) {
|
||||
addOptions(options) {
|
||||
let selected = null;
|
||||
for (let option of options) {
|
||||
if (option.selected) {
|
||||
|
|
@ -61,11 +62,11 @@ VoiceSelect.prototype = {
|
|||
this._select(selected || this.options[0], true);
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
clear() {
|
||||
this.listbox.innerHTML = "";
|
||||
},
|
||||
|
||||
toggleList: function(force, focus = true) {
|
||||
toggleList(force, focus = true) {
|
||||
if (this.element.classList.toggle("open", force)) {
|
||||
if (focus) {
|
||||
(this.selected || this.options[0]).focus();
|
||||
|
|
@ -84,7 +85,7 @@ VoiceSelect.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
handleEvent: function(evt) {
|
||||
handleEvent(evt) {
|
||||
let target = evt.target;
|
||||
|
||||
switch (evt.type) {
|
||||
|
|
@ -131,7 +132,7 @@ VoiceSelect.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
_getPagedOption: function(option, up) {
|
||||
_getPagedOption(option, up) {
|
||||
let height = elem => elem.getBoundingClientRect().height;
|
||||
let listboxHeight = height(this.listbox);
|
||||
|
||||
|
|
@ -148,7 +149,7 @@ VoiceSelect.prototype = {
|
|||
return next;
|
||||
},
|
||||
|
||||
_keyPressedButton: function(evt) {
|
||||
_keyPressedButton(evt) {
|
||||
if (evt.altKey && (evt.key === "ArrowUp" || evt.key === "ArrowUp")) {
|
||||
this.toggleList(true);
|
||||
return;
|
||||
|
|
@ -178,7 +179,7 @@ VoiceSelect.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
_keyPressedInBox: function(evt) {
|
||||
_keyPressedInBox(evt) {
|
||||
let toFocus;
|
||||
let cur = this._doc.activeElement;
|
||||
|
||||
|
|
@ -212,7 +213,7 @@ VoiceSelect.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
_select: function(option, suppressEvent = false) {
|
||||
_select(option, suppressEvent = false) {
|
||||
let oldSelected = this.selected;
|
||||
if (oldSelected) {
|
||||
oldSelected.removeAttribute("aria-selected");
|
||||
|
|
@ -233,7 +234,7 @@ VoiceSelect.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
_updateDropdownHeight: function(now) {
|
||||
_updateDropdownHeight(now) {
|
||||
let updateInner = () => {
|
||||
let winHeight = this._win.innerHeight;
|
||||
let listbox = this.listbox;
|
||||
|
|
@ -252,7 +253,7 @@ VoiceSelect.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
_getOptionFromValue: function(value) {
|
||||
_getOptionFromValue(value) {
|
||||
return Array.from(this.options).find(o => o.dataset.value === value);
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* This file defines specific rules for print preview when using simplify mode.
|
||||
* These rules already exist on aboutReaderControls.css, however, we decoupled it
|
||||
* These rules already exist on aboutReader.css, however, we decoupled it
|
||||
* from the original file so we don't need to load a bunch of extra queries that
|
||||
* will not take effect when using the simplify page checkbox. This file defines
|
||||
* styling for title and author on the header element. */
|
||||
|
|
|
|||
|
|
@ -15,8 +15,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|||
XPCOMUtils.defineLazyModuleGetter(this, "AsyncPrefs", "resource://gre/modules/AsyncPrefs.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "NarrateControls", "resource://gre/modules/narrate/NarrateControls.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Rect", "resource://gre/modules/Geometry.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", "resource://gre/modules/UITelemetry.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
|
||||
|
||||
var gStrings = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
|
||||
|
||||
|
|
@ -45,45 +44,49 @@ var AboutReader = function(mm, win, articlePromise) {
|
|||
.getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
|
||||
|
||||
this._article = null;
|
||||
this._languagePromise = new Promise(resolve => {
|
||||
this._foundLanguage = resolve;
|
||||
});
|
||||
|
||||
if (articlePromise) {
|
||||
this._articlePromise = articlePromise;
|
||||
}
|
||||
|
||||
this._headerElementRef = Cu.getWeakReference(doc.getElementById("reader-header"));
|
||||
this._domainElementRef = Cu.getWeakReference(doc.getElementById("reader-domain"));
|
||||
this._titleElementRef = Cu.getWeakReference(doc.getElementById("reader-title"));
|
||||
this._creditsElementRef = Cu.getWeakReference(doc.getElementById("reader-credits"));
|
||||
this._contentElementRef = Cu.getWeakReference(doc.getElementById("moz-reader-content"));
|
||||
this._toolbarElementRef = Cu.getWeakReference(doc.getElementById("reader-toolbar"));
|
||||
this._messageElementRef = Cu.getWeakReference(doc.getElementById("reader-message"));
|
||||
this._headerElementRef = Cu.getWeakReference(doc.querySelector(".reader-header"));
|
||||
this._domainElementRef = Cu.getWeakReference(doc.querySelector(".reader-domain"));
|
||||
this._titleElementRef = Cu.getWeakReference(doc.querySelector(".reader-title"));
|
||||
this._readTimeElementRef = Cu.getWeakReference(doc.querySelector(".reader-estimated-time"));
|
||||
this._creditsElementRef = Cu.getWeakReference(doc.querySelector(".reader-credits"));
|
||||
this._contentElementRef = Cu.getWeakReference(doc.querySelector(".moz-reader-content"));
|
||||
this._toolbarElementRef = Cu.getWeakReference(doc.querySelector(".reader-toolbar"));
|
||||
this._messageElementRef = Cu.getWeakReference(doc.querySelector(".reader-message"));
|
||||
this._containerElementRef = Cu.getWeakReference(doc.querySelector(".container"));
|
||||
|
||||
this._scrollOffset = win.pageYOffset;
|
||||
|
||||
doc.addEventListener("click", this, false);
|
||||
doc.addEventListener("click", this);
|
||||
|
||||
win.addEventListener("pagehide", this, false);
|
||||
win.addEventListener("scroll", this, false);
|
||||
win.addEventListener("resize", this, false);
|
||||
win.addEventListener("pagehide", this);
|
||||
win.addEventListener("scroll", this);
|
||||
win.addEventListener("resize", this);
|
||||
|
||||
Services.obs.addObserver(this, "inner-window-destroyed", false);
|
||||
|
||||
doc.addEventListener("visibilitychange", this, false);
|
||||
doc.addEventListener("visibilitychange", this);
|
||||
|
||||
this._setupStyleDropdown();
|
||||
this._setupButton("close-button", this._onReaderClose.bind(this), "aboutReader.toolbar.close");
|
||||
|
||||
const gIsFirefoxDesktop = Services.appinfo.ID == "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
|
||||
if (gIsFirefoxDesktop) {
|
||||
// we're ready for any external setup, send a signal for that.
|
||||
this._mm.sendAsyncMessage("Reader:OnSetup");
|
||||
}
|
||||
// we're ready for any external setup, send a signal for that.
|
||||
this._mm.sendAsyncMessage("Reader:OnSetup");
|
||||
|
||||
let colorSchemeValues = JSON.parse(Services.prefs.getCharPref("reader.color_scheme.values"));
|
||||
let colorSchemeOptions = colorSchemeValues.map((value) => {
|
||||
return { name: gStrings.GetStringFromName("aboutReader.colorScheme." + value),
|
||||
value: value,
|
||||
itemClass: value + "-button" };
|
||||
return {
|
||||
name: gStrings.GetStringFromName("aboutReader.colorScheme." + value),
|
||||
value,
|
||||
itemClass: value + "-button"
|
||||
};
|
||||
});
|
||||
|
||||
let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
|
||||
|
|
@ -114,7 +117,7 @@ var AboutReader = function(mm, win, articlePromise) {
|
|||
this._setupLineHeightButtons();
|
||||
|
||||
if (win.speechSynthesis && Services.prefs.getBoolPref("narrate.enabled")) {
|
||||
new NarrateControls(mm, win);
|
||||
new NarrateControls(mm, win, this._languagePromise);
|
||||
}
|
||||
|
||||
this._loadArticle();
|
||||
|
|
@ -146,6 +149,10 @@ AboutReader.prototype = {
|
|||
return this._titleElementRef.get();
|
||||
},
|
||||
|
||||
get _readTimeElement() {
|
||||
return this._readTimeElementRef.get();
|
||||
},
|
||||
|
||||
get _creditsElement() {
|
||||
return this._creditsElementRef.get();
|
||||
},
|
||||
|
|
@ -162,6 +169,10 @@ AboutReader.prototype = {
|
|||
return this._messageElementRef.get();
|
||||
},
|
||||
|
||||
get _containerElement() {
|
||||
return this._containerElementRef.get();
|
||||
},
|
||||
|
||||
get _isToolbarVertical() {
|
||||
if (this._toolbarVertical !== undefined) {
|
||||
return this._toolbarVertical;
|
||||
|
|
@ -178,7 +189,7 @@ AboutReader.prototype = {
|
|||
return _viewId;
|
||||
},
|
||||
|
||||
receiveMessage: function (message) {
|
||||
receiveMessage (message) {
|
||||
switch (message.name) {
|
||||
// Triggered by Android user pressing BACK while the banner font-dropdown is open.
|
||||
case "Reader:CloseDropdown": {
|
||||
|
|
@ -191,14 +202,14 @@ AboutReader.prototype = {
|
|||
if (message.data.id && message.data.image &&
|
||||
!this._doc.getElementById(message.data.id)) {
|
||||
let btn = this._doc.createElement("button");
|
||||
btn.setAttribute("class", "button");
|
||||
btn.setAttribute("class", "button " + message.data.id);
|
||||
btn.setAttribute("style", "background-image: url('" + message.data.image + "')");
|
||||
btn.setAttribute("id", message.data.id);
|
||||
if (message.data.title)
|
||||
btn.setAttribute("title", message.data.title);
|
||||
if (message.data.text)
|
||||
btn.textContent = message.data.text;
|
||||
let tb = this._doc.getElementById("reader-toolbar");
|
||||
let tb = this._toolbarElement;
|
||||
tb.appendChild(btn);
|
||||
this._setupButton(message.data.id, button => {
|
||||
this._mm.sendAsyncMessage("Reader:Clicked-" + button.getAttribute("id"), { article: this._article });
|
||||
|
|
@ -220,18 +231,21 @@ AboutReader.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
handleEvent: function(aEvent) {
|
||||
handleEvent(aEvent) {
|
||||
if (!aEvent.isTrusted)
|
||||
return;
|
||||
|
||||
switch (aEvent.type) {
|
||||
case "click":
|
||||
let target = aEvent.target;
|
||||
if (target.classList.contains('dropdown-toggle')) {
|
||||
if (target.classList.contains("dropdown-toggle")) {
|
||||
this._toggleDropdownClicked(aEvent);
|
||||
} else if (!target.closest('.dropdown-popup')) {
|
||||
} else if (!target.closest(".dropdown-popup")) {
|
||||
this._closeDropdowns();
|
||||
}
|
||||
if (target.tagName == "A" && !target.classList.contains("reader-domain")) {
|
||||
this._linkClicked(aEvent);
|
||||
}
|
||||
break;
|
||||
case "scroll":
|
||||
this._closeDropdowns(true);
|
||||
|
|
@ -243,7 +257,7 @@ AboutReader.prototype = {
|
|||
this._updateImageMargins();
|
||||
if (this._isToolbarVertical) {
|
||||
this._win.setTimeout(() => {
|
||||
for (let dropdown of this._doc.querySelectorAll('.dropdown.open')) {
|
||||
for (let dropdown of this._doc.querySelectorAll(".dropdown.open")) {
|
||||
this._updatePopupPosition(dropdown);
|
||||
}
|
||||
}, 0);
|
||||
|
|
@ -271,12 +285,12 @@ AboutReader.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
observe: function(subject, topic, data) {
|
||||
observe(subject, topic, data) {
|
||||
if (subject.QueryInterface(Ci.nsISupportsPRUint64).data != this._innerWindowId) {
|
||||
return;
|
||||
}
|
||||
|
||||
Services.obs.removeObserver(this, "inner-window-destroyed", false);
|
||||
Services.obs.removeObserver(this, "inner-window-destroyed");
|
||||
|
||||
this._mm.removeMessageListener("Reader:CloseDropdown", this);
|
||||
this._mm.removeMessageListener("Reader:AddButton", this);
|
||||
|
|
@ -284,12 +298,12 @@ AboutReader.prototype = {
|
|||
this._windowUnloaded = true;
|
||||
},
|
||||
|
||||
_onReaderClose: function() {
|
||||
_onReaderClose() {
|
||||
ReaderMode.leaveReaderMode(this._mm.docShell, this._win);
|
||||
},
|
||||
|
||||
_setFontSize: function(newFontSize) {
|
||||
let containerClasses = this._doc.getElementById("container").classList;
|
||||
_setFontSize(newFontSize) {
|
||||
let containerClasses = this._containerElement.classList;
|
||||
|
||||
if (this._fontSize > 0)
|
||||
containerClasses.remove("font-size" + this._fontSize);
|
||||
|
|
@ -299,19 +313,19 @@ AboutReader.prototype = {
|
|||
return AsyncPrefs.set("reader.font_size", this._fontSize);
|
||||
},
|
||||
|
||||
_setupFontSizeButtons: function() {
|
||||
_setupFontSizeButtons() {
|
||||
const FONT_SIZE_MIN = 1;
|
||||
const FONT_SIZE_MAX = 9;
|
||||
|
||||
// Sample text shown in Android UI.
|
||||
let sampleText = this._doc.getElementById("font-size-sample");
|
||||
let sampleText = this._doc.querySelector(".font-size-sample");
|
||||
sampleText.textContent = gStrings.GetStringFromName("aboutReader.fontTypeSample");
|
||||
|
||||
let currentSize = Services.prefs.getIntPref("reader.font_size");
|
||||
currentSize = Math.max(FONT_SIZE_MIN, Math.min(FONT_SIZE_MAX, currentSize));
|
||||
|
||||
let plusButton = this._doc.getElementById("font-size-plus");
|
||||
let minusButton = this._doc.getElementById("font-size-minus");
|
||||
let plusButton = this._doc.querySelector(".plus-button");
|
||||
let minusButton = this._doc.querySelector(".minus-button");
|
||||
|
||||
function updateControls() {
|
||||
if (currentSize === FONT_SIZE_MIN) {
|
||||
|
|
@ -360,8 +374,8 @@ AboutReader.prototype = {
|
|||
}, true);
|
||||
},
|
||||
|
||||
_setContentWidth: function(newContentWidth) {
|
||||
let containerClasses = this._doc.getElementById("container").classList;
|
||||
_setContentWidth(newContentWidth) {
|
||||
let containerClasses = this._containerElement.classList;
|
||||
|
||||
if (this._contentWidth > 0)
|
||||
containerClasses.remove("content-width" + this._contentWidth);
|
||||
|
|
@ -371,15 +385,15 @@ AboutReader.prototype = {
|
|||
return AsyncPrefs.set("reader.content_width", this._contentWidth);
|
||||
},
|
||||
|
||||
_setupContentWidthButtons: function() {
|
||||
_setupContentWidthButtons() {
|
||||
const CONTENT_WIDTH_MIN = 1;
|
||||
const CONTENT_WIDTH_MAX = 9;
|
||||
|
||||
let currentContentWidth = Services.prefs.getIntPref("reader.content_width");
|
||||
currentContentWidth = Math.max(CONTENT_WIDTH_MIN, Math.min(CONTENT_WIDTH_MAX, currentContentWidth));
|
||||
|
||||
let plusButton = this._doc.getElementById("content-width-plus");
|
||||
let minusButton = this._doc.getElementById("content-width-minus");
|
||||
let plusButton = this._doc.querySelector(".content-width-plus-button");
|
||||
let minusButton = this._doc.querySelector(".content-width-minus-button");
|
||||
|
||||
function updateControls() {
|
||||
if (currentContentWidth === CONTENT_WIDTH_MIN) {
|
||||
|
|
@ -428,8 +442,8 @@ AboutReader.prototype = {
|
|||
}, true);
|
||||
},
|
||||
|
||||
_setLineHeight: function(newLineHeight) {
|
||||
let contentClasses = this._doc.getElementById("moz-reader-content").classList;
|
||||
_setLineHeight(newLineHeight) {
|
||||
let contentClasses = this._contentElement.classList;
|
||||
|
||||
if (this._lineHeight > 0)
|
||||
contentClasses.remove("line-height" + this._lineHeight);
|
||||
|
|
@ -439,15 +453,15 @@ AboutReader.prototype = {
|
|||
return AsyncPrefs.set("reader.line_height", this._lineHeight);
|
||||
},
|
||||
|
||||
_setupLineHeightButtons: function() {
|
||||
_setupLineHeightButtons() {
|
||||
const LINE_HEIGHT_MIN = 1;
|
||||
const LINE_HEIGHT_MAX = 9;
|
||||
|
||||
let currentLineHeight = Services.prefs.getIntPref("reader.line_height");
|
||||
currentLineHeight = Math.max(LINE_HEIGHT_MIN, Math.min(LINE_HEIGHT_MAX, currentLineHeight));
|
||||
|
||||
let plusButton = this._doc.getElementById("line-height-plus");
|
||||
let minusButton = this._doc.getElementById("line-height-minus");
|
||||
let plusButton = this._doc.querySelector(".line-height-plus-button");
|
||||
let minusButton = this._doc.querySelector(".line-height-minus-button");
|
||||
|
||||
function updateControls() {
|
||||
if (currentLineHeight === LINE_HEIGHT_MIN) {
|
||||
|
|
@ -496,7 +510,7 @@ AboutReader.prototype = {
|
|||
}, true);
|
||||
},
|
||||
|
||||
_handleDeviceLight: function(newLux) {
|
||||
_handleDeviceLight(newLux) {
|
||||
// Desired size of the this._luxValues array.
|
||||
let luxValuesSize = 10;
|
||||
// Add new lux value at the front of the array.
|
||||
|
|
@ -513,7 +527,7 @@ AboutReader.prototype = {
|
|||
return;
|
||||
}
|
||||
// Holds the average of the lux values collected in this._luxValues.
|
||||
let averageLuxValue = this._totalLux/luxValuesSize;
|
||||
let averageLuxValue = this._totalLux / luxValuesSize;
|
||||
|
||||
this._updateColorScheme(averageLuxValue);
|
||||
// Pop the oldest value off the array.
|
||||
|
|
@ -522,7 +536,7 @@ AboutReader.prototype = {
|
|||
this._totalLux -= oldLux;
|
||||
},
|
||||
|
||||
_handleVisibilityChange: function() {
|
||||
_handleVisibilityChange() {
|
||||
let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
|
||||
if (colorScheme != "auto") {
|
||||
return;
|
||||
|
|
@ -533,19 +547,19 @@ AboutReader.prototype = {
|
|||
},
|
||||
|
||||
// Setup or teardown the ambient light tracking system.
|
||||
_enableAmbientLighting: function(enable) {
|
||||
_enableAmbientLighting(enable) {
|
||||
if (enable) {
|
||||
this._win.addEventListener("devicelight", this, false);
|
||||
this._win.addEventListener("devicelight", this);
|
||||
this._luxValues = [];
|
||||
this._totalLux = 0;
|
||||
} else {
|
||||
this._win.removeEventListener("devicelight", this, false);
|
||||
this._win.removeEventListener("devicelight", this);
|
||||
delete this._luxValues;
|
||||
delete this._totalLux;
|
||||
}
|
||||
},
|
||||
|
||||
_updateColorScheme: function(luxValue) {
|
||||
_updateColorScheme(luxValue) {
|
||||
// Upper bound value for "dark" color scheme beyond which it changes to "light".
|
||||
let upperBoundDark = 50;
|
||||
// Lower bound value for "light" color scheme beyond which it changes to "dark".
|
||||
|
|
@ -564,7 +578,7 @@ AboutReader.prototype = {
|
|||
this._setColorScheme("light");
|
||||
},
|
||||
|
||||
_setColorScheme: function(newColorScheme) {
|
||||
_setColorScheme(newColorScheme) {
|
||||
// "auto" is not a real color scheme
|
||||
if (this._colorScheme === newColorScheme || newColorScheme === "auto")
|
||||
return;
|
||||
|
|
@ -580,14 +594,14 @@ AboutReader.prototype = {
|
|||
|
||||
// Pref values include "dark", "light", and "auto", which automatically switches
|
||||
// between light and dark color schemes based on the ambient light level.
|
||||
_setColorSchemePref: function(colorSchemePref) {
|
||||
_setColorSchemePref(colorSchemePref) {
|
||||
this._enableAmbientLighting(colorSchemePref === "auto");
|
||||
this._setColorScheme(colorSchemePref);
|
||||
|
||||
AsyncPrefs.set("reader.color_scheme", colorSchemePref);
|
||||
},
|
||||
|
||||
_setFontType: function(newFontType) {
|
||||
_setFontType(newFontType) {
|
||||
if (this._fontType === newFontType)
|
||||
return;
|
||||
|
||||
|
|
@ -602,20 +616,38 @@ AboutReader.prototype = {
|
|||
AsyncPrefs.set("reader.font_type", this._fontType);
|
||||
},
|
||||
|
||||
_setSystemUIVisibility: function(visible) {
|
||||
_setSystemUIVisibility(visible) {
|
||||
this._mm.sendAsyncMessage("Reader:SystemUIVisibility", { visible: visible });
|
||||
},
|
||||
|
||||
_loadArticle: Task.async(function* () {
|
||||
_setToolbarVisibility(visible) {
|
||||
let tb = this._toolbarElement;
|
||||
|
||||
if (visible) {
|
||||
if (tb.style.opacity != "1") {
|
||||
tb.removeAttribute("hidden");
|
||||
tb.style.opacity = "1";
|
||||
}
|
||||
} else if (tb.style.opacity != "0") {
|
||||
tb.addEventListener("transitionend", evt => {
|
||||
if (tb.style.opacity == "0") {
|
||||
tb.setAttribute("hidden", "");
|
||||
}
|
||||
}, { once: true });
|
||||
tb.style.opacity = "0";
|
||||
}
|
||||
},
|
||||
|
||||
async _loadArticle() {
|
||||
let url = this._getOriginalUrl();
|
||||
this._showProgressDelayed();
|
||||
|
||||
let article;
|
||||
if (this._articlePromise) {
|
||||
article = yield this._articlePromise;
|
||||
article = await this._articlePromise;
|
||||
} else {
|
||||
try {
|
||||
article = yield this._getArticle(url);
|
||||
article = await this._getArticle(url);
|
||||
} catch (e) {
|
||||
if (e && e.newURL) {
|
||||
let readerURL = "about:reader?url=" + encodeURIComponent(e.newURL);
|
||||
|
|
@ -638,24 +670,26 @@ AboutReader.prototype = {
|
|||
}
|
||||
|
||||
this._showContent(article);
|
||||
}),
|
||||
|
||||
_getArticle: function(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let listener = (message) => {
|
||||
this._mm.removeMessageListener("Reader:ArticleData", listener);
|
||||
if (message.data.newURL) {
|
||||
reject({ newURL: message.data.newURL });
|
||||
return;
|
||||
}
|
||||
resolve(message.data.article);
|
||||
};
|
||||
this._mm.addMessageListener("Reader:ArticleData", listener);
|
||||
this._mm.sendAsyncMessage("Reader:ArticleGet", { url: url });
|
||||
});
|
||||
},
|
||||
|
||||
_requestFavicon: function() {
|
||||
_getArticle(url) {
|
||||
// return new Promise((resolve, reject) => {
|
||||
// let listener = (message) => {
|
||||
// this._mm.removeMessageListener("Reader:ArticleData", listener);
|
||||
// if (message.data.newURL) {
|
||||
// reject({ newURL: message.data.newURL });
|
||||
// return;
|
||||
// }
|
||||
// resolve(message.data.article);
|
||||
// };
|
||||
// this._mm.addMessageListener("Reader:ArticleData", listener);
|
||||
// this._mm.sendAsyncMessage("Reader:ArticleGet", { url });
|
||||
// });
|
||||
//}
|
||||
return ReaderMode.downloadAndParseDocument(url);
|
||||
},
|
||||
|
||||
_requestFavicon() {
|
||||
let handleFaviconReturn = (message) => {
|
||||
this._mm.removeMessageListener("Reader:FaviconReturn", handleFaviconReturn);
|
||||
this._loadFavicon(message.data.url, message.data.faviconUrl);
|
||||
|
|
@ -665,20 +699,20 @@ AboutReader.prototype = {
|
|||
this._mm.sendAsyncMessage("Reader:FaviconRequest", { url: this._article.url });
|
||||
},
|
||||
|
||||
_loadFavicon: function(url, faviconUrl) {
|
||||
_loadFavicon(url, faviconUrl) {
|
||||
if (this._article.url !== url)
|
||||
return;
|
||||
|
||||
let doc = this._doc;
|
||||
|
||||
let link = doc.createElement('link');
|
||||
link.rel = 'shortcut icon';
|
||||
let link = doc.createElement("link");
|
||||
link.rel = "shortcut icon";
|
||||
link.href = faviconUrl;
|
||||
|
||||
doc.getElementsByTagName('head')[0].appendChild(link);
|
||||
doc.getElementsByTagName("head")[0].appendChild(link);
|
||||
},
|
||||
|
||||
_updateImageMargins: function() {
|
||||
_updateImageMargins() {
|
||||
let windowWidth = this._win.innerWidth;
|
||||
let bodyWidth = this._doc.body.clientWidth;
|
||||
|
||||
|
|
@ -691,7 +725,7 @@ AboutReader.prototype = {
|
|||
}
|
||||
|
||||
// If the image is at least half as wide as the body, center it on desktop.
|
||||
if (img.naturalWidth >= bodyWidth/2) {
|
||||
if (img.naturalWidth >= bodyWidth / 2) {
|
||||
img.setAttribute("moz-reader-center", true);
|
||||
} else {
|
||||
img.removeAttribute("moz-reader-center");
|
||||
|
|
@ -713,30 +747,32 @@ AboutReader.prototype = {
|
|||
},
|
||||
|
||||
_maybeSetTextDirection: function Read_maybeSetTextDirection(article) {
|
||||
if (!article.dir)
|
||||
return;
|
||||
if (article.dir) {
|
||||
// Set "dir" attribute on content
|
||||
this._contentElement.setAttribute("dir", article.dir);
|
||||
this._headerElement.setAttribute("dir", article.dir);
|
||||
|
||||
// Set "dir" attribute on content
|
||||
this._contentElement.setAttribute("dir", article.dir);
|
||||
this._headerElement.setAttribute("dir", article.dir);
|
||||
},
|
||||
|
||||
_fixLocalLinks() {
|
||||
// We need to do this because preprocessing the content through nsIParserUtils
|
||||
// gives back a DOM with a <base> element. That influences how these URLs get
|
||||
// resolved, making them no longer match the document URI (which is
|
||||
// about:reader?url=...). To fix this, make all the hash URIs absolute. This
|
||||
// is hacky, but the alternative of removing the base element has potential
|
||||
// security implications if Readability has not successfully made all the URLs
|
||||
// absolute, so we pick just fixing these in-document links explicitly.
|
||||
let localLinks = this._contentElement.querySelectorAll("a[href^='#']");
|
||||
for (let localLink of localLinks) {
|
||||
// Have to get the attribute because .href provides an absolute URI.
|
||||
localLink.href = this._doc.documentURI + localLink.getAttribute("href");
|
||||
// The native locale could be set differently than the article's text direction.
|
||||
var localeDirection = Services.locale.isAppLocaleRTL ? "rtl" : "ltr";
|
||||
this._readTimeElement.setAttribute("dir", localeDirection);
|
||||
this._readTimeElement.style.textAlign = article.dir == "rtl" ? "right" : "left";
|
||||
}
|
||||
},
|
||||
|
||||
_showError: function() {
|
||||
_formatReadTime(slowEstimate, fastEstimate) {
|
||||
let displayStringKey = "aboutReader.estimatedReadTimeRange1";
|
||||
|
||||
// only show one reading estimate when they are the same value
|
||||
if (slowEstimate == fastEstimate) {
|
||||
displayStringKey = "aboutReader.estimatedReadTimeValue1";
|
||||
}
|
||||
|
||||
return PluralForm.get(slowEstimate, gStrings.GetStringFromName(displayStringKey))
|
||||
.replace("#1", fastEstimate)
|
||||
.replace("#2", slowEstimate);
|
||||
},
|
||||
|
||||
_showError() {
|
||||
this._headerElement.style.display = "none";
|
||||
this._contentElement.style.display = "none";
|
||||
|
||||
|
|
@ -746,11 +782,16 @@ AboutReader.prototype = {
|
|||
|
||||
this._doc.title = errorMessage;
|
||||
|
||||
this._doc.documentElement.dataset.isError = true;
|
||||
|
||||
this._error = true;
|
||||
|
||||
this._doc.dispatchEvent(
|
||||
new this._win.CustomEvent("AboutReaderContentError", { bubbles: true, cancelable: false }));
|
||||
},
|
||||
|
||||
// This function is the JS version of Java's StringUtils.stripCommonSubdomains.
|
||||
_stripHost: function(host) {
|
||||
_stripHost(host) {
|
||||
if (!host)
|
||||
return host;
|
||||
|
||||
|
|
@ -766,17 +807,18 @@ AboutReader.prototype = {
|
|||
return host.substring(start);
|
||||
},
|
||||
|
||||
_showContent: function(article) {
|
||||
_showContent(article) {
|
||||
this._messageElement.style.display = "none";
|
||||
|
||||
this._article = article;
|
||||
|
||||
this._domainElement.href = article.url;
|
||||
let articleUri = Services.io.newURI(article.url, null, null);
|
||||
let articleUri = Services.io.newURI(article.url);
|
||||
this._domainElement.textContent = this._stripHost(articleUri.host);
|
||||
this._creditsElement.textContent = article.byline;
|
||||
|
||||
this._titleElement.textContent = article.title;
|
||||
this._readTimeElement.textContent = this._formatReadTime(article.readingTimeMinsSlow, article.readingTimeMinsFast);
|
||||
this._doc.title = article.title;
|
||||
|
||||
this._headerElement.style.display = "block";
|
||||
|
|
@ -787,8 +829,8 @@ AboutReader.prototype = {
|
|||
false, articleUri, this._contentElement);
|
||||
this._contentElement.innerHTML = "";
|
||||
this._contentElement.appendChild(contentFragment);
|
||||
this._fixLocalLinks();
|
||||
this._maybeSetTextDirection(article);
|
||||
this._foundLanguage(article.language);
|
||||
|
||||
this._contentElement.style.display = "block";
|
||||
this._updateImageMargins();
|
||||
|
|
@ -804,13 +846,13 @@ AboutReader.prototype = {
|
|||
new this._win.CustomEvent("AboutReaderContentReady", { bubbles: true, cancelable: false }));
|
||||
},
|
||||
|
||||
_hideContent: function() {
|
||||
_hideContent() {
|
||||
this._headerElement.style.display = "none";
|
||||
this._contentElement.style.display = "none";
|
||||
},
|
||||
|
||||
_showProgressDelayed: function() {
|
||||
this._win.setTimeout(function() {
|
||||
_showProgressDelayed() {
|
||||
this._win.setTimeout(() => {
|
||||
// No need to show progress if the article has been loaded,
|
||||
// if the window has been unloaded, or if there was an error
|
||||
// trying to load the article.
|
||||
|
|
@ -823,20 +865,20 @@ AboutReader.prototype = {
|
|||
|
||||
this._messageElement.textContent = gStrings.GetStringFromName("aboutReader.loading2");
|
||||
this._messageElement.style.display = "block";
|
||||
}.bind(this), 300);
|
||||
}, 300);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the original article URL for this about:reader view.
|
||||
*/
|
||||
_getOriginalUrl: function(win) {
|
||||
_getOriginalUrl(win) {
|
||||
let url = win ? win.location.href : this._win.location.href;
|
||||
return ReaderMode.getOriginalUrl(url) || url;
|
||||
},
|
||||
|
||||
_setupSegmentedButton: function(id, options, initialValue, callback) {
|
||||
_setupSegmentedButton(id, options, initialValue, callback) {
|
||||
let doc = this._doc;
|
||||
let segmentedButton = doc.getElementById(id);
|
||||
let segmentedButton = doc.getElementsByClassName(id)[0];
|
||||
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
let option = options[i];
|
||||
|
|
@ -867,10 +909,6 @@ AboutReader.prototype = {
|
|||
|
||||
aEvent.stopPropagation();
|
||||
|
||||
// Just pass the ID of the button as an extra and hope the ID doesn't change
|
||||
// unless the context changes
|
||||
UITelemetry.addEvent("action.1", "button", null, id);
|
||||
|
||||
let items = segmentedButton.children;
|
||||
for (let j = items.length - 1; j >= 0; j--) {
|
||||
items[j].classList.remove("selected");
|
||||
|
|
@ -878,19 +916,19 @@ AboutReader.prototype = {
|
|||
|
||||
item.classList.add("selected");
|
||||
callback(option.value);
|
||||
}.bind(this), true);
|
||||
}, true);
|
||||
|
||||
if (option.value === initialValue)
|
||||
item.classList.add("selected");
|
||||
}
|
||||
},
|
||||
|
||||
_setupButton: function(id, callback, titleEntity, textEntity) {
|
||||
_setupButton(id, callback, titleEntity, textEntity) {
|
||||
if (titleEntity) {
|
||||
this._setButtonTip(id, titleEntity);
|
||||
}
|
||||
|
||||
let button = this._doc.getElementById(id);
|
||||
let button = this._doc.getElementsByClassName(id)[0];
|
||||
if (textEntity) {
|
||||
button.textContent = gStrings.GetStringFromName(textEntity);
|
||||
}
|
||||
|
|
@ -910,17 +948,17 @@ AboutReader.prototype = {
|
|||
* and dynamically as button state changes.
|
||||
* @param Localizable string providing UI element usage tip.
|
||||
*/
|
||||
_setButtonTip: function(id, titleEntity) {
|
||||
let button = this._doc.getElementById(id);
|
||||
_setButtonTip(id, titleEntity) {
|
||||
let button = this._doc.getElementsByClassName(id)[0];
|
||||
button.setAttribute("title", gStrings.GetStringFromName(titleEntity));
|
||||
},
|
||||
|
||||
_setupStyleDropdown: function() {
|
||||
let dropdownToggle = this._doc.querySelector("#style-dropdown .dropdown-toggle");
|
||||
_setupStyleDropdown() {
|
||||
let dropdownToggle = this._doc.querySelector(".style-dropdown .dropdown-toggle");
|
||||
dropdownToggle.setAttribute("title", gStrings.GetStringFromName("aboutReader.toolbar.typeControls"));
|
||||
},
|
||||
|
||||
_updatePopupPosition: function(dropdown) {
|
||||
_updatePopupPosition(dropdown) {
|
||||
let dropdownToggle = dropdown.querySelector(".dropdown-toggle");
|
||||
let dropdownPopup = dropdown.querySelector(".dropdown-popup");
|
||||
|
||||
|
|
@ -931,8 +969,8 @@ AboutReader.prototype = {
|
|||
dropdownPopup.style.top = popupTop + "px";
|
||||
},
|
||||
|
||||
_toggleDropdownClicked: function(event) {
|
||||
let dropdown = event.target.closest('.dropdown');
|
||||
_toggleDropdownClicked(event) {
|
||||
let dropdown = event.target.closest(".dropdown");
|
||||
|
||||
if (!dropdown)
|
||||
return;
|
||||
|
|
@ -952,7 +990,7 @@ AboutReader.prototype = {
|
|||
/*
|
||||
* If the ReaderView banner font-dropdown is closed, open it.
|
||||
*/
|
||||
_openDropdown: function(dropdown) {
|
||||
_openDropdown(dropdown) {
|
||||
if (dropdown.classList.contains("open")) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -969,7 +1007,7 @@ AboutReader.prototype = {
|
|||
* dropdowns because the page is scrolling, allow popups to stay open with
|
||||
* the keep-open class.
|
||||
*/
|
||||
_closeDropdowns: function(scrolling) {
|
||||
_closeDropdowns(scrolling) {
|
||||
let selector = ".dropdown.open";
|
||||
if (scrolling) {
|
||||
selector += ":not(.keep-open)";
|
||||
|
|
@ -986,6 +1024,18 @@ AboutReader.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Override link handling for same-page references so we don't exit Reader View.
|
||||
*/
|
||||
_linkClicked(event) {
|
||||
var originalUrl = Services.io.newURI(this._getOriginalUrl(), null, null);
|
||||
var targetUrl = Services.io.newURI(event.target.href, null, null);
|
||||
if (originalUrl.specIgnoringRef == targetUrl.specIgnoringRef) {
|
||||
event.preventDefault();
|
||||
this._goToReference(targetUrl.ref);
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Scroll reader view to a reference
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1017,46 +1017,6 @@
|
|||
}
|
||||
},
|
||||
|
||||
readScript: function (node) {
|
||||
while (this.currentChar < this.html.length) {
|
||||
var c = this.nextChar();
|
||||
var nextC = this.peekNext();
|
||||
if (c === "<") {
|
||||
if (nextC === "!" || nextC === "?") {
|
||||
// We're still before the ! or ? that is starting this comment:
|
||||
this.currentChar++;
|
||||
node.appendChild(this.discardNextComment());
|
||||
continue;
|
||||
}
|
||||
if (nextC === "/" && this.html.substr(this.currentChar, 8 /*"/script>".length */).toLowerCase() == "/script>") {
|
||||
// Go back before the '<' so we find the end tag.
|
||||
this.currentChar--;
|
||||
// Done with this script tag, the caller will close:
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Either c wasn't a '<' or it was but we couldn't find either a comment
|
||||
// or a closing script tag, so we should just parse as text until the next one
|
||||
// comes along:
|
||||
|
||||
var haveTextNode = node.lastChild && node.lastChild.nodeType === Node.TEXT_NODE;
|
||||
var textNode = haveTextNode ? node.lastChild : new Text();
|
||||
var n = this.html.indexOf("<", this.currentChar);
|
||||
// Decrement this to include the current character *afterwards* so we don't get stuck
|
||||
// looking for the same < all the time.
|
||||
this.currentChar--;
|
||||
if (n === -1) {
|
||||
textNode.innerHTML += this.html.substring(this.currentChar, this.html.length);
|
||||
this.currentChar = this.html.length;
|
||||
} else {
|
||||
textNode.innerHTML += this.html.substring(this.currentChar, n);
|
||||
this.currentChar = n;
|
||||
}
|
||||
if (!haveTextNode)
|
||||
node.appendChild(textNode);
|
||||
}
|
||||
},
|
||||
|
||||
discardNextComment: function() {
|
||||
if (this.match("--")) {
|
||||
this.discardTo("-->");
|
||||
|
|
@ -1131,11 +1091,7 @@
|
|||
|
||||
// If this isn't a void Element, read its child nodes
|
||||
if (!closed) {
|
||||
if (localName == "script") {
|
||||
this.readScript(node);
|
||||
} else {
|
||||
this.readChildren(node);
|
||||
}
|
||||
this.readChildren(node);
|
||||
var closingTag = "</" + localName + ">";
|
||||
if (!this.match(closingTag)) {
|
||||
this.error("expected '" + closingTag + "' and got " + this.html.substr(this.currentChar, closingTag.length));
|
||||
|
|
|
|||
|
|
@ -8,15 +8,18 @@ this.EXPORTED_SYMBOLS = ["ReaderMode"];
|
|||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
// Constants for telemetry.
|
||||
const DOWNLOAD_SUCCESS = 0;
|
||||
const DOWNLOAD_ERROR_XHR = 1;
|
||||
const DOWNLOAD_ERROR_NO_DOC = 2;
|
||||
|
||||
const PARSE_SUCCESS = 0;
|
||||
const PARSE_ERROR_TOO_MANY_ELEMENTS = 1;
|
||||
const PARSE_ERROR_WORKER = 2;
|
||||
const PARSE_ERROR_NO_ARTICLE = 3;
|
||||
// Class names to preserve in the readerized output. We preserve these class
|
||||
// names so that rules in aboutReader.css can match them.
|
||||
const CLASSES_TO_PRESERVE = [
|
||||
"caption",
|
||||
"hidden",
|
||||
"invisble",
|
||||
"sr-only",
|
||||
"visually-hidden",
|
||||
"visuallyhidden",
|
||||
"wp-caption",
|
||||
"wp-caption-text",
|
||||
];
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
|
@ -24,17 +27,15 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|||
Cu.importGlobalProperties(["XMLHttpRequest"]);
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils", "resource://services-common/utils.js");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "EventDispatcher", "resource://gre/modules/Messaging.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ReaderWorker", "resource://gre/modules/reader/ReaderWorker.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", "resource://gre/modules/TelemetryStopwatch.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "Readability", function() {
|
||||
let scope = {};
|
||||
scope.dump = this.dump;
|
||||
Services.scriptloader.loadSubScript("resource://gre/modules/reader/Readability.js", scope);
|
||||
return scope["Readability"];
|
||||
return scope.Readability;
|
||||
});
|
||||
|
||||
this.ReaderMode = {
|
||||
|
|
@ -61,21 +62,13 @@ this.ReaderMode = {
|
|||
return this.isEnabledForParseOnLoad = this._getStateForParseOnLoad();
|
||||
},
|
||||
|
||||
get isOnLowMemoryPlatform() {
|
||||
let memory = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory);
|
||||
delete this.isOnLowMemoryPlatform;
|
||||
return this.isOnLowMemoryPlatform = memory.isLowMemoryPlatform();
|
||||
},
|
||||
|
||||
_getStateForParseOnLoad: function () {
|
||||
_getStateForParseOnLoad() {
|
||||
let isEnabled = Services.prefs.getBoolPref("reader.parse-on-load.enabled");
|
||||
let isForceEnabled = Services.prefs.getBoolPref("reader.parse-on-load.force-enabled");
|
||||
// For low-memory devices, don't allow reader mode since it takes up a lot of memory.
|
||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=792603 for details.
|
||||
return isForceEnabled || (isEnabled && !this.isOnLowMemoryPlatform);
|
||||
return isForceEnabled || isEnabled;
|
||||
},
|
||||
|
||||
observe: function(aMessage, aTopic, aData) {
|
||||
observe(aMessage, aTopic, aData) {
|
||||
switch (aTopic) {
|
||||
case "nsPref:changed":
|
||||
if (aData.startsWith("reader.parse-on-load.")) {
|
||||
|
|
@ -91,7 +84,7 @@ this.ReaderMode = {
|
|||
* Enter the reader mode by going forward one step in history if applicable,
|
||||
* if not, append the about:reader page in the history instead.
|
||||
*/
|
||||
enterReaderMode: function(docShell, win) {
|
||||
enterReaderMode(docShell, win) {
|
||||
let url = win.document.location.href;
|
||||
let readerURL = "about:reader?url=" + encodeURIComponent(url);
|
||||
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
|
||||
|
|
@ -112,7 +105,7 @@ this.ReaderMode = {
|
|||
* Exit the reader mode by going back one step in history if applicable,
|
||||
* if not, append the original page in the history instead.
|
||||
*/
|
||||
leaveReaderMode: function(docShell, win) {
|
||||
leaveReaderMode(docShell, win) {
|
||||
let url = win.document.location.href;
|
||||
let originalURL = this.getOriginalUrl(url);
|
||||
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
|
||||
|
|
@ -136,14 +129,14 @@ this.ReaderMode = {
|
|||
* @return The original URL for the article, or null if we did not find
|
||||
* a properly formatted about:reader URL.
|
||||
*/
|
||||
getOriginalUrl: function(url) {
|
||||
getOriginalUrl(url) {
|
||||
if (!url.startsWith("about:reader?")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let outerHash = "";
|
||||
try {
|
||||
let uriObj = Services.io.newURI(url, null, null);
|
||||
let uriObj = Services.io.newURI(url);
|
||||
url = uriObj.specIgnoringRef;
|
||||
outerHash = uriObj.ref;
|
||||
} catch (ex) { /* ignore, use the raw string */ }
|
||||
|
|
@ -155,27 +148,45 @@ this.ReaderMode = {
|
|||
let originalUrl = searchParams.get("url");
|
||||
if (outerHash) {
|
||||
try {
|
||||
let uriObj = Services.io.newURI(originalUrl, null, null);
|
||||
uriObj = Services.io.newURI('#' + outerHash, null, uriObj);
|
||||
let uriObj = Services.io.newURI(originalUrl);
|
||||
uriObj = Services.io.newURI("#" + outerHash, null, uriObj);
|
||||
originalUrl = uriObj.spec;
|
||||
} catch (ex) {}
|
||||
}
|
||||
return originalUrl;
|
||||
},
|
||||
|
||||
getOriginalUrlObjectForDisplay(url) {
|
||||
let originalUrl = this.getOriginalUrl(url);
|
||||
if (originalUrl) {
|
||||
let uriObj;
|
||||
try {
|
||||
uriObj = Services.uriFixup.createFixupURI(originalUrl, Services.uriFixup.FIXUP_FLAG_NONE);
|
||||
} catch (ex) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Services.uriFixup.createExposableURI(uriObj);
|
||||
} catch (ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Decides whether or not a document is reader-able without parsing the whole thing.
|
||||
*
|
||||
* @param doc A document to parse.
|
||||
* @return boolean Whether or not we should show the reader mode button.
|
||||
*/
|
||||
isProbablyReaderable: function(doc) {
|
||||
isProbablyReaderable(doc) {
|
||||
// Only care about 'real' HTML documents:
|
||||
if (doc.mozSyntheticDocument || !(doc instanceof doc.defaultView.HTMLDocument)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let uri = Services.io.newURI(doc.location.href, null, null);
|
||||
let uri = Services.io.newURI(doc.location.href);
|
||||
if (!this._shouldCheckUri(uri)) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -187,12 +198,12 @@ this.ReaderMode = {
|
|||
return new Readability(uri, doc).isProbablyReaderable(this.isNodeVisible.bind(this, utils));
|
||||
},
|
||||
|
||||
isNodeVisible: function(utils, node) {
|
||||
isNodeVisible(utils, node) {
|
||||
let bounds = utils.getBoundsWithoutFlushing(node);
|
||||
return bounds.height > 0 && bounds.width > 0;
|
||||
},
|
||||
|
||||
getUtilsForWin: function(win) {
|
||||
getUtilsForWin(win) {
|
||||
return win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
|
||||
},
|
||||
|
||||
|
|
@ -204,16 +215,14 @@ this.ReaderMode = {
|
|||
* @return {Promise}
|
||||
* @resolves JS object representing the article, or null if no article is found.
|
||||
*/
|
||||
parseDocument: Task.async(function* (doc) {
|
||||
let documentURI = Services.io.newURI(doc.documentURI, null, null);
|
||||
let baseURI = Services.io.newURI(doc.baseURI, null, null);
|
||||
if (!this._shouldCheckUri(documentURI) || !this._shouldCheckUri(baseURI, true)) {
|
||||
parseDocument(doc) {
|
||||
if (!this._shouldCheckUri(doc.documentURIObject) || !this._shouldCheckUri(doc.baseURIObject, true)) {
|
||||
this.log("Reader mode disabled for URI");
|
||||
return null;
|
||||
}
|
||||
|
||||
return yield this._readerParse(baseURI, doc);
|
||||
}),
|
||||
return this._readerParse(doc);
|
||||
},
|
||||
|
||||
/**
|
||||
* Downloads and parses a document from a URL.
|
||||
|
|
@ -222,19 +231,28 @@ this.ReaderMode = {
|
|||
* @return {Promise}
|
||||
* @resolves JS object representing the article, or null if no article is found.
|
||||
*/
|
||||
downloadAndParseDocument: Task.async(function* (url) {
|
||||
let doc = yield this._downloadDocument(url);
|
||||
let uri = Services.io.newURI(doc.baseURI, null, null);
|
||||
if (!this._shouldCheckUri(uri, true)) {
|
||||
async downloadAndParseDocument(url) {
|
||||
let doc = await this._downloadDocument(url);
|
||||
if (!doc) {
|
||||
return null;
|
||||
}
|
||||
if (!this._shouldCheckUri(doc.documentURIObject) || !this._shouldCheckUri(doc.baseURIObject, true)) {
|
||||
this.log("Reader mode disabled for URI");
|
||||
return null;
|
||||
}
|
||||
|
||||
return yield this._readerParse(uri, doc);
|
||||
}),
|
||||
return await this._readerParse(doc);
|
||||
},
|
||||
|
||||
_downloadDocument: function (url) {
|
||||
let histogram = Services.telemetry.getHistogramById("READER_MODE_DOWNLOAD_RESULT");
|
||||
_downloadDocument(url) {
|
||||
try {
|
||||
if (!this._shouldCheckUri(Services.io.newURI(url))) {
|
||||
return null;
|
||||
}
|
||||
} catch (ex) {
|
||||
Cu.reportError(new Error(`Couldn't create URI from ${url} to download: ${ex}`));
|
||||
return null;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", url, true);
|
||||
|
|
@ -243,14 +261,12 @@ this.ReaderMode = {
|
|||
xhr.onload = evt => {
|
||||
if (xhr.status !== 200) {
|
||||
reject("Reader mode XHR failed with status: " + xhr.status);
|
||||
histogram.add(DOWNLOAD_ERROR_XHR);
|
||||
return;
|
||||
}
|
||||
|
||||
let doc = xhr.responseXML;
|
||||
if (!doc) {
|
||||
reject("Reader mode XHR didn't return a document");
|
||||
histogram.add(DOWNLOAD_ERROR_NO_DOC);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -261,7 +277,7 @@ this.ReaderMode = {
|
|||
if (content) {
|
||||
let urlIndex = content.toUpperCase().indexOf("URL=");
|
||||
if (urlIndex > -1) {
|
||||
let baseURI = Services.io.newURI(url, null, null);
|
||||
let baseURI = Services.io.newURI(url);
|
||||
let newURI = Services.io.newURI(content.substring(urlIndex + 4), null, baseURI);
|
||||
let newURL = newURI.spec;
|
||||
let ssm = Services.scriptSecurityManager;
|
||||
|
|
@ -290,10 +306,10 @@ this.ReaderMode = {
|
|||
// Convert these to real URIs to make sure the escaping (or lack
|
||||
// thereof) is identical:
|
||||
try {
|
||||
responseURL = Services.io.newURI(responseURL, null, null).specIgnoringRef;
|
||||
responseURL = Services.io.newURI(responseURL).specIgnoringRef;
|
||||
} catch (ex) { /* Ignore errors - we'll use what we had before */ }
|
||||
try {
|
||||
givenURL = Services.io.newURI(givenURL, null, null).specIgnoringRef;
|
||||
givenURL = Services.io.newURI(givenURL).specIgnoringRef;
|
||||
} catch (ex) { /* Ignore errors - we'll use what we had before */ }
|
||||
|
||||
if (responseURL != givenURL) {
|
||||
|
|
@ -303,7 +319,6 @@ this.ReaderMode = {
|
|||
return;
|
||||
}
|
||||
resolve(doc);
|
||||
histogram.add(DOWNLOAD_SUCCESS);
|
||||
};
|
||||
xhr.send();
|
||||
});
|
||||
|
|
@ -318,17 +333,17 @@ this.ReaderMode = {
|
|||
* @resolves JS object representing the article, or null if no article is found.
|
||||
* @rejects OS.File.Error
|
||||
*/
|
||||
getArticleFromCache: Task.async(function* (url) {
|
||||
async getArticleFromCache(url) {
|
||||
let path = this._toHashedPath(url);
|
||||
try {
|
||||
let array = yield OS.File.read(path);
|
||||
let array = await OS.File.read(path);
|
||||
return JSON.parse(new TextDecoder().decode(array));
|
||||
} catch (e) {
|
||||
if (!(e instanceof OS.File.Error) || !e.becauseNoSuchFile)
|
||||
throw e;
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
/**
|
||||
* Stores an article in the cache.
|
||||
|
|
@ -338,14 +353,14 @@ this.ReaderMode = {
|
|||
* @resolves When the article is stored.
|
||||
* @rejects OS.File.Error
|
||||
*/
|
||||
storeArticleInCache: Task.async(function* (article) {
|
||||
async storeArticleInCache(article) {
|
||||
let array = new TextEncoder().encode(JSON.stringify(article));
|
||||
let path = this._toHashedPath(article.url);
|
||||
yield this._ensureCacheDir();
|
||||
await this._ensureCacheDir();
|
||||
return OS.File.writeAtomic(path, array, { tmpPath: path + ".tmp" })
|
||||
.then(success => {
|
||||
OS.File.stat(path).then(info => {
|
||||
return Messaging.sendRequest({
|
||||
return EventDispatcher.instance.sendRequest({
|
||||
type: "Reader:AddedToCache",
|
||||
url: article.url,
|
||||
size: info.size,
|
||||
|
|
@ -353,7 +368,7 @@ this.ReaderMode = {
|
|||
});
|
||||
});
|
||||
});
|
||||
}),
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes an article from the cache given an article URI.
|
||||
|
|
@ -363,26 +378,29 @@ this.ReaderMode = {
|
|||
* @resolves When the article is removed.
|
||||
* @rejects OS.File.Error
|
||||
*/
|
||||
removeArticleFromCache: Task.async(function* (url) {
|
||||
async removeArticleFromCache(url) {
|
||||
let path = this._toHashedPath(url);
|
||||
yield OS.File.remove(path);
|
||||
}),
|
||||
await OS.File.remove(path);
|
||||
},
|
||||
|
||||
log: function(msg) {
|
||||
log(msg) {
|
||||
if (this.DEBUG)
|
||||
dump("Reader: " + msg);
|
||||
},
|
||||
|
||||
_blockedHosts: [
|
||||
"mail.google.com",
|
||||
"amazon.com",
|
||||
"basilisk-browser.org",
|
||||
"github.com",
|
||||
"mail.google.com",
|
||||
"palemoon.org",
|
||||
"pinterest.com",
|
||||
"reddit.com",
|
||||
"twitter.com",
|
||||
"youtube.com",
|
||||
],
|
||||
|
||||
_shouldCheckUri: function (uri, isBaseUri = false) {
|
||||
_shouldCheckUri(uri, isBaseUri = false) {
|
||||
if (!(uri.schemeIs("http") || uri.schemeIs("https"))) {
|
||||
this.log("Not parsing URI scheme: " + uri.scheme);
|
||||
return false;
|
||||
|
|
@ -412,59 +430,77 @@ this.ReaderMode = {
|
|||
* Attempts to parse a document into an article. Heavy lifting happens
|
||||
* in readerWorker.js.
|
||||
*
|
||||
* @param uri The base URI of the article.
|
||||
* @param doc The document to parse.
|
||||
* @return {Promise}
|
||||
* @resolves JS object representing the article, or null if no article is found.
|
||||
*/
|
||||
_readerParse: Task.async(function* (uri, doc) {
|
||||
let histogram = Services.telemetry.getHistogramById("READER_MODE_PARSE_RESULT");
|
||||
async _readerParse(doc) {
|
||||
if (this.parseNodeLimit) {
|
||||
let numTags = doc.getElementsByTagName("*").length;
|
||||
if (numTags > this.parseNodeLimit) {
|
||||
this.log("Aborting parse for " + uri.spec + "; " + numTags + " elements found");
|
||||
histogram.add(PARSE_ERROR_TOO_MANY_ELEMENTS);
|
||||
this.log("Aborting parse for " + doc.baseURIObject.spec + "; " + numTags + " elements found");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch this here before we send `doc` off to the worker thread, as later on the
|
||||
// document might be nuked but we will still want the URI.
|
||||
let {documentURI} = doc;
|
||||
|
||||
let uriParam = {
|
||||
spec: uri.spec,
|
||||
host: uri.host,
|
||||
prePath: uri.prePath,
|
||||
scheme: uri.scheme,
|
||||
pathBase: Services.io.newURI(".", null, uri).spec
|
||||
spec: doc.baseURIObject.spec,
|
||||
host: doc.baseURIObject.host,
|
||||
prePath: doc.baseURIObject.prePath,
|
||||
scheme: doc.baseURIObject.scheme,
|
||||
pathBase: Services.io.newURI(".", null, doc.baseURIObject).spec
|
||||
};
|
||||
|
||||
let langAttributes = {
|
||||
charset: doc.characterSet,
|
||||
lang: doc.documentElement.lang
|
||||
};
|
||||
|
||||
let serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"].
|
||||
createInstance(Ci.nsIDOMSerializer);
|
||||
let serializedDoc = serializer.serializeToString(doc);
|
||||
|
||||
let options = {
|
||||
classesToPreserve: CLASSES_TO_PRESERVE,
|
||||
};
|
||||
|
||||
let article = null;
|
||||
try {
|
||||
article = yield ReaderWorker.post("parseDocument", [uriParam, serializedDoc]);
|
||||
article = await ReaderWorker.post("parseDocument", [uriParam, serializedDoc, options]);
|
||||
} catch (e) {
|
||||
Cu.reportError("Error in ReaderWorker: " + e);
|
||||
histogram.add(PARSE_ERROR_WORKER);
|
||||
}
|
||||
|
||||
// Explicitly null out doc to make it clear it might not be available from this
|
||||
// point on.
|
||||
doc = null;
|
||||
|
||||
if (!article) {
|
||||
this.log("Worker did not return an article");
|
||||
histogram.add(PARSE_ERROR_NO_ARTICLE);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Readability returns a URI object, but we only care about the URL.
|
||||
article.url = article.uri.spec;
|
||||
// Readability returns a URI object based on the baseURI, but we only care
|
||||
// about the original document's URL from now on. This also avoids spoofing
|
||||
// attempts where the baseURI doesn't match the domain of the documentURI
|
||||
article.url = documentURI;
|
||||
delete article.uri;
|
||||
|
||||
let flags = Ci.nsIDocumentEncoder.OutputSelectionOnly | Ci.nsIDocumentEncoder.OutputAbsoluteLinks;
|
||||
article.title = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils)
|
||||
.convertToPlainText(article.title, flags, 0);
|
||||
|
||||
histogram.add(PARSE_SUCCESS);
|
||||
await this._assignLanguage(article, langAttributes);
|
||||
this._maybeAssignTextDirection(article);
|
||||
|
||||
this._assignReadTime(article);
|
||||
|
||||
return article;
|
||||
}),
|
||||
},
|
||||
|
||||
get _cryptoHash() {
|
||||
delete this._cryptoHash;
|
||||
|
|
@ -485,7 +521,7 @@ this.ReaderMode = {
|
|||
* @param url The article URL. This should have referrers removed.
|
||||
* @return The file path to the cached article.
|
||||
*/
|
||||
_toHashedPath: function (url) {
|
||||
_toHashedPath(url) {
|
||||
let value = this._unicodeConverter.convertToByteArray(url);
|
||||
this._cryptoHash.init(this._cryptoHash.MD5);
|
||||
this._cryptoHash.update(value, value.length);
|
||||
|
|
@ -502,7 +538,7 @@ this.ReaderMode = {
|
|||
* @resolves When the cache directory exists.
|
||||
* @rejects OS.File.Error
|
||||
*/
|
||||
_ensureCacheDir: function () {
|
||||
_ensureCacheDir() {
|
||||
let dir = OS.Path.join(OS.Constants.Path.profileDir, "readercache");
|
||||
return OS.File.exists(dir).then(exists => {
|
||||
if (!exists) {
|
||||
|
|
@ -510,5 +546,107 @@ this.ReaderMode = {
|
|||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets a global language string value if possible. If langauge detection is
|
||||
* available, use that. Otherwise, revert to a simpler mechanism using the
|
||||
* document's lang attribute or charset.
|
||||
*
|
||||
* @return Promise
|
||||
* @resolves when the language is detected
|
||||
*/
|
||||
_assignLanguage(article, attributes) {
|
||||
try {
|
||||
Cu.import("resource://modules/translation/LanguageDetector.jsm");
|
||||
return LanguageDetector.detectLanguage(article.textContent).then(result => {
|
||||
article.language = result.confident ? result.language : null;
|
||||
});
|
||||
} catch(ex) {
|
||||
return new Promise((resolve) => {
|
||||
resolve(this._assignSimpleLanguage(attributes));
|
||||
}).then(result => {
|
||||
article.language = result;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_assignSimpleLanguage(attributes) {
|
||||
var lang = attributes.lang.substring(0,2);
|
||||
if (lang) {
|
||||
return lang;
|
||||
}
|
||||
|
||||
// If there is no lang attribute, try the charset.
|
||||
// We can only use this for charsets that are specific to one language.
|
||||
const charsetLang = new Map([
|
||||
[ "us-ascii", "en" ],
|
||||
[ "iso-8859-6", "ar" ],
|
||||
[ "iso-8859-7", "el" ],
|
||||
[ "iso-8859-8", "he" ],
|
||||
[ "iso-8859-9", "tr" ],
|
||||
[ "iso-8859-11", "th" ],
|
||||
[ "jis_x0201", "ja" ],
|
||||
[ "shift_jis", "ja" ],
|
||||
[ "euc-jp", "ja" ]
|
||||
]);
|
||||
|
||||
return charsetLang.get(attributes.charset);
|
||||
},
|
||||
|
||||
_maybeAssignTextDirection(article) {
|
||||
// TODO: Remove the hardcoded language codes below once bug 1320265 is resolved.
|
||||
if (!article.dir && ["ar", "fa", "he", "ug", "ur"].includes(article.language)) {
|
||||
article.dir = "rtl";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Assigns the estimated reading time range of the article to the article object.
|
||||
*
|
||||
* @param article the article object to assign the reading time estimate to.
|
||||
*/
|
||||
_assignReadTime(article) {
|
||||
let lang = article.language || "en";
|
||||
const readingSpeed = this._getReadingSpeedForLanguage(lang);
|
||||
const charactersPerMinuteLow = readingSpeed.cpm - readingSpeed.variance;
|
||||
const charactersPerMinuteHigh = readingSpeed.cpm + readingSpeed.variance;
|
||||
const length = article.length;
|
||||
|
||||
article.readingTimeMinsSlow = Math.ceil(length / charactersPerMinuteLow);
|
||||
article.readingTimeMinsFast = Math.ceil(length / charactersPerMinuteHigh);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the reading speed of a selection of languages with likely variance.
|
||||
*
|
||||
* Reading speed estimated from a study done on reading speeds in various languages.
|
||||
* study can be found here: http://iovs.arvojournals.org/article.aspx?articleid=2166061
|
||||
*
|
||||
* @return object with characters per minute and variance. Defaults to English
|
||||
* if no suitable language is found in the collection.
|
||||
*/
|
||||
_getReadingSpeedForLanguage(lang) {
|
||||
const readingSpeed = new Map([
|
||||
[ "en", {cpm: 987, variance: 118 } ],
|
||||
[ "ar", {cpm: 612, variance: 88 } ],
|
||||
[ "de", {cpm: 920, variance: 86 } ],
|
||||
[ "es", {cpm: 1025, variance: 127 } ],
|
||||
[ "fi", {cpm: 1078, variance: 121 } ],
|
||||
[ "fr", {cpm: 998, variance: 126 } ],
|
||||
[ "he", {cpm: 833, variance: 130 } ],
|
||||
[ "it", {cpm: 950, variance: 140 } ],
|
||||
[ "jw", {cpm: 357, variance: 56 } ],
|
||||
[ "nl", {cpm: 978, variance: 143 } ],
|
||||
[ "pl", {cpm: 916, variance: 126 } ],
|
||||
[ "pt", {cpm: 913, variance: 145 } ],
|
||||
[ "ru", {cpm: 986, variance: 175 } ],
|
||||
[ "sk", {cpm: 885, variance: 145 } ],
|
||||
[ "sv", {cpm: 917, variance: 156 } ],
|
||||
[ "tr", {cpm: 1054, variance: 156 } ],
|
||||
[ "zh", {cpm: 255, variance: 29 } ],
|
||||
]);
|
||||
|
||||
return readingSpeed.get(lang) || readingSpeed.get("en");
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
* 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/. */
|
||||
|
||||
/* eslint-env mozilla/chrome-worker */
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
|
|
@ -40,11 +42,12 @@ var Agent = {
|
|||
*
|
||||
* @param {object} uri URI data for the document.
|
||||
* @param {string} serializedDoc The serialized document.
|
||||
* @param {object} options Options object to pass to Readability.
|
||||
*
|
||||
* @return {object} Article object returned from Readability.
|
||||
*/
|
||||
parseDocument: function (uri, serializedDoc) {
|
||||
parseDocument(uri, serializedDoc, options) {
|
||||
let doc = new JSDOMParser().parse(serializedDoc);
|
||||
return new Readability(uri, doc).parse();
|
||||
return new Readability(uri, doc, options).parse();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,63 +7,56 @@
|
|||
|
||||
<link rel="stylesheet" href="chrome://global/skin/aboutReader.css" type="text/css"/>
|
||||
|
||||
<script type="text/javascript;version=1.8" src="chrome://global/content/reader/aboutReader.js"></script>
|
||||
<script type="text/javascript" src="chrome://global/content/reader/aboutReader.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="container" class="container">
|
||||
<div id="reader-header" class="header">
|
||||
<style scoped>
|
||||
@import url("chrome://global/skin/aboutReaderControls.css");
|
||||
</style>
|
||||
<a id="reader-domain" class="domain"></a>
|
||||
<div class="container">
|
||||
<div class="header reader-header">
|
||||
<a class="domain reader-domain"></a>
|
||||
<div class="domain-border"></div>
|
||||
<h1 id="reader-title"></h1>
|
||||
<div id="reader-credits" class="credits"></div>
|
||||
<h1 class="reader-title"></h1>
|
||||
<div class="credits reader-credits"></div>
|
||||
<div class="meta-data">
|
||||
<div class="reader-estimated-time"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="content">
|
||||
<style scoped>
|
||||
@import url("chrome://global/skin/aboutReaderContent.css");
|
||||
</style>
|
||||
<div id="moz-reader-content"></div>
|
||||
<div class="moz-reader-content"></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<style scoped>
|
||||
@import url("chrome://global/skin/aboutReaderControls.css");
|
||||
</style>
|
||||
<div id="reader-message"></div>
|
||||
<div class="reader-message"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul id="reader-toolbar" class="toolbar">
|
||||
<style scoped>
|
||||
@import url("chrome://global/skin/aboutReaderControls.css");
|
||||
</style>
|
||||
<li><button id="close-button" class="button close-button"/></li>
|
||||
<ul id="style-dropdown" class="dropdown">
|
||||
<ul class="toolbar reader-toolbar">
|
||||
<li><button class="button close-button"/></li>
|
||||
<ul class="dropdown style-dropdown">
|
||||
<li><button class="dropdown-toggle button style-button"/></li>
|
||||
<li id="reader-popup" class="dropdown-popup">
|
||||
<div id="font-type-buttons"></div>
|
||||
<hr></hr>
|
||||
<div id="font-size-buttons">
|
||||
<button id="font-size-minus" class="minus-button"/>
|
||||
<button id="font-size-sample"/>
|
||||
<button id="font-size-plus" class="plus-button"/>
|
||||
<li class="dropdown-popup">
|
||||
<div class="font-type-buttons"></div>
|
||||
<hr>
|
||||
<div class="font-size-buttons">
|
||||
<button class="minus-button"/>
|
||||
<button class="font-size-sample"/>
|
||||
<button class="plus-button"/>
|
||||
</div>
|
||||
<hr></hr>
|
||||
<div id="content-width-buttons">
|
||||
<button id="content-width-minus" class="content-width-minus-button"/>
|
||||
<button id="content-width-plus" class="content-width-plus-button"/>
|
||||
<hr>
|
||||
<div class="content-width-buttons">
|
||||
<button class="content-width-minus-button"/>
|
||||
<button class="content-width-plus-button"/>
|
||||
</div>
|
||||
<hr></hr>
|
||||
<div id="line-height-buttons">
|
||||
<button id="line-height-minus" class="line-height-minus-button"/>
|
||||
<button id="line-height-plus" class="line-height-plus-button"/>
|
||||
<hr>
|
||||
<div class="line-height-buttons">
|
||||
<button class="line-height-minus-button"/>
|
||||
<button class="line-height-plus-button"/>
|
||||
</div>
|
||||
<hr></hr>
|
||||
<div id="color-scheme-buttons"></div>
|
||||
<hr>
|
||||
<div class="color-scheme-buttons"></div>
|
||||
<div class="dropdown-arrow"/>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,6 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
window.addEventListener("DOMContentLoaded", function () {
|
||||
window.addEventListener("DOMContentLoaded", function() {
|
||||
document.dispatchEvent(new CustomEvent("AboutReaderContentLoaded", { bubbles: true }));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -594,12 +594,6 @@ var Printing = {
|
|||
contentElement.setAttribute("class", "content");
|
||||
containerElement.appendChild(contentElement);
|
||||
|
||||
// Create style element for content div and import aboutReaderContent.css
|
||||
let controlContentStyle = content.document.createElement("style");
|
||||
controlContentStyle.setAttribute("scoped", "");
|
||||
controlContentStyle.textContent = "@import url(\"chrome://global/skin/aboutReaderContent.css\");";
|
||||
contentElement.appendChild(controlContentStyle);
|
||||
|
||||
// Jam the article's content into content div
|
||||
let readerContent = content.document.createElement("div");
|
||||
readerContent.setAttribute("id", "moz-reader-content");
|
||||
|
|
|
|||
|
|
@ -13,6 +13,22 @@ aboutReader.colorScheme.dark=Dark
|
|||
aboutReader.colorScheme.sepia=Sepia
|
||||
aboutReader.colorScheme.auto=Auto
|
||||
|
||||
# LOCALIZATION NOTE (aboutReader.estimatedReadTimeValue1): Semi-colon list of plural forms.
|
||||
# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
|
||||
# #1 is the number of minutes it is estimated to take to read the article
|
||||
# example: `3 minutes`
|
||||
aboutReader.estimatedReadTimeValue1=#1 minute;#1 minutes
|
||||
|
||||
#LOCALIZATION NOTE (aboutReader.estimatedReadingTimeRange1): Semi-colon list of plural forms.
|
||||
# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
|
||||
# When there is some uncertainty in how long the article will take to read show a range of
|
||||
# minutes it is expected to take.
|
||||
# #1 is the number of minutes it is estimated to take to read the article for a fast reader
|
||||
# #2 is the number of minutes it is estimated to take to read the article for a slow reader
|
||||
# #2 is the variable used to determine the plural form to use.
|
||||
# example: `5-8 minutes`
|
||||
aboutReader.estimatedReadTimeRange1=#1-#2 minute;#1-#2 minutes
|
||||
|
||||
# LOCALIZATION NOTE (aboutReader.fontType.serif, aboutReader.fontType.sans-serif):
|
||||
# These are the styles of typeface that are options in the reader view controls.
|
||||
aboutReader.fontType.serif=Serif
|
||||
|
|
|
|||
|
|
@ -2,23 +2,13 @@
|
|||
* 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/. */
|
||||
|
||||
/* Avoid adding ID selector rules in this style sheet, since they could
|
||||
* inadvertently match elements in the article content. */
|
||||
|
||||
body {
|
||||
padding: 64px 51px;
|
||||
}
|
||||
|
||||
@media print {
|
||||
#container {
|
||||
max-width: 100% !important;
|
||||
font-size: 14px !important;
|
||||
font-family: Georgia, "Times New Roman", serif !important;
|
||||
}
|
||||
|
||||
body {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
body.loaded {
|
||||
transition: color 0.4s, background-color 0.4s;
|
||||
}
|
||||
|
|
@ -56,80 +46,80 @@ body.serif .remove-button {
|
|||
font-family: Georgia, "Times New Roman", serif;
|
||||
}
|
||||
|
||||
#container {
|
||||
.container {
|
||||
max-width: 30em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#container.font-size1 {
|
||||
.container.font-size1 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#container.font-size2 {
|
||||
.container.font-size2 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#container.font-size3 {
|
||||
.container.font-size3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#container.font-size4 {
|
||||
.container.font-size4 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#container.font-size5 {
|
||||
.container.font-size5 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#container.font-size6 {
|
||||
.container.font-size6 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
#container.font-size7 {
|
||||
.container.font-size7 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
#container.font-size8 {
|
||||
.container.font-size8 {
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
#container.font-size9 {
|
||||
.container.font-size9 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
#container.content-width1 {
|
||||
.container.content-width1 {
|
||||
max-width: 20em;
|
||||
}
|
||||
|
||||
#container.content-width2 {
|
||||
.container.content-width2 {
|
||||
max-width: 25em;
|
||||
}
|
||||
|
||||
#container.content-width3 {
|
||||
.container.content-width3 {
|
||||
max-width: 30em;
|
||||
}
|
||||
|
||||
#container.content-width4 {
|
||||
.container.content-width4 {
|
||||
max-width: 35em;
|
||||
}
|
||||
|
||||
#container.content-width5 {
|
||||
.container.content-width5 {
|
||||
max-width: 40em;
|
||||
}
|
||||
|
||||
#container.content-width6 {
|
||||
.container.content-width6 {
|
||||
max-width: 45em;
|
||||
}
|
||||
|
||||
#container.content-width7 {
|
||||
.container.content-width7 {
|
||||
max-width: 50em;
|
||||
}
|
||||
|
||||
#container.content-width8 {
|
||||
.container.content-width8 {
|
||||
max-width: 55em;
|
||||
}
|
||||
|
||||
#container.content-width9 {
|
||||
.container.content-width9 {
|
||||
max-width: 60em;
|
||||
}
|
||||
|
||||
|
|
@ -176,3 +166,575 @@ body:not(.loaded) .toolbar:-moz-locale-dir(ltr) {
|
|||
body:not(.loaded) .toolbar:-moz-locale-dir(rtl) {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.light-button {
|
||||
color: #333333;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.dark-button {
|
||||
color: #eeeeee;
|
||||
background-color: #333333;
|
||||
}
|
||||
|
||||
.sepia-button {
|
||||
color: #5b4636;
|
||||
background-color: #f4ecd8;
|
||||
}
|
||||
|
||||
.sans-serif-button {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.serif-button {
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
}
|
||||
|
||||
/* Loading/error message */
|
||||
|
||||
.reader-message {
|
||||
margin-top: 40px;
|
||||
display: none;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
|
||||
.header {
|
||||
text-align: start;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.domain {
|
||||
font-size: 0.9em;
|
||||
line-height: 1.48em;
|
||||
padding-bottom: 4px;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid;
|
||||
color: #0095dd;
|
||||
}
|
||||
|
||||
.header > h1 {
|
||||
font-size: 1.6em;
|
||||
line-height: 1.25em;
|
||||
width: 100%;
|
||||
margin: 30px 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.header > .credits {
|
||||
font-size: 0.9em;
|
||||
line-height: 1.48em;
|
||||
margin: 0 0 10px 0;
|
||||
padding: 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.header > .meta-data {
|
||||
font-size: 0.65em;
|
||||
margin: 0 0 15px 0;
|
||||
}
|
||||
|
||||
/*======= Controls toolbar =======*/
|
||||
|
||||
.toolbar {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
background-color: #fbfbfb;
|
||||
-moz-user-select: none;
|
||||
border-right: 1px solid #b5b5b5;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: block;
|
||||
background-size: 24px 24px;
|
||||
background-repeat: no-repeat;
|
||||
color: #333;
|
||||
background-color: #fbfbfb;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.toolbar .button {
|
||||
width: 40px;
|
||||
background-position: center;
|
||||
margin-right: -1px;
|
||||
border-top: 0;
|
||||
border-left: 0;
|
||||
border-right: 1px solid #b5b5b5;
|
||||
border-bottom: 1px solid #c1c1c1;
|
||||
}
|
||||
|
||||
.button[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
text-align: center;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dropdown li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*======= Popup =======*/
|
||||
|
||||
.dropdown-popup {
|
||||
min-width: 300px;
|
||||
text-align: start;
|
||||
position: absolute;
|
||||
left: 48px; /* offset to account for toolbar width */
|
||||
z-index: 1000;
|
||||
background-color: #fbfbfb;
|
||||
visibility: hidden;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #b5b5b5;
|
||||
border-bottom-width: 0;
|
||||
box-shadow: 0 1px 3px #c1c1c1;
|
||||
}
|
||||
|
||||
.keep-open .dropdown-popup {
|
||||
z-index: initial;
|
||||
}
|
||||
|
||||
.dropdown-popup > hr {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.open > .dropdown-popup {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
position: absolute;
|
||||
top: 30px; /* offset arrow from top of popup */
|
||||
left: -16px;
|
||||
width: 16px;
|
||||
height: 24px;
|
||||
background-image: url("chrome://global/skin/reader/RM-Type-Controls-Arrow.svg");
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*======= Font style popup =======*/
|
||||
|
||||
.font-type-buttons,
|
||||
.font-size-buttons,
|
||||
.color-scheme-buttons,
|
||||
.content-width-buttons,
|
||||
.line-height-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.font-type-buttons > button:first-child {
|
||||
border-top-left-radius: 3px;
|
||||
}
|
||||
.font-type-buttons > button:last-child {
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
.color-scheme-buttons > button:first-child {
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
.color-scheme-buttons > button:last-child {
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
|
||||
.font-type-buttons > button,
|
||||
.font-size-buttons > button,
|
||||
.color-scheme-buttons > button,
|
||||
.content-width-buttons > button,
|
||||
.line-height-buttons > button {
|
||||
text-align: center;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.font-type-buttons > button,
|
||||
.font-size-buttons > button,
|
||||
.content-width-buttons > button,
|
||||
.line-height-buttons > button {
|
||||
width: 50%;
|
||||
background-color: transparent;
|
||||
border-left: 1px solid #B5B5B5;
|
||||
border-bottom: 1px solid #B5B5B5;
|
||||
}
|
||||
|
||||
.color-scheme-buttons > button {
|
||||
width: 33.33%;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.color-scheme-buttons > .dark-button {
|
||||
margin-top: -1px;
|
||||
height: 61px;
|
||||
}
|
||||
|
||||
.font-type-buttons > button:first-child,
|
||||
.font-size-buttons > button:first-child,
|
||||
.content-width-buttons > button:first-child,
|
||||
.line-height-buttons > button:first-child {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.font-type-buttons > button {
|
||||
display: inline-block;
|
||||
font-size: 62px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.font-size-buttons > button,
|
||||
.color-scheme-buttons > button,
|
||||
.content-width-buttons > button,
|
||||
.line-height-buttons > button {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.font-type-buttons > button:active:hover,
|
||||
.font-type-buttons > button.selected,
|
||||
.color-scheme-buttons > button:active:hover,
|
||||
.color-scheme-buttons > button.selected {
|
||||
box-shadow: inset 0 -3px 0 0 #fc6420;
|
||||
}
|
||||
|
||||
.font-type-buttons > button:active:hover,
|
||||
.font-type-buttons > button.selected {
|
||||
border-bottom: 1px solid #FC6420;
|
||||
}
|
||||
|
||||
/* Make the serif button content the same size as the sans-serif button content. */
|
||||
.font-type-buttons > button > .description {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
/* Font sizes are different per-platform, so we need custom CSS to line them up. */
|
||||
%ifdef XP_MACOSX
|
||||
.font-type-buttons > .sans-serif-button > .name {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.font-type-buttons > .sans-serif-button > .description {
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.font-type-buttons > .serif-button > .name {
|
||||
font-size: 63px;
|
||||
}
|
||||
%elifdef XP_WIN
|
||||
.font-type-buttons > .sans-serif-button > .name {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.font-type-buttons > .sans-serif-button > .description {
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.font-type-buttons > .serif-button > .name {
|
||||
font-size: 63px;
|
||||
}
|
||||
%else
|
||||
.font-type-buttons > .sans-serif-button > .name {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.font-type-buttons > .sans-serif-button > .description {
|
||||
margin-top: -8px;
|
||||
}
|
||||
|
||||
.font-type-buttons > .serif-button > .name {
|
||||
font-size: 70px;
|
||||
}
|
||||
%endif
|
||||
|
||||
.button:hover,
|
||||
.font-size-buttons > button:hover,
|
||||
.font-type-buttons > button:hover,
|
||||
.content-width-buttons > button:hover,
|
||||
.line-height-buttons > button:hover {
|
||||
background-color: #ebebeb;
|
||||
}
|
||||
|
||||
.dropdown.open,
|
||||
.button:active,
|
||||
.font-size-buttons > button:active,
|
||||
.font-size-buttons > button.selected,
|
||||
.content-width-buttons > button:active,
|
||||
.content-width-buttons > button.selected,
|
||||
.line-height-buttons > button:active,
|
||||
.line-height-buttons > button.selected {
|
||||
background-color: #dadada;
|
||||
}
|
||||
|
||||
/* Only used on Android */
|
||||
.font-size-sample {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.minus-button,
|
||||
.plus-button,
|
||||
.content-width-minus-button,
|
||||
.content-width-plus-button,
|
||||
.line-height-minus-button,
|
||||
.line-height-plus-button {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
background-size: 18px 18px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
/*======= Toolbar icons =======*/
|
||||
|
||||
.close-button {
|
||||
background-image: url("chrome://global/skin/reader/RM-Close-24x24.svg#close");
|
||||
height: 68px;
|
||||
background-position: center 8px;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background-image: url("chrome://global/skin/reader/RM-Close-24x24.svg#close-hover");
|
||||
background-color: #d94141;
|
||||
border-bottom: 1px solid #d94141;
|
||||
border-right: 1px solid #d94141;
|
||||
}
|
||||
|
||||
.close-button:hover:active {
|
||||
background-image: url("chrome://global/skin/reader/RM-Close-24x24.svg#close-hover");
|
||||
background-color: #AE2325;
|
||||
border-bottom: 1px solid #AE2325;
|
||||
border-right: 1px solid #AE2325;
|
||||
}
|
||||
|
||||
.style-button {
|
||||
background-image: url("chrome://global/skin/reader/RM-Type-Controls-24x24.svg");
|
||||
}
|
||||
|
||||
.minus-button {
|
||||
background-image: url("chrome://global/skin/reader/RM-Minus-24x24.svg");
|
||||
}
|
||||
|
||||
.plus-button {
|
||||
background-image: url("chrome://global/skin/reader/RM-Plus-24x24.svg");
|
||||
}
|
||||
|
||||
.content-width-minus-button {
|
||||
background-size: 42px 16px;
|
||||
background-image: url("chrome://global/skin/reader/RM-Content-Width-Minus-42x16.svg");
|
||||
}
|
||||
|
||||
.content-width-plus-button {
|
||||
background-size: 44px 16px;
|
||||
background-image: url("chrome://global/skin/reader/RM-Content-Width-Plus-44x16.svg");
|
||||
}
|
||||
|
||||
.line-height-minus-button {
|
||||
background-size: 34px 14px;
|
||||
background-image: url("chrome://global/skin/reader/RM-Line-Height-Minus-38x14.svg");
|
||||
}
|
||||
|
||||
.line-height-plus-button {
|
||||
background-size: 34px 24px;
|
||||
background-image: url("chrome://global/skin/reader/RM-Line-Height-Plus-38x24.svg");
|
||||
}
|
||||
|
||||
@media print {
|
||||
.toolbar {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/*======= Article content =======*/
|
||||
|
||||
/* Note that any class names from the original article that we want to match on
|
||||
* must be added to CLASSES_TO_PRESERVE in ReaderMode.jsm, so that
|
||||
* Readability.js doesn't strip them out */
|
||||
|
||||
.moz-reader-content {
|
||||
display: none;
|
||||
font-size: 1em;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
.moz-reader-content.line-height1 {
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
.moz-reader-content.line-height2 {
|
||||
line-height: 1.2em;
|
||||
}
|
||||
|
||||
.moz-reader-content.line-height3 {
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
.moz-reader-content.line-height4 {
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
.moz-reader-content.line-height5 {
|
||||
line-height: 1.8em;
|
||||
}
|
||||
|
||||
.moz-reader-content.line-height6 {
|
||||
line-height: 2.0em;
|
||||
}
|
||||
|
||||
.moz-reader-content.line-height7 {
|
||||
line-height: 2.2em;
|
||||
}
|
||||
|
||||
.moz-reader-content.line-height8 {
|
||||
line-height: 2.4em;
|
||||
}
|
||||
|
||||
.moz-reader-content.line-height9 {
|
||||
line-height: 2.6em;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.moz-reader-content p,
|
||||
.moz-reader-content code,
|
||||
.moz-reader-content pre,
|
||||
.moz-reader-content blockquote,
|
||||
.moz-reader-content ul,
|
||||
.moz-reader-content ol,
|
||||
.moz-reader-content li,
|
||||
.moz-reader-content figure,
|
||||
.moz-reader-content .wp-caption {
|
||||
margin: 0 0 10px 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.moz-reader-content h1,
|
||||
.moz-reader-content h2,
|
||||
.moz-reader-content h3 {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.moz-reader-content h1 {
|
||||
font-size: 1.6em;
|
||||
line-height: 1.25em;
|
||||
}
|
||||
|
||||
.moz-reader-content h2 {
|
||||
font-size: 1.2em;
|
||||
line-height: 1.51em;
|
||||
}
|
||||
|
||||
.moz-reader-content h3 {
|
||||
font-size: 1em;
|
||||
line-height: 1.66em;
|
||||
}
|
||||
|
||||
.moz-reader-content a:link {
|
||||
text-decoration: underline;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.moz-reader-content a:link,
|
||||
.moz-reader-content a:link:hover,
|
||||
.moz-reader-content a:link:active {
|
||||
color: #0095dd;
|
||||
}
|
||||
|
||||
.moz-reader-content a:visited {
|
||||
color: #c2e;
|
||||
}
|
||||
|
||||
.moz-reader-content * {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.moz-reader-content p,
|
||||
.moz-reader-content p,
|
||||
.moz-reader-content code,
|
||||
.moz-reader-content pre,
|
||||
.moz-reader-content blockquote,
|
||||
.moz-reader-content ul,
|
||||
.moz-reader-content ol,
|
||||
.moz-reader-content li,
|
||||
.moz-reader-content figure,
|
||||
.moz-reader-content .wp-caption {
|
||||
margin: -10px -10px 20px -10px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.moz-reader-content li {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.moz-reader-content li > ul,
|
||||
.moz-reader-content li > ol {
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
|
||||
.moz-reader-content p > img:only-child,
|
||||
.moz-reader-content p > a:only-child > img:only-child,
|
||||
.moz-reader-content .wp-caption img,
|
||||
.moz-reader-content figure img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.moz-reader-content img[moz-reader-center] {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.moz-reader-content .caption,
|
||||
.moz-reader-content .wp-caption-text
|
||||
.moz-reader-content figcaption {
|
||||
font-size: 0.9em;
|
||||
line-height: 1.48em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.moz-reader-content code,
|
||||
.moz-reader-content pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.moz-reader-content blockquote {
|
||||
padding: 0;
|
||||
padding-inline-start: 16px;
|
||||
}
|
||||
|
||||
.moz-reader-content ul,
|
||||
.moz-reader-content ol {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.moz-reader-content ul {
|
||||
padding-inline-start: 30px;
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
.moz-reader-content ol {
|
||||
padding-inline-start: 30px;
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
/* Hide elements with common "hidden" class names */
|
||||
.moz-reader-content .visually-hidden,
|
||||
.moz-reader-content .visuallyhidden,
|
||||
.moz-reader-content .hidden,
|
||||
.moz-reader-content .invisible,
|
||||
.moz-reader-content .sr-only {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,177 +0,0 @@
|
|||
/* 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/. */
|
||||
|
||||
#moz-reader-content {
|
||||
display: none;
|
||||
font-size: 1em;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
#moz-reader-content.line-height1 {
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
#moz-reader-content.line-height2 {
|
||||
line-height: 1.2em;
|
||||
}
|
||||
|
||||
#moz-reader-content.line-height3 {
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
#moz-reader-content.line-height4 {
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
#moz-reader-content.line-height5 {
|
||||
line-height: 1.8em;
|
||||
}
|
||||
|
||||
#moz-reader-content.line-height6 {
|
||||
line-height: 2.0em;
|
||||
}
|
||||
|
||||
#moz-reader-content.line-height7 {
|
||||
line-height: 2.2em;
|
||||
}
|
||||
|
||||
#moz-reader-content.line-height8 {
|
||||
line-height: 2.4em;
|
||||
}
|
||||
|
||||
#moz-reader-content.line-height9 {
|
||||
line-height: 2.6em;
|
||||
}
|
||||
|
||||
@media print {
|
||||
p,
|
||||
code,
|
||||
pre,
|
||||
blockquote,
|
||||
ul,
|
||||
ol,
|
||||
li,
|
||||
figure,
|
||||
.wp-caption {
|
||||
margin: 0 0 10px 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.6em;
|
||||
line-height: 1.25em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2em;
|
||||
line-height: 1.51em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1em;
|
||||
line-height: 1.66em;
|
||||
}
|
||||
|
||||
a:link {
|
||||
text-decoration: underline;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a:link,
|
||||
a:link:hover,
|
||||
a:link:active {
|
||||
color: #0095dd;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #c2e;
|
||||
}
|
||||
|
||||
* {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
p,
|
||||
code,
|
||||
pre,
|
||||
blockquote,
|
||||
ul,
|
||||
ol,
|
||||
li,
|
||||
figure,
|
||||
.wp-caption {
|
||||
margin: -10px -10px 20px -10px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
li > ul,
|
||||
li > ol {
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
|
||||
p > img:only-child,
|
||||
p > a:only-child > img:only-child,
|
||||
.wp-caption img,
|
||||
figure img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
img[moz-reader-center] {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.caption,
|
||||
.wp-caption-text,
|
||||
figcaption {
|
||||
font-size: 0.9em;
|
||||
line-height: 1.48em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
code,
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding: 0;
|
||||
padding-inline-start: 16px;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-inline-start: 30px;
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
ol {
|
||||
padding-inline-start: 30px;
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
/* Hide elements with common "hidden" class names */
|
||||
.visually-hidden,
|
||||
.visuallyhidden,
|
||||
.hidden,
|
||||
.invisible,
|
||||
.sr-only {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -1,388 +0,0 @@
|
|||
/* 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/. */
|
||||
|
||||
.light-button {
|
||||
color: #333333;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.dark-button {
|
||||
color: #eeeeee;
|
||||
background-color: #333333;
|
||||
}
|
||||
|
||||
.sepia-button {
|
||||
color: #5b4636;
|
||||
background-color: #f4ecd8;
|
||||
}
|
||||
|
||||
.sans-serif-button {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.serif-button {
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
}
|
||||
|
||||
/* Loading/error message */
|
||||
|
||||
#reader-message {
|
||||
margin-top: 40px;
|
||||
display: none;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
|
||||
.header {
|
||||
text-align: start;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.domain {
|
||||
font-size: 0.9em;
|
||||
line-height: 1.48em;
|
||||
padding-bottom: 4px;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid;
|
||||
color: #0095dd;
|
||||
}
|
||||
|
||||
.header > h1 {
|
||||
font-size: 1.6em;
|
||||
line-height: 1.25em;
|
||||
width: 100%;
|
||||
margin: 30px 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.header > .credits {
|
||||
font-size: 0.9em;
|
||||
line-height: 1.48em;
|
||||
margin: 0 0 30px 0;
|
||||
padding: 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/*======= Controls toolbar =======*/
|
||||
|
||||
.toolbar {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
background-color: #fbfbfb;
|
||||
-moz-user-select: none;
|
||||
border-right: 1px solid #b5b5b5;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: block;
|
||||
background-size: 24px 24px;
|
||||
background-repeat: no-repeat;
|
||||
color: #333;
|
||||
background-color: #fbfbfb;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.toolbar .button {
|
||||
width: 40px;
|
||||
background-position: center;
|
||||
margin-right: -1px;
|
||||
border-top: 0;
|
||||
border-left: 0;
|
||||
border-right: 1px solid #b5b5b5;
|
||||
border-bottom: 1px solid #c1c1c1;
|
||||
}
|
||||
|
||||
.button[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
text-align: center;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dropdown li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*======= Popup =======*/
|
||||
|
||||
.dropdown-popup {
|
||||
min-width: 300px;
|
||||
text-align: start;
|
||||
position: absolute;
|
||||
left: 48px; /* offset to account for toolbar width */
|
||||
z-index: 1000;
|
||||
background-color: #fbfbfb;
|
||||
visibility: hidden;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #b5b5b5;
|
||||
border-bottom-width: 0;
|
||||
box-shadow: 0 1px 12px #666;
|
||||
}
|
||||
|
||||
.keep-open .dropdown-popup {
|
||||
z-index: initial;
|
||||
}
|
||||
|
||||
.dropdown-popup > hr {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.open > .dropdown-popup {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
position: absolute;
|
||||
top: 30px; /* offset arrow from top of popup */
|
||||
left: -16px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-image: url("chrome://global/skin/reader/RM-Type-Controls-Arrow.svg");
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*======= Font style popup =======*/
|
||||
|
||||
#font-type-buttons,
|
||||
#font-size-buttons,
|
||||
#color-scheme-buttons,
|
||||
#content-width-buttons,
|
||||
#line-height-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#font-type-buttons > button:first-child {
|
||||
border-top-left-radius: 3px;
|
||||
}
|
||||
#font-type-buttons > button:last-child {
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
#color-scheme-buttons > button:first-child {
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
#color-scheme-buttons > button:last-child {
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
|
||||
#font-type-buttons > button,
|
||||
#font-size-buttons > button,
|
||||
#color-scheme-buttons > button,
|
||||
#content-width-buttons > button,
|
||||
#line-height-buttons > button {
|
||||
text-align: center;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
#font-type-buttons > button,
|
||||
#font-size-buttons > button,
|
||||
#content-width-buttons > button,
|
||||
#line-height-buttons > button {
|
||||
width: 50%;
|
||||
background-color: transparent;
|
||||
border-left: 1px solid #B5B5B5;
|
||||
border-bottom: 1px solid #B5B5B5;
|
||||
}
|
||||
|
||||
#color-scheme-buttons > button {
|
||||
width: 33.33%;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#color-scheme-buttons > .dark-button {
|
||||
margin-top: -1px;
|
||||
height: 61px;
|
||||
}
|
||||
|
||||
#font-type-buttons > button:first-child,
|
||||
#font-size-buttons > button:first-child,
|
||||
#content-width-buttons > button:first-child,
|
||||
#line-height-buttons > button:first-child {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
#font-type-buttons > button {
|
||||
display: inline-block;
|
||||
font-size: 62px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
#font-size-buttons > button,
|
||||
#color-scheme-buttons > button,
|
||||
#content-width-buttons > button,
|
||||
#line-height-buttons > button {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
#font-type-buttons > button:active:hover,
|
||||
#font-type-buttons > button.selected,
|
||||
#color-scheme-buttons > button:active:hover,
|
||||
#color-scheme-buttons > button.selected {
|
||||
box-shadow: inset 0 -3px 0 0 #fc6420;
|
||||
}
|
||||
|
||||
#font-type-buttons > button:active:hover,
|
||||
#font-type-buttons > button.selected {
|
||||
border-bottom: 1px solid #FC6420;
|
||||
}
|
||||
|
||||
/* Make the serif button content the same size as the sans-serif button content. */
|
||||
#font-type-buttons > button > .description {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
/* Font sizes are different per-platform, so we need custom CSS to line them up. */
|
||||
%ifdef XP_MACOSX
|
||||
#font-type-buttons > .sans-serif-button > .name {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#font-type-buttons > .sans-serif-button > .description {
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
#font-type-buttons > .serif-button > .name {
|
||||
font-size: 63px;
|
||||
}
|
||||
%elifdef XP_WIN
|
||||
#font-type-buttons > .sans-serif-button > .name {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
#font-type-buttons > .sans-serif-button > .description {
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
#font-type-buttons > .serif-button > .name {
|
||||
font-size: 63px;
|
||||
}
|
||||
%else
|
||||
#font-type-buttons > .sans-serif-button > .name {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
#font-type-buttons > .sans-serif-button > .description {
|
||||
margin-top: -8px;
|
||||
}
|
||||
|
||||
#font-type-buttons > .serif-button > .name {
|
||||
font-size: 70px;
|
||||
}
|
||||
%endif
|
||||
|
||||
.button:hover,
|
||||
#font-size-buttons > button:hover,
|
||||
#font-type-buttons > button:hover,
|
||||
#content-width-buttons > button:hover,
|
||||
#line-height-buttons > button:hover {
|
||||
background-color: #ebebeb;
|
||||
}
|
||||
|
||||
.dropdown.open,
|
||||
.button:active,
|
||||
#font-size-buttons > button:active,
|
||||
#font-size-buttons > button.selected,
|
||||
#content-width-buttons > button:active,
|
||||
#content-width-buttons > button.selected,
|
||||
#line-height-buttons > button:active,
|
||||
#line-height-buttons > button.selected {
|
||||
background-color: #dadada;
|
||||
}
|
||||
|
||||
/* Only used on Android */
|
||||
#font-size-sample {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.minus-button,
|
||||
.plus-button,
|
||||
.content-width-minus-button,
|
||||
.content-width-plus-button,
|
||||
.line-height-minus-button,
|
||||
.line-height-plus-button {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
background-size: 18px 18px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
/*======= Toolbar icons =======*/
|
||||
|
||||
.close-button {
|
||||
background-image: url("chrome://global/skin/reader/RM-Close-24x24.svg#close");
|
||||
height: 68px;
|
||||
background-position: center 8px;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background-image: url("chrome://global/skin/reader/RM-Close-24x24.svg#close-hover");
|
||||
background-color: #d94141;
|
||||
border-bottom: 1px solid #d94141;
|
||||
border-right: 1px solid #d94141;
|
||||
}
|
||||
|
||||
.close-button:hover:active {
|
||||
background-image: url("chrome://global/skin/reader/RM-Close-24x24.svg#close-hover");
|
||||
background-color: #AE2325;
|
||||
border-bottom: 1px solid #AE2325;
|
||||
border-right: 1px solid #AE2325;
|
||||
}
|
||||
|
||||
.style-button {
|
||||
background-image: url("chrome://global/skin/reader/RM-Type-Controls-24x24.svg");
|
||||
}
|
||||
|
||||
.minus-button {
|
||||
background-image: url("chrome://global/skin/reader/RM-Minus-24x24.svg");
|
||||
}
|
||||
|
||||
.plus-button {
|
||||
background-image: url("chrome://global/skin/reader/RM-Plus-24x24.svg");
|
||||
}
|
||||
|
||||
.content-width-minus-button {
|
||||
background-size: 42px 16px;
|
||||
background-image: url("chrome://global/skin/reader/RM-Content-Width-Minus-42x16.svg");
|
||||
}
|
||||
|
||||
.content-width-plus-button {
|
||||
background-size: 44px 16px;
|
||||
background-image: url("chrome://global/skin/reader/RM-Content-Width-Plus-44x16.svg");
|
||||
}
|
||||
|
||||
.line-height-minus-button {
|
||||
background-size: 34px 14px;
|
||||
background-image: url("chrome://global/skin/reader/RM-Line-Height-Minus-38x14.svg");
|
||||
}
|
||||
|
||||
.line-height-plus-button {
|
||||
background-size: 34px 24px;
|
||||
background-image: url("chrome://global/skin/reader/RM-Line-Height-Plus-38x24.svg");
|
||||
}
|
||||
|
||||
@media print {
|
||||
.toolbar {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,9 +15,7 @@ toolkit.jar:
|
|||
skin/classic/global/aboutCache.css (../../shared/aboutCache.css)
|
||||
skin/classic/global/aboutCacheEntry.css (../../shared/aboutCacheEntry.css)
|
||||
skin/classic/global/aboutMemory.css (../../shared/aboutMemory.css)
|
||||
skin/classic/global/aboutReader.css (../../shared/aboutReader.css)
|
||||
skin/classic/global/aboutReaderContent.css (../../shared/aboutReaderContent.css)
|
||||
* skin/classic/global/aboutReaderControls.css (../../shared/aboutReaderControls.css)
|
||||
* skin/classic/global/aboutReader.css (../../shared/aboutReader.css)
|
||||
skin/classic/global/aboutSupport.css (../../shared/aboutSupport.css)
|
||||
skin/classic/global/appPicker.css (../../shared/appPicker.css)
|
||||
skin/classic/global/config.css (../../shared/config.css)
|
||||
|
|
@ -34,7 +32,6 @@ toolkit.jar:
|
|||
skin/classic/global/icons/blocked.svg (../../shared/incontent-icons/blocked.svg)
|
||||
skin/classic/global/alerts/alert-common.css (../../shared/alert-common.css)
|
||||
skin/classic/global/narrate.css (../../shared/narrate.css)
|
||||
skin/classic/global/narrateControls.css (../../shared/narrateControls.css)
|
||||
skin/classic/global/narrate/arrow.svg (../../shared/narrate/arrow.svg)
|
||||
skin/classic/global/narrate/back.svg (../../shared/narrate/back.svg)
|
||||
skin/classic/global/narrate/fast.svg (../../shared/narrate/fast.svg)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
/* Avoid adding ID selector rules in this style sheet, since they could
|
||||
* inadvertently match elements in the article content. */
|
||||
|
||||
.narrating {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
|
@ -22,7 +25,7 @@ body.dark .narrating {
|
|||
z-index: -1;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 7px;
|
||||
transition: left 0.1s ease;
|
||||
transition: left 0.1s ease, width 0.1s ease;
|
||||
}
|
||||
|
||||
.narrating > .narrate-word-highlight {
|
||||
|
|
@ -44,3 +47,193 @@ body.sepia .narrate-word-highlight {
|
|||
body.dark .narrate-word-highlight {
|
||||
border-bottom-color: #6f6f6f;
|
||||
}
|
||||
|
||||
.narrate-dropdown {
|
||||
--border-color: #e5e5e5;
|
||||
}
|
||||
|
||||
.narrate-toggle > svg {
|
||||
display: block;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.narrate-dropdown > .dropdown-popup button {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.narrate-dropdown > .dropdown-popup button:hover:not(:disabled) {
|
||||
background-color: #eaeaea;
|
||||
}
|
||||
|
||||
.narrate-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 40px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.narrate-row:not(:first-child) {
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* Control buttons */
|
||||
|
||||
.narrate-control > button {
|
||||
background-size: 24px 24px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
height: 64px;
|
||||
width: 100px;
|
||||
border: none;
|
||||
color: #666;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.narrate-control > button:not(:first-child) {
|
||||
border-left: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.narrate-skip-previous {
|
||||
border-top-left-radius: 3px;
|
||||
background-image: url("chrome://global/skin/narrate/back.svg#enabled");
|
||||
-moz-context-properties: fill;
|
||||
fill: rgb(128 128 128);
|
||||
}
|
||||
|
||||
.narrate-skip-next {
|
||||
border-top-right-radius: 3px;
|
||||
background-image: url("chrome://global/skin/narrate/forward.svg#enabled");
|
||||
-moz-context-properties: fill;
|
||||
fill: rgb(128 128 128);
|
||||
}
|
||||
|
||||
.narrate-skip-previous:disabled {
|
||||
background-image: url("chrome://global/skin/narrate/back.svg#disabled");
|
||||
}
|
||||
|
||||
.narrate-skip-next:disabled {
|
||||
background-image: url("chrome://global/skin/narrate/forward.svg#disabled");
|
||||
}
|
||||
|
||||
.narrate-start-stop {
|
||||
background-image: url("chrome://global/skin/narrate/start.svg");
|
||||
}
|
||||
|
||||
.narrate-dropdown.speaking .narrate-start-stop {
|
||||
background-image: url("chrome://global/skin/narrate/stop.svg");
|
||||
}
|
||||
|
||||
/* Rate control */
|
||||
|
||||
.narrate-rate::before, .narrate-rate::after {
|
||||
content: '';
|
||||
width: 48px;
|
||||
height: 40px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 24px auto;
|
||||
}
|
||||
|
||||
.narrate-rate::before {
|
||||
background-image: url("chrome://global/skin/narrate/slow.svg");
|
||||
}
|
||||
|
||||
.narrate-rate::after {
|
||||
background-image: url("chrome://global/skin/narrate/fast.svg");
|
||||
}
|
||||
|
||||
.narrate-rate-input {
|
||||
margin: 0 1px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.narrate-rate-input::-moz-range-track {
|
||||
background-color: #979797;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.narrate-rate-input::-moz-range-progress {
|
||||
background-color: #2EA3FF;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.narrate-rate-input::-moz-range-thumb {
|
||||
background-color: #808080;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-radius: 8px;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.narrate-rate-input:active::-moz-range-thumb {
|
||||
background-color: #2EA3FF;
|
||||
}
|
||||
|
||||
/* Voice selection */
|
||||
|
||||
.voiceselect {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.voiceselect > button.select-toggle,
|
||||
.voiceselect > .options > button.option {
|
||||
-moz-appearance: none;
|
||||
border: none;
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.voiceselect.open > button.select-toggle {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.voiceselect > button.select-toggle::after {
|
||||
content: '';
|
||||
background-image: url("chrome://global/skin/narrate/arrow.svg");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 12px 12px;
|
||||
display: inline-block;
|
||||
width: 1.5em;
|
||||
height: 1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.voiceselect > .options > button.option:not(:first-child) {
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.voiceselect > .options > button.option {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.voiceselect > .options:not(.hovering) > button.option:focus {
|
||||
background-color: #eaeaea;
|
||||
}
|
||||
|
||||
.voiceselect > .options:not(.hovering) > button.option:hover:not(:focus) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.voiceselect > .options > button.option::-moz-focus-inner {
|
||||
outline: none;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.voiceselect > .options {
|
||||
display: none;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.voiceselect.open > .options {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.current-voice {
|
||||
color: #7f7f7f;
|
||||
}
|
||||
|
||||
.voiceselect:not(.open) > button,
|
||||
.voiceselect .option:last-child {
|
||||
border-radius: 0 0 3px 3px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,185 +0,0 @@
|
|||
:scope {
|
||||
--border-color: #e5e5e5;
|
||||
}
|
||||
|
||||
#narrate-toggle > svg {
|
||||
display: block;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.dropdown-popup button {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.dropdown-popup button:hover:not(:disabled) {
|
||||
background-color: #eaeaea;
|
||||
}
|
||||
|
||||
.narrate-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 40px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.narrate-row:not(:first-child) {
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* Control buttons */
|
||||
|
||||
#narrate-control > button {
|
||||
background-size: 24px 24px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
height: 64px;
|
||||
width: 100px;
|
||||
border: none;
|
||||
color: #666;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#narrate-control > button:not(:first-child) {
|
||||
border-left: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
#narrate-skip-previous {
|
||||
border-top-left-radius: 3px;
|
||||
background-image: url("chrome://global/skin/narrate/back.svg#enabled");
|
||||
}
|
||||
|
||||
#narrate-skip-next {
|
||||
border-top-right-radius: 3px;
|
||||
background-image: url("chrome://global/skin/narrate/forward.svg#enabled");
|
||||
}
|
||||
|
||||
#narrate-skip-previous:disabled {
|
||||
background-image: url("chrome://global/skin/narrate/back.svg#disabled");
|
||||
}
|
||||
|
||||
#narrate-skip-next:disabled {
|
||||
background-image: url("chrome://global/skin/narrate/forward.svg#disabled");
|
||||
}
|
||||
|
||||
#narrate-start-stop {
|
||||
background-image: url("chrome://global/skin/narrate/start.svg");
|
||||
}
|
||||
|
||||
#narrate-dropdown.speaking #narrate-start-stop {
|
||||
background-image: url("chrome://global/skin/narrate/stop.svg");
|
||||
}
|
||||
|
||||
/* Rate control */
|
||||
|
||||
#narrate-rate::before, #narrate-rate::after {
|
||||
content: '';
|
||||
width: 48px;
|
||||
height: 40px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 24px auto;
|
||||
}
|
||||
|
||||
#narrate-rate::before {
|
||||
background-image: url("chrome://global/skin/narrate/slow.svg");
|
||||
}
|
||||
|
||||
#narrate-rate::after {
|
||||
background-image: url("chrome://global/skin/narrate/fast.svg");
|
||||
}
|
||||
|
||||
#narrate-rate-input {
|
||||
margin: 0 1px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#narrate-rate-input::-moz-range-track {
|
||||
background-color: #979797;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
#narrate-rate-input::-moz-range-progress {
|
||||
background-color: #2EA3FF;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
#narrate-rate-input::-moz-range-thumb {
|
||||
background-color: #808080;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-radius: 8px;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
#narrate-rate-input:active::-moz-range-thumb {
|
||||
background-color: #2EA3FF;
|
||||
}
|
||||
|
||||
/* Voice selection */
|
||||
|
||||
.voiceselect {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.voiceselect > button.select-toggle,
|
||||
.voiceselect > .options > button.option {
|
||||
-moz-appearance: none;
|
||||
border: none;
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.voiceselect.open > button.select-toggle {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.voiceselect > button.select-toggle::after {
|
||||
content: '';
|
||||
background-image: url("chrome://global/skin/narrate/arrow.svg");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 12px 12px;
|
||||
display: inline-block;
|
||||
width: 1.5em;
|
||||
height: 1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.voiceselect > .options > button.option:not(:first-child) {
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.voiceselect > .options > button.option {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.voiceselect > .options:not(.hovering) > button.option:focus {
|
||||
background-color: #eaeaea;
|
||||
}
|
||||
|
||||
.voiceselect > .options:not(.hovering) > button.option:hover:not(:focus) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.voiceselect > .options > button.option::-moz-focus-inner {
|
||||
outline: none;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.voiceselect > .options {
|
||||
display: none;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.voiceselect.open > .options {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.current-voice {
|
||||
color: #7f7f7f;
|
||||
}
|
||||
|
||||
.voiceselect:not(.open) > button,
|
||||
.option:last-child {
|
||||
border-radius: 0 0 3px 3px;
|
||||
}
|
||||
|
|
@ -1,13 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- 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/. -->
|
||||
|
||||
<svg version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="42"
|
||||
height="16"
|
||||
viewBox="0 0 42 16"
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 905 B After Width: | Height: | Size: 739 B |
|
|
@ -1,13 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- 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/. -->
|
||||
|
||||
<svg version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="44"
|
||||
height="16"
|
||||
viewBox="0 0 44 16"
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 904 B After Width: | Height: | Size: 738 B |
|
|
@ -1,13 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- 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/. -->
|
||||
|
||||
<svg version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="38"
|
||||
height="14"
|
||||
viewBox="0 0 38 14"
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 680 B After Width: | Height: | Size: 514 B |
|
|
@ -1,13 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- 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/. -->
|
||||
|
||||
<svg version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="38"
|
||||
height="24"
|
||||
viewBox="0 0 38 24"
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 681 B After Width: | Height: | Size: 515 B |
|
|
@ -2,7 +2,7 @@
|
|||
<!-- 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/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<polygon opacity="0.15" points="16.583,0.015 16.569,0 4.583,12 16.569,24 16.583,23.985"/>
|
||||
<polygon fill="#fbfbfb" points="16.575,1.021 16.561,1.008 5.583,12 16.577,23.008 16.591,22.994 "/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 24">
|
||||
<polygon points="16.58 0.01 16.57 0 4.58 12 16.57 24 16.58 23.98 16.58 0.01" fill="#b5b5b5"/>
|
||||
<polyline points="16.63 1.51 6.08 12.01 16.63 22.5" fill="#fbfbfb"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 513 B After Width: | Height: | Size: 486 B |