Initial updates for Reader View.
|
|
@ -285,7 +285,7 @@
|
||||||
<key id="key_fullScreen_old" key="&fullScreenCmd.macCommandKey;" command="View:FullScreen" modifiers="accel,shift"/>
|
<key id="key_fullScreen_old" key="&fullScreenCmd.macCommandKey;" command="View:FullScreen" modifiers="accel,shift"/>
|
||||||
<key keycode="VK_F11" command="View:FullScreen"/>
|
<key keycode="VK_F11" command="View:FullScreen"/>
|
||||||
#endif
|
#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:Reload" modifiers="accel" id="key_reload"/>
|
||||||
<key key="&reloadCmd.commandkey;" command="Browser:ReloadSkipCache" modifiers="accel,shift"/>
|
<key key="&reloadCmd.commandkey;" command="Browser:ReloadSkipCache" modifiers="accel,shift"/>
|
||||||
<key id="key_viewSource" key="&pageSourceCmd.commandkey;" command="View:PageSource" modifiers="accel"/>
|
<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/XPCOMUtils.jsm");
|
||||||
Cu.import("resource://gre/modules/Services.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, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.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");
|
const gStringBundle = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
|
||||||
|
|
||||||
var ReaderParent = {
|
var ReaderParent = {
|
||||||
_readerModeInfoPanelOpen: false,
|
|
||||||
|
|
||||||
MESSAGES: [
|
MESSAGES: [
|
||||||
"Reader:ArticleGet",
|
"Reader:ArticleGet",
|
||||||
"Reader:FaviconRequest",
|
"Reader:FaviconRequest",
|
||||||
"Reader:UpdateReaderButton",
|
"Reader:UpdateReaderButton",
|
||||||
],
|
],
|
||||||
|
|
||||||
init: function() {
|
init() {
|
||||||
let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
|
let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
|
||||||
for (let msg of this.MESSAGES) {
|
for (let msg of this.MESSAGES) {
|
||||||
mm.addMessageListener(msg, this);
|
mm.addMessageListener(msg, this);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
receiveMessage: function(message) {
|
receiveMessage(message) {
|
||||||
switch (message.name) {
|
switch (message.name) {
|
||||||
case "Reader:ArticleGet":
|
case "Reader:ArticleGet":
|
||||||
this._getArticle(message.data.url, message.target).then((article) => {
|
this._getArticle(message.data.url, message.target).then((article) => {
|
||||||
|
|
@ -60,7 +56,7 @@ var ReaderParent = {
|
||||||
message.target.messageManager.sendAsyncMessage("Reader:FaviconReturn", {
|
message.target.messageManager.sendAsyncMessage("Reader:FaviconReturn", {
|
||||||
url: message.data.url,
|
url: message.data.url,
|
||||||
faviconUrl: favicon.path.replace(/^favicon:/, "")
|
faviconUrl: favicon.path.replace(/^favicon:/, "")
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
function onRejection(reason) {
|
function onRejection(reason) {
|
||||||
Cu.reportError("Error requesting favicon URL for about:reader content: " + 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;
|
let win = browser.ownerGlobal;
|
||||||
if (browser != win.gBrowser.selectedBrowser) {
|
if (browser != win.gBrowser.selectedBrowser) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -88,7 +84,11 @@ var ReaderParent = {
|
||||||
|
|
||||||
let button = win.document.getElementById("reader-mode-button");
|
let button = win.document.getElementById("reader-mode-button");
|
||||||
let command = win.document.getElementById("View:ReaderView");
|
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")) {
|
if (browser.currentURI.spec.startsWith("about:reader")) {
|
||||||
button.setAttribute("readeractive", true);
|
button.setAttribute("readeractive", true);
|
||||||
button.hidden = false;
|
button.hidden = false;
|
||||||
|
|
@ -98,6 +98,7 @@ var ReaderParent = {
|
||||||
command.setAttribute("hidden", false);
|
command.setAttribute("hidden", false);
|
||||||
command.setAttribute("accesskey", gStringBundle.GetStringFromName("readerView.close.accesskey"));
|
command.setAttribute("accesskey", gStringBundle.GetStringFromName("readerView.close.accesskey"));
|
||||||
key.setAttribute("disabled", false);
|
key.setAttribute("disabled", false);
|
||||||
|
browser.setAttribute("aria-reader", "active");
|
||||||
} else {
|
} else {
|
||||||
button.removeAttribute("readeractive");
|
button.removeAttribute("readeractive");
|
||||||
button.hidden = !browser.isArticle;
|
button.hidden = !browser.isArticle;
|
||||||
|
|
@ -107,24 +108,15 @@ var ReaderParent = {
|
||||||
command.setAttribute("hidden", !browser.isArticle);
|
command.setAttribute("hidden", !browser.isArticle);
|
||||||
command.setAttribute("accesskey", gStringBundle.GetStringFromName("readerView.enter.accesskey"));
|
command.setAttribute("accesskey", gStringBundle.GetStringFromName("readerView.enter.accesskey"));
|
||||||
key.setAttribute("disabled", !browser.isArticle);
|
key.setAttribute("disabled", !browser.isArticle);
|
||||||
}
|
if (browser.isArticle) {
|
||||||
|
browser.setAttribute("aria-reader", "available");
|
||||||
let currentUriHost = browser.currentURI && browser.currentURI.asciiHost;
|
} else {
|
||||||
if (browser.isArticle &&
|
browser.removeAttribute("aria-reader");
|
||||||
!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);
|
|
||||||
}
|
}
|
||||||
this._readerModeInfoPanelOpen = false;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
forceShowReaderIcon: function(browser) {
|
forceShowReaderIcon(browser) {
|
||||||
browser.isArticle = true;
|
browser.isArticle = true;
|
||||||
this.updateReaderButton(browser);
|
this.updateReaderButton(browser);
|
||||||
},
|
},
|
||||||
|
|
@ -136,35 +128,12 @@ var ReaderParent = {
|
||||||
this.toggleReaderMode(event);
|
this.toggleReaderMode(event);
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleReaderMode: function(event) {
|
toggleReaderMode(event) {
|
||||||
let win = event.target.ownerGlobal;
|
let win = event.target.ownerGlobal;
|
||||||
let browser = win.gBrowser.selectedBrowser;
|
let browser = win.gBrowser.selectedBrowser;
|
||||||
browser.messageManager.sendAsyncMessage("Reader:ToggleReaderMode");
|
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.
|
* Gets an article for a given URL. This method will download and parse a document.
|
||||||
*
|
*
|
||||||
|
|
@ -173,8 +142,8 @@ var ReaderParent = {
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
* @resolves JS object representing the article, or null if no article is found.
|
* @resolves JS object representing the article, or null if no article is found.
|
||||||
*/
|
*/
|
||||||
_getArticle: Task.async(function* (url, browser) {
|
async _getArticle(url, browser) {
|
||||||
return yield ReaderMode.downloadAndParseDocument(url).catch(e => {
|
return await ReaderMode.downloadAndParseDocument(url).catch(e => {
|
||||||
if (e && e.newURL) {
|
if (e && e.newURL) {
|
||||||
// Pass up the error so we can navigate the browser in question to the new URL:
|
// Pass up the error so we can navigate the browser in question to the new URL:
|
||||||
throw e;
|
throw e;
|
||||||
|
|
@ -182,5 +151,5 @@ var ReaderParent = {
|
||||||
Cu.reportError("Error downloading and parsing document: " + e);
|
Cu.reportError("Error downloading and parsing document: " + e);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
})
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,6 @@ chrome.jar:
|
||||||
skin/aboutMemory.css (aboutMemory.css)
|
skin/aboutMemory.css (aboutMemory.css)
|
||||||
skin/aboutPrivateBrowsing.css (aboutPrivateBrowsing.css)
|
skin/aboutPrivateBrowsing.css (aboutPrivateBrowsing.css)
|
||||||
skin/aboutReader.css (aboutReader.css)
|
skin/aboutReader.css (aboutReader.css)
|
||||||
skin/aboutReaderContent.css (aboutReaderContent.css)
|
|
||||||
skin/aboutReaderControls.css (aboutReaderControls.css)
|
|
||||||
skin/aboutSupport.css (aboutSupport.css)
|
skin/aboutSupport.css (aboutSupport.css)
|
||||||
skin/content.css (content.css)
|
skin/content.css (content.css)
|
||||||
skin/scrollbar.css (scrollbar-apz.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/about.css chrome://browser/skin/about.css
|
||||||
% override chrome://global/skin/aboutMemory.css chrome://browser/skin/aboutMemory.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/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/aboutSupport.css chrome://browser/skin/aboutSupport.css
|
||||||
% override chrome://global/skin/media/videocontrols.css chrome://browser/skin/touchcontrols.css
|
% override chrome://global/skin/media/videocontrols.css chrome://browser/skin/touchcontrols.css
|
||||||
% override chrome://global/skin/netError.css chrome://browser/skin/netError.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");
|
var gStrings = Services.strings.createBundle("chrome://global/locale/narrate.properties");
|
||||||
|
|
||||||
function NarrateControls(mm, win) {
|
function NarrateControls(mm, win, languagePromise) {
|
||||||
this._mm = mm;
|
this._mm = mm;
|
||||||
this._winRef = Cu.getWeakReference(win);
|
this._winRef = Cu.getWeakReference(win);
|
||||||
|
this._languagePromise = languagePromise;
|
||||||
|
|
||||||
win.addEventListener("unload", this);
|
win.addEventListener("unload", this);
|
||||||
|
|
||||||
|
|
@ -37,16 +38,12 @@ function NarrateControls(mm, win) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let dropdown = win.document.createElement("ul");
|
let dropdown = win.document.createElement("ul");
|
||||||
dropdown.className = "dropdown";
|
dropdown.className = "dropdown narrate-dropdown";
|
||||||
dropdown.id = "narrate-dropdown";
|
|
||||||
// We need inline svg here for the animation to work (bug 908634 & 1190881).
|
// 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 =
|
dropdown.innerHTML =
|
||||||
localize`<style scoped>
|
localize`<li>
|
||||||
@import url("chrome://global/skin/narrateControls.css");
|
<button class="dropdown-toggle button narrate-toggle"
|
||||||
</style>
|
|
||||||
<li>
|
|
||||||
<button class="dropdown-toggle button" id="narrate-toggle"
|
|
||||||
title="${"narrate"}" hidden>
|
title="${"narrate"}" hidden>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg"
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
|
@ -60,11 +57,11 @@ function NarrateControls(mm, win) {
|
||||||
100% { transform: scaleY(1); }
|
100% { transform: scaleY(1); }
|
||||||
}
|
}
|
||||||
|
|
||||||
#waveform > rect {
|
.waveform > rect {
|
||||||
fill: #808080;
|
fill: #808080;
|
||||||
}
|
}
|
||||||
|
|
||||||
.speaking #waveform > rect {
|
.speaking .waveform > rect {
|
||||||
fill: #58bf43;
|
fill: #58bf43;
|
||||||
transform-box: fill-box;
|
transform-box: fill-box;
|
||||||
transform-origin: 50% 50%;
|
transform-origin: 50% 50%;
|
||||||
|
|
@ -74,15 +71,15 @@ function NarrateControls(mm, win) {
|
||||||
animation-timing-function: linear;
|
animation-timing-function: linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
#waveform > rect:nth-child(2) { animation-delay: 250ms; }
|
.waveform > rect:nth-child(2) { animation-delay: 250ms; }
|
||||||
#waveform > rect:nth-child(3) { animation-delay: 500ms; }
|
.waveform > rect:nth-child(3) { animation-delay: 500ms; }
|
||||||
#waveform > rect:nth-child(4) { animation-delay: 750ms; }
|
.waveform > rect:nth-child(4) { animation-delay: 750ms; }
|
||||||
#waveform > rect:nth-child(5) { animation-delay: 1000ms; }
|
.waveform > rect:nth-child(5) { animation-delay: 1000ms; }
|
||||||
#waveform > rect:nth-child(6) { animation-delay: 1250ms; }
|
.waveform > rect:nth-child(6) { animation-delay: 1250ms; }
|
||||||
#waveform > rect:nth-child(7) { animation-delay: 1500ms; }
|
.waveform > rect:nth-child(7) { animation-delay: 1500ms; }
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<g id="waveform">
|
<g class="waveform">
|
||||||
<rect x="1" y="8" width="2" height="8" rx=".5" ry=".5" />
|
<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="4" y="5" width="2" height="14" rx=".5" ry=".5" />
|
||||||
<rect x="7" y="8" width="2" height="8" 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>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown-popup">
|
<li class="dropdown-popup">
|
||||||
<div id="narrate-control" class="narrate-row">
|
<div class="narrate-row narrate-control">
|
||||||
<button disabled id="narrate-skip-previous"
|
<button disabled class="narrate-skip-previous"
|
||||||
title="${"back"}"></button>
|
title="${"back"}"></button>
|
||||||
<button id="narrate-start-stop" title="${"start"}"></button>
|
<button class="narrate-start-stop" title="${"start"}"></button>
|
||||||
<button disabled id="narrate-skip-next"
|
<button disabled class="narrate-skip-next"
|
||||||
title="${"forward"}"></button>
|
title="${"forward"}"></button>
|
||||||
</div>
|
</div>
|
||||||
<div id="narrate-rate" class="narrate-row">
|
<div class="narrate-row narrate-rate">
|
||||||
<input id="narrate-rate-input" value="0" title="${"speed"}"
|
<input class="narrate-rate-input" value="0" title="${"speed"}"
|
||||||
step="5" max="100" min="-100" type="range">
|
step="5" max="100" min="-100" type="range">
|
||||||
</div>
|
</div>
|
||||||
<div id="narrate-voices" class="narrate-row"></div>
|
<div class="narrate-row narrate-voices"></div>
|
||||||
<div class="dropdown-arrow"></div>
|
<div class="dropdown-arrow"></div>
|
||||||
</li>`;
|
</li>`;
|
||||||
|
|
||||||
this.narrator = new Narrator(win);
|
this.narrator = new Narrator(win, languagePromise);
|
||||||
|
|
||||||
let branch = Services.prefs.getBranch("narrate.");
|
let branch = Services.prefs.getBranch("narrate.");
|
||||||
let selectLabel = gStrings.GetStringFromName("selectvoicelabel");
|
let selectLabel = gStrings.GetStringFromName("selectvoicelabel");
|
||||||
this.voiceSelect = new VoiceSelect(win, selectLabel);
|
this.voiceSelect = new VoiceSelect(win, selectLabel);
|
||||||
this.voiceSelect.element.addEventListener("change", this);
|
this.voiceSelect.element.addEventListener("change", this);
|
||||||
this.voiceSelect.element.id = "voice-select";
|
this.voiceSelect.element.classList.add("voice-select");
|
||||||
win.speechSynthesis.addEventListener("voiceschanged", this);
|
win.speechSynthesis.addEventListener("voiceschanged", this);
|
||||||
dropdown.querySelector("#narrate-voices").appendChild(
|
dropdown.querySelector(".narrate-voices").appendChild(
|
||||||
this.voiceSelect.element);
|
this.voiceSelect.element);
|
||||||
|
|
||||||
dropdown.addEventListener("click", this, true);
|
dropdown.addEventListener("click", this, true);
|
||||||
|
|
||||||
let rateRange = dropdown.querySelector("#narrate-rate > input");
|
let rateRange = dropdown.querySelector(".narrate-rate > input");
|
||||||
rateRange.addEventListener("change", this);
|
rateRange.addEventListener("change", this);
|
||||||
|
|
||||||
// The rate is stored as an integer.
|
// The rate is stored as an integer.
|
||||||
|
|
@ -131,15 +128,15 @@ function NarrateControls(mm, win) {
|
||||||
|
|
||||||
this._setupVoices();
|
this._setupVoices();
|
||||||
|
|
||||||
let tb = win.document.getElementById("reader-toolbar");
|
let tb = win.document.querySelector(".reader-toolbar");
|
||||||
tb.appendChild(dropdown);
|
tb.appendChild(dropdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
NarrateControls.prototype = {
|
NarrateControls.prototype = {
|
||||||
handleEvent: function(evt) {
|
handleEvent(evt) {
|
||||||
switch (evt.type) {
|
switch (evt.type) {
|
||||||
case "change":
|
case "change":
|
||||||
if (evt.target.id == "narrate-rate-input") {
|
if (evt.target.classList.contains("narrate-rate-input")) {
|
||||||
this._onRateInput(evt);
|
this._onRateInput(evt);
|
||||||
} else {
|
} else {
|
||||||
this._onVoiceChange();
|
this._onVoiceChange();
|
||||||
|
|
@ -162,8 +159,8 @@ NarrateControls.prototype = {
|
||||||
/**
|
/**
|
||||||
* Returns true if synth voices are available.
|
* Returns true if synth voices are available.
|
||||||
*/
|
*/
|
||||||
_setupVoices: function() {
|
_setupVoices() {
|
||||||
return this.narrator.languagePromise.then(language => {
|
return this._languagePromise.then(language => {
|
||||||
this.voiceSelect.clear();
|
this.voiceSelect.clear();
|
||||||
let win = this._win;
|
let win = this._win;
|
||||||
let voicePrefs = this._getVoicePref();
|
let voicePrefs = this._getVoicePref();
|
||||||
|
|
@ -190,7 +187,7 @@ NarrateControls.prototype = {
|
||||||
this.voiceSelect.addOptions(options);
|
this.voiceSelect.addOptions(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
let narrateToggle = win.document.getElementById("narrate-toggle");
|
let narrateToggle = win.document.querySelector(".narrate-toggle");
|
||||||
let histogram = Services.telemetry.getKeyedHistogramById(
|
let histogram = Services.telemetry.getKeyedHistogramById(
|
||||||
"NARRATE_CONTENT_BY_LANGUAGE_2");
|
"NARRATE_CONTENT_BY_LANGUAGE_2");
|
||||||
let initial = !this._voicesInitialized;
|
let initial = !this._voicesInitialized;
|
||||||
|
|
@ -210,7 +207,7 @@ NarrateControls.prototype = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_getVoicePref: function() {
|
_getVoicePref() {
|
||||||
let voicePref = Services.prefs.getCharPref("narrate.voice");
|
let voicePref = Services.prefs.getCharPref("narrate.voice");
|
||||||
try {
|
try {
|
||||||
return JSON.parse(voicePref);
|
return JSON.parse(voicePref);
|
||||||
|
|
@ -219,15 +216,15 @@ NarrateControls.prototype = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_onRateInput: function(evt) {
|
_onRateInput(evt) {
|
||||||
AsyncPrefs.set("narrate.rate", parseInt(evt.target.value, 10));
|
AsyncPrefs.set("narrate.rate", parseInt(evt.target.value, 10));
|
||||||
this.narrator.setRate(this._convertRate(evt.target.value));
|
this.narrator.setRate(this._convertRate(evt.target.value));
|
||||||
},
|
},
|
||||||
|
|
||||||
_onVoiceChange: function() {
|
_onVoiceChange() {
|
||||||
let voice = this.voice;
|
let voice = this.voice;
|
||||||
this.narrator.setVoice(voice);
|
this.narrator.setVoice(voice);
|
||||||
this.narrator.languagePromise.then(language => {
|
this._languagePromise.then(language => {
|
||||||
if (language) {
|
if (language) {
|
||||||
let voicePref = this._getVoicePref();
|
let voicePref = this._getVoicePref();
|
||||||
voicePref[language || "default"] = voice;
|
voicePref[language || "default"] = voice;
|
||||||
|
|
@ -236,42 +233,39 @@ NarrateControls.prototype = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onButtonClick: function(evt) {
|
_onButtonClick(evt) {
|
||||||
switch (evt.target.id) {
|
let classList = evt.target.classList;
|
||||||
case "narrate-skip-previous":
|
if (classList.contains("narrate-skip-previous")) {
|
||||||
this.narrator.skipPrevious();
|
this.narrator.skipPrevious();
|
||||||
break;
|
} else if (classList.contains("narrate-skip-next")) {
|
||||||
case "narrate-skip-next":
|
this.narrator.skipNext();
|
||||||
this.narrator.skipNext();
|
} else if (classList.contains("narrate-start-stop")) {
|
||||||
break;
|
if (this.narrator.speaking) {
|
||||||
case "narrate-start-stop":
|
this.narrator.stop();
|
||||||
if (this.narrator.speaking) {
|
} else {
|
||||||
this.narrator.stop();
|
this._updateSpeechControls(true);
|
||||||
} else {
|
let options = { rate: this.rate, voice: this.voice };
|
||||||
this._updateSpeechControls(true);
|
this.narrator.start(options).then(() => {
|
||||||
let options = { rate: this.rate, voice: this.voice };
|
this._updateSpeechControls(false);
|
||||||
this.narrator.start(options).then(() => {
|
}, err => {
|
||||||
this._updateSpeechControls(false);
|
Cu.reportError(`Narrate failed: ${err}.`);
|
||||||
}, err => {
|
this._updateSpeechControls(false);
|
||||||
Cu.reportError(`Narrate failed: ${err}.`);
|
});
|
||||||
this._updateSpeechControls(false);
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_updateSpeechControls: function(speaking) {
|
_updateSpeechControls(speaking) {
|
||||||
let dropdown = this._doc.getElementById("narrate-dropdown");
|
let dropdown = this._doc.querySelector(".narrate-dropdown");
|
||||||
dropdown.classList.toggle("keep-open", speaking);
|
dropdown.classList.toggle("keep-open", speaking);
|
||||||
dropdown.classList.toggle("speaking", speaking);
|
dropdown.classList.toggle("speaking", speaking);
|
||||||
|
|
||||||
let startStopButton = this._doc.getElementById("narrate-start-stop");
|
let startStopButton = this._doc.querySelector(".narrate-start-stop");
|
||||||
startStopButton.title =
|
startStopButton.title =
|
||||||
gStrings.GetStringFromName(speaking ? "stop" : "start");
|
gStrings.GetStringFromName(speaking ? "stop" : "start");
|
||||||
|
|
||||||
this._doc.getElementById("narrate-skip-previous").disabled = !speaking;
|
this._doc.querySelector(".narrate-skip-previous").disabled = !speaking;
|
||||||
this._doc.getElementById("narrate-skip-next").disabled = !speaking;
|
this._doc.querySelector(".narrate-skip-next").disabled = !speaking;
|
||||||
|
|
||||||
if (speaking) {
|
if (speaking) {
|
||||||
TelemetryStopwatch.start("NARRATE_CONTENT_SPEAKTIME_MS", this);
|
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
|
// This is a highly imperfect method of making human-readable labels
|
||||||
// for system voices. Because each platform has a different naming scheme
|
// for system voices. Because each platform has a different naming scheme
|
||||||
// for voices, we use a different method for each platform.
|
// for voices, we use a different method for each platform.
|
||||||
|
|
@ -303,7 +297,7 @@ NarrateControls.prototype = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_getLanguageName: function(lang) {
|
_getLanguageName(lang) {
|
||||||
if (!this._langStrings) {
|
if (!this._langStrings) {
|
||||||
this._langStrings = Services.strings.createBundle(
|
this._langStrings = Services.strings.createBundle(
|
||||||
"chrome://global/locale/languageNames.properties ");
|
"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.
|
// 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,
|
// 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.
|
// 0.5 is half the speed and 2 is twice the speed in fractions.
|
||||||
|
|
@ -334,7 +328,7 @@ NarrateControls.prototype = {
|
||||||
|
|
||||||
get rate() {
|
get rate() {
|
||||||
return this._convertRate(
|
return this._convertRate(
|
||||||
this._doc.getElementById("narrate-rate-input").value);
|
this._doc.querySelector(".narrate-rate-input").value);
|
||||||
},
|
},
|
||||||
|
|
||||||
get voice() {
|
get voice() {
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@ const { interfaces: Ci, utils: Cu } = Components;
|
||||||
|
|
||||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
|
|
||||||
"resource:///modules/translation/LanguageDetector.jsm");
|
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "Services",
|
XPCOMUtils.defineLazyModuleGetter(this, "Services",
|
||||||
"resource://gre/modules/Services.jsm");
|
"resource://gre/modules/Services.jsm");
|
||||||
|
|
||||||
|
|
@ -24,29 +22,13 @@ const kTextStylesRules = ["font-family", "font-kerning", "font-size",
|
||||||
"line-height", "letter-spacing", "text-orientation",
|
"line-height", "letter-spacing", "text-orientation",
|
||||||
"text-transform", "word-spacing"];
|
"text-transform", "word-spacing"];
|
||||||
|
|
||||||
function Narrator(win) {
|
function Narrator(win, languagePromise) {
|
||||||
this._winRef = Cu.getWeakReference(win);
|
this._winRef = Cu.getWeakReference(win);
|
||||||
|
this._languagePromise = languagePromise;
|
||||||
this._inTest = Services.prefs.getBoolPref("narrate.test");
|
this._inTest = Services.prefs.getBoolPref("narrate.test");
|
||||||
this._speechOptions = {};
|
this._speechOptions = {};
|
||||||
this._startTime = 0;
|
this._startTime = 0;
|
||||||
this._stopped = false;
|
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 = {
|
Narrator.prototype = {
|
||||||
|
|
@ -71,7 +53,7 @@ Narrator.prototype = {
|
||||||
// For example, paragraphs. But nested anchors and other elements
|
// For example, paragraphs. But nested anchors and other elements
|
||||||
// are not interesting since their text already appears in their
|
// are not interesting since their text already appears in their
|
||||||
// parent's textContent.
|
// parent's textContent.
|
||||||
acceptNode: function(node) {
|
acceptNode(node) {
|
||||||
if (this._matches.has(node.parentNode)) {
|
if (this._matches.has(node.parentNode)) {
|
||||||
// Reject sub-trees of accepted nodes.
|
// Reject sub-trees of accepted nodes.
|
||||||
return nf.FILTER_REJECT;
|
return nf.FILTER_REJECT;
|
||||||
|
|
@ -107,7 +89,7 @@ Narrator.prototype = {
|
||||||
// are no other strong references, and it will be GC'ed. Instead,
|
// 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.
|
// we rely on the window's lifetime and use it as a weak reference.
|
||||||
this._treeWalkerRef.set(this._win,
|
this._treeWalkerRef.set(this._win,
|
||||||
this._doc.createTreeWalker(this._doc.getElementById("container"),
|
this._doc.createTreeWalker(this._doc.querySelector(".container"),
|
||||||
nf.SHOW_ELEMENT, filter, false));
|
nf.SHOW_ELEMENT, filter, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,7 +106,7 @@ Narrator.prototype = {
|
||||||
this._win.speechSynthesis.pending;
|
this._win.speechSynthesis.pending;
|
||||||
},
|
},
|
||||||
|
|
||||||
_getVoice: function(voiceURI) {
|
_getVoice(voiceURI) {
|
||||||
if (!this._voiceMap || !this._voiceMap.has(voiceURI)) {
|
if (!this._voiceMap || !this._voiceMap.has(voiceURI)) {
|
||||||
this._voiceMap = new Map(
|
this._voiceMap = new Map(
|
||||||
this._win.speechSynthesis.getVoices().map(v => [v.voiceURI, v]));
|
this._win.speechSynthesis.getVoices().map(v => [v.voiceURI, v]));
|
||||||
|
|
@ -133,7 +115,7 @@ Narrator.prototype = {
|
||||||
return this._voiceMap.get(voiceURI);
|
return this._voiceMap.get(voiceURI);
|
||||||
},
|
},
|
||||||
|
|
||||||
_isParagraphInView: function(paragraph) {
|
_isParagraphInView(paragraph) {
|
||||||
if (!paragraph) {
|
if (!paragraph) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -142,13 +124,13 @@ Narrator.prototype = {
|
||||||
return bb.top >= 0 && bb.top < this._win.innerHeight;
|
return bb.top >= 0 && bb.top < this._win.innerHeight;
|
||||||
},
|
},
|
||||||
|
|
||||||
_sendTestEvent: function(eventType, detail) {
|
_sendTestEvent(eventType, detail) {
|
||||||
let win = this._win;
|
let win = this._win;
|
||||||
win.dispatchEvent(new win.CustomEvent(eventType,
|
win.dispatchEvent(new win.CustomEvent(eventType,
|
||||||
{ detail: Cu.cloneInto(detail, win.document) }));
|
{ detail: Cu.cloneInto(detail, win.document) }));
|
||||||
},
|
},
|
||||||
|
|
||||||
_speakInner: function() {
|
_speakInner() {
|
||||||
this._win.speechSynthesis.cancel();
|
this._win.speechSynthesis.cancel();
|
||||||
let tw = this._treeWalker;
|
let tw = this._treeWalker;
|
||||||
let paragraph = tw.currentNode;
|
let paragraph = tw.currentNode;
|
||||||
|
|
@ -238,18 +220,12 @@ Narrator.prototype = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match non-whitespace. This isn't perfect, but the most universal
|
if (e.charLength) {
|
||||||
// solution for now.
|
highlighter.highlight(e.charIndex, e.charLength);
|
||||||
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 (this._inTest) {
|
if (this._inTest) {
|
||||||
this._sendTestEvent("wordhighlight", {
|
this._sendTestEvent("wordhighlight", {
|
||||||
start: firstIndex.index,
|
start: e.charIndex,
|
||||||
end: reWordBoundary.lastIndex
|
end: e.charIndex + e.charLength
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -259,14 +235,14 @@ Narrator.prototype = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
start: function(speechOptions) {
|
start(speechOptions) {
|
||||||
this._speechOptions = {
|
this._speechOptions = {
|
||||||
rate: speechOptions.rate,
|
rate: speechOptions.rate,
|
||||||
voice: this._getVoice(speechOptions.voice)
|
voice: this._getVoice(speechOptions.voice)
|
||||||
};
|
};
|
||||||
|
|
||||||
this._stopped = false;
|
this._stopped = false;
|
||||||
return this.languagePromise.then(language => {
|
return this._languagePromise.then(language => {
|
||||||
if (!this._speechOptions.voice) {
|
if (!this._speechOptions.voice) {
|
||||||
this._speechOptions.lang = language;
|
this._speechOptions.lang = language;
|
||||||
}
|
}
|
||||||
|
|
@ -288,32 +264,32 @@ Narrator.prototype = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
stop: function() {
|
stop() {
|
||||||
this._stopped = true;
|
this._stopped = true;
|
||||||
this._win.speechSynthesis.cancel();
|
this._win.speechSynthesis.cancel();
|
||||||
},
|
},
|
||||||
|
|
||||||
skipNext: function() {
|
skipNext() {
|
||||||
this._win.speechSynthesis.cancel();
|
this._win.speechSynthesis.cancel();
|
||||||
},
|
},
|
||||||
|
|
||||||
skipPrevious: function() {
|
skipPrevious() {
|
||||||
this._goBackParagraphs(this._timeIntoParagraph < PREV_THRESHOLD ? 2 : 1);
|
this._goBackParagraphs(this._timeIntoParagraph < PREV_THRESHOLD ? 2 : 1);
|
||||||
},
|
},
|
||||||
|
|
||||||
setRate: function(rate) {
|
setRate(rate) {
|
||||||
this._speechOptions.rate = rate;
|
this._speechOptions.rate = rate;
|
||||||
/* repeat current paragraph */
|
/* repeat current paragraph */
|
||||||
this._goBackParagraphs(1);
|
this._goBackParagraphs(1);
|
||||||
},
|
},
|
||||||
|
|
||||||
setVoice: function(voice) {
|
setVoice(voice) {
|
||||||
this._speechOptions.voice = this._getVoice(voice);
|
this._speechOptions.voice = this._getVoice(voice);
|
||||||
/* repeat current paragraph */
|
/* repeat current paragraph */
|
||||||
this._goBackParagraphs(1);
|
this._goBackParagraphs(1);
|
||||||
},
|
},
|
||||||
|
|
||||||
_goBackParagraphs: function(count) {
|
_goBackParagraphs(count) {
|
||||||
let tw = this._treeWalker;
|
let tw = this._treeWalker;
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
if (!tw.previousNode()) {
|
if (!tw.previousNode()) {
|
||||||
|
|
@ -338,13 +314,13 @@ Highlighter.prototype = {
|
||||||
* Highlight the range within offsets relative to the container.
|
* Highlight the range within offsets relative to the container.
|
||||||
*
|
*
|
||||||
* @param {Number} startOffset the start offset
|
* @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 containerRect = this.container.getBoundingClientRect();
|
||||||
let range = this._getRange(startOffset, endOffset);
|
let range = this._getRange(startOffset, startOffset + length);
|
||||||
let rangeRects = range.getClientRects();
|
let rangeRects = range.getClientRects();
|
||||||
let win = this.container.ownerDocument.defaultView;
|
let win = this.container.ownerGlobal;
|
||||||
let computedStyle = win.getComputedStyle(range.endContainer.parentNode);
|
let computedStyle = win.getComputedStyle(range.endContainer.parentNode);
|
||||||
let nodes = this._getFreshHighlightNodes(rangeRects.length);
|
let nodes = this._getFreshHighlightNodes(rangeRects.length);
|
||||||
|
|
||||||
|
|
@ -386,7 +362,7 @@ Highlighter.prototype = {
|
||||||
/**
|
/**
|
||||||
* Releases reference to container and removes all highlight nodes.
|
* Releases reference to container and removes all highlight nodes.
|
||||||
*/
|
*/
|
||||||
remove: function() {
|
remove() {
|
||||||
for (let node of this._nodes) {
|
for (let node of this._nodes) {
|
||||||
node.remove();
|
node.remove();
|
||||||
}
|
}
|
||||||
|
|
@ -400,7 +376,7 @@ Highlighter.prototype = {
|
||||||
*
|
*
|
||||||
* @param {Number} count number of nodes needed
|
* @param {Number} count number of nodes needed
|
||||||
*/
|
*/
|
||||||
_getFreshHighlightNodes: function(count) {
|
_getFreshHighlightNodes(count) {
|
||||||
let doc = this.container.ownerDocument;
|
let doc = this.container.ownerDocument;
|
||||||
let nodes = Array.from(this._nodes);
|
let nodes = Array.from(this._nodes);
|
||||||
|
|
||||||
|
|
@ -427,7 +403,7 @@ Highlighter.prototype = {
|
||||||
* @param {Number} startOffset the start offset
|
* @param {Number} startOffset the start offset
|
||||||
* @param {Number} endOffset the end offset
|
* @param {Number} endOffset the end offset
|
||||||
*/
|
*/
|
||||||
_getRange: function(startOffset, endOffset) {
|
_getRange(startOffset, endOffset) {
|
||||||
let doc = this.container.ownerDocument;
|
let doc = this.container.ownerDocument;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
let treeWalker = doc.createTreeWalker(
|
let treeWalker = doc.createTreeWalker(
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ function VoiceSelect(win, label) {
|
||||||
|
|
||||||
let element = win.document.createElement("div");
|
let element = win.document.createElement("div");
|
||||||
element.classList.add("voiceselect");
|
element.classList.add("voiceselect");
|
||||||
|
// eslint-disable-next-line no-unsanitized/property
|
||||||
element.innerHTML =
|
element.innerHTML =
|
||||||
`<button class="select-toggle" aria-controls="voice-options">
|
`<button class="select-toggle" aria-controls="voice-options">
|
||||||
<span class="label">${label}</span> <span class="current-voice"></span>
|
<span class="label">${label}</span> <span class="current-voice"></span>
|
||||||
|
|
@ -37,7 +38,7 @@ function VoiceSelect(win, label) {
|
||||||
}
|
}
|
||||||
|
|
||||||
VoiceSelect.prototype = {
|
VoiceSelect.prototype = {
|
||||||
add: function(label, value) {
|
add(label, value) {
|
||||||
let option = this._doc.createElement("button");
|
let option = this._doc.createElement("button");
|
||||||
option.dataset.value = value;
|
option.dataset.value = value;
|
||||||
option.classList.add("option");
|
option.classList.add("option");
|
||||||
|
|
@ -48,7 +49,7 @@ VoiceSelect.prototype = {
|
||||||
return option;
|
return option;
|
||||||
},
|
},
|
||||||
|
|
||||||
addOptions: function(options) {
|
addOptions(options) {
|
||||||
let selected = null;
|
let selected = null;
|
||||||
for (let option of options) {
|
for (let option of options) {
|
||||||
if (option.selected) {
|
if (option.selected) {
|
||||||
|
|
@ -61,11 +62,11 @@ VoiceSelect.prototype = {
|
||||||
this._select(selected || this.options[0], true);
|
this._select(selected || this.options[0], true);
|
||||||
},
|
},
|
||||||
|
|
||||||
clear: function() {
|
clear() {
|
||||||
this.listbox.innerHTML = "";
|
this.listbox.innerHTML = "";
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleList: function(force, focus = true) {
|
toggleList(force, focus = true) {
|
||||||
if (this.element.classList.toggle("open", force)) {
|
if (this.element.classList.toggle("open", force)) {
|
||||||
if (focus) {
|
if (focus) {
|
||||||
(this.selected || this.options[0]).focus();
|
(this.selected || this.options[0]).focus();
|
||||||
|
|
@ -84,7 +85,7 @@ VoiceSelect.prototype = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleEvent: function(evt) {
|
handleEvent(evt) {
|
||||||
let target = evt.target;
|
let target = evt.target;
|
||||||
|
|
||||||
switch (evt.type) {
|
switch (evt.type) {
|
||||||
|
|
@ -131,7 +132,7 @@ VoiceSelect.prototype = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_getPagedOption: function(option, up) {
|
_getPagedOption(option, up) {
|
||||||
let height = elem => elem.getBoundingClientRect().height;
|
let height = elem => elem.getBoundingClientRect().height;
|
||||||
let listboxHeight = height(this.listbox);
|
let listboxHeight = height(this.listbox);
|
||||||
|
|
||||||
|
|
@ -148,7 +149,7 @@ VoiceSelect.prototype = {
|
||||||
return next;
|
return next;
|
||||||
},
|
},
|
||||||
|
|
||||||
_keyPressedButton: function(evt) {
|
_keyPressedButton(evt) {
|
||||||
if (evt.altKey && (evt.key === "ArrowUp" || evt.key === "ArrowUp")) {
|
if (evt.altKey && (evt.key === "ArrowUp" || evt.key === "ArrowUp")) {
|
||||||
this.toggleList(true);
|
this.toggleList(true);
|
||||||
return;
|
return;
|
||||||
|
|
@ -178,7 +179,7 @@ VoiceSelect.prototype = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_keyPressedInBox: function(evt) {
|
_keyPressedInBox(evt) {
|
||||||
let toFocus;
|
let toFocus;
|
||||||
let cur = this._doc.activeElement;
|
let cur = this._doc.activeElement;
|
||||||
|
|
||||||
|
|
@ -212,7 +213,7 @@ VoiceSelect.prototype = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_select: function(option, suppressEvent = false) {
|
_select(option, suppressEvent = false) {
|
||||||
let oldSelected = this.selected;
|
let oldSelected = this.selected;
|
||||||
if (oldSelected) {
|
if (oldSelected) {
|
||||||
oldSelected.removeAttribute("aria-selected");
|
oldSelected.removeAttribute("aria-selected");
|
||||||
|
|
@ -233,7 +234,7 @@ VoiceSelect.prototype = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_updateDropdownHeight: function(now) {
|
_updateDropdownHeight(now) {
|
||||||
let updateInner = () => {
|
let updateInner = () => {
|
||||||
let winHeight = this._win.innerHeight;
|
let winHeight = this._win.innerHeight;
|
||||||
let listbox = this.listbox;
|
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);
|
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/. */
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
/* This file defines specific rules for print preview when using simplify mode.
|
/* 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
|
* 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
|
* will not take effect when using the simplify page checkbox. This file defines
|
||||||
* styling for title and author on the header element. */
|
* 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, "AsyncPrefs", "resource://gre/modules/AsyncPrefs.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "NarrateControls", "resource://gre/modules/narrate/NarrateControls.jsm");
|
XPCOMUtils.defineLazyModuleGetter(this, "NarrateControls", "resource://gre/modules/narrate/NarrateControls.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "Rect", "resource://gre/modules/Geometry.jsm");
|
XPCOMUtils.defineLazyModuleGetter(this, "Rect", "resource://gre/modules/Geometry.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
|
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", "resource://gre/modules/UITelemetry.jsm");
|
|
||||||
|
|
||||||
var gStrings = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
|
var gStrings = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
|
||||||
|
|
||||||
|
|
@ -45,45 +44,49 @@ var AboutReader = function(mm, win, articlePromise) {
|
||||||
.getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
|
.getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
|
||||||
|
|
||||||
this._article = null;
|
this._article = null;
|
||||||
|
this._languagePromise = new Promise(resolve => {
|
||||||
|
this._foundLanguage = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
if (articlePromise) {
|
if (articlePromise) {
|
||||||
this._articlePromise = articlePromise;
|
this._articlePromise = articlePromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._headerElementRef = Cu.getWeakReference(doc.getElementById("reader-header"));
|
this._headerElementRef = Cu.getWeakReference(doc.querySelector(".reader-header"));
|
||||||
this._domainElementRef = Cu.getWeakReference(doc.getElementById("reader-domain"));
|
this._domainElementRef = Cu.getWeakReference(doc.querySelector(".reader-domain"));
|
||||||
this._titleElementRef = Cu.getWeakReference(doc.getElementById("reader-title"));
|
this._titleElementRef = Cu.getWeakReference(doc.querySelector(".reader-title"));
|
||||||
this._creditsElementRef = Cu.getWeakReference(doc.getElementById("reader-credits"));
|
this._readTimeElementRef = Cu.getWeakReference(doc.querySelector(".reader-estimated-time"));
|
||||||
this._contentElementRef = Cu.getWeakReference(doc.getElementById("moz-reader-content"));
|
this._creditsElementRef = Cu.getWeakReference(doc.querySelector(".reader-credits"));
|
||||||
this._toolbarElementRef = Cu.getWeakReference(doc.getElementById("reader-toolbar"));
|
this._contentElementRef = Cu.getWeakReference(doc.querySelector(".moz-reader-content"));
|
||||||
this._messageElementRef = Cu.getWeakReference(doc.getElementById("reader-message"));
|
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;
|
this._scrollOffset = win.pageYOffset;
|
||||||
|
|
||||||
doc.addEventListener("click", this, false);
|
doc.addEventListener("click", this);
|
||||||
|
|
||||||
win.addEventListener("pagehide", this, false);
|
win.addEventListener("pagehide", this);
|
||||||
win.addEventListener("scroll", this, false);
|
win.addEventListener("scroll", this);
|
||||||
win.addEventListener("resize", this, false);
|
win.addEventListener("resize", this);
|
||||||
|
|
||||||
Services.obs.addObserver(this, "inner-window-destroyed", false);
|
Services.obs.addObserver(this, "inner-window-destroyed", false);
|
||||||
|
|
||||||
doc.addEventListener("visibilitychange", this, false);
|
doc.addEventListener("visibilitychange", this);
|
||||||
|
|
||||||
this._setupStyleDropdown();
|
this._setupStyleDropdown();
|
||||||
this._setupButton("close-button", this._onReaderClose.bind(this), "aboutReader.toolbar.close");
|
this._setupButton("close-button", this._onReaderClose.bind(this), "aboutReader.toolbar.close");
|
||||||
|
|
||||||
const gIsFirefoxDesktop = Services.appinfo.ID == "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
|
// we're ready for any external setup, send a signal for that.
|
||||||
if (gIsFirefoxDesktop) {
|
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 colorSchemeValues = JSON.parse(Services.prefs.getCharPref("reader.color_scheme.values"));
|
||||||
let colorSchemeOptions = colorSchemeValues.map((value) => {
|
let colorSchemeOptions = colorSchemeValues.map((value) => {
|
||||||
return { name: gStrings.GetStringFromName("aboutReader.colorScheme." + value),
|
return {
|
||||||
value: value,
|
name: gStrings.GetStringFromName("aboutReader.colorScheme." + value),
|
||||||
itemClass: value + "-button" };
|
value,
|
||||||
|
itemClass: value + "-button"
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
|
let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
|
||||||
|
|
@ -114,7 +117,7 @@ var AboutReader = function(mm, win, articlePromise) {
|
||||||
this._setupLineHeightButtons();
|
this._setupLineHeightButtons();
|
||||||
|
|
||||||
if (win.speechSynthesis && Services.prefs.getBoolPref("narrate.enabled")) {
|
if (win.speechSynthesis && Services.prefs.getBoolPref("narrate.enabled")) {
|
||||||
new NarrateControls(mm, win);
|
new NarrateControls(mm, win, this._languagePromise);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._loadArticle();
|
this._loadArticle();
|
||||||
|
|
@ -146,6 +149,10 @@ AboutReader.prototype = {
|
||||||
return this._titleElementRef.get();
|
return this._titleElementRef.get();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get _readTimeElement() {
|
||||||
|
return this._readTimeElementRef.get();
|
||||||
|
},
|
||||||
|
|
||||||
get _creditsElement() {
|
get _creditsElement() {
|
||||||
return this._creditsElementRef.get();
|
return this._creditsElementRef.get();
|
||||||
},
|
},
|
||||||
|
|
@ -162,6 +169,10 @@ AboutReader.prototype = {
|
||||||
return this._messageElementRef.get();
|
return this._messageElementRef.get();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get _containerElement() {
|
||||||
|
return this._containerElementRef.get();
|
||||||
|
},
|
||||||
|
|
||||||
get _isToolbarVertical() {
|
get _isToolbarVertical() {
|
||||||
if (this._toolbarVertical !== undefined) {
|
if (this._toolbarVertical !== undefined) {
|
||||||
return this._toolbarVertical;
|
return this._toolbarVertical;
|
||||||
|
|
@ -178,7 +189,7 @@ AboutReader.prototype = {
|
||||||
return _viewId;
|
return _viewId;
|
||||||
},
|
},
|
||||||
|
|
||||||
receiveMessage: function (message) {
|
receiveMessage (message) {
|
||||||
switch (message.name) {
|
switch (message.name) {
|
||||||
// Triggered by Android user pressing BACK while the banner font-dropdown is open.
|
// Triggered by Android user pressing BACK while the banner font-dropdown is open.
|
||||||
case "Reader:CloseDropdown": {
|
case "Reader:CloseDropdown": {
|
||||||
|
|
@ -191,14 +202,14 @@ AboutReader.prototype = {
|
||||||
if (message.data.id && message.data.image &&
|
if (message.data.id && message.data.image &&
|
||||||
!this._doc.getElementById(message.data.id)) {
|
!this._doc.getElementById(message.data.id)) {
|
||||||
let btn = this._doc.createElement("button");
|
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("style", "background-image: url('" + message.data.image + "')");
|
||||||
btn.setAttribute("id", message.data.id);
|
btn.setAttribute("id", message.data.id);
|
||||||
if (message.data.title)
|
if (message.data.title)
|
||||||
btn.setAttribute("title", message.data.title);
|
btn.setAttribute("title", message.data.title);
|
||||||
if (message.data.text)
|
if (message.data.text)
|
||||||
btn.textContent = message.data.text;
|
btn.textContent = message.data.text;
|
||||||
let tb = this._doc.getElementById("reader-toolbar");
|
let tb = this._toolbarElement;
|
||||||
tb.appendChild(btn);
|
tb.appendChild(btn);
|
||||||
this._setupButton(message.data.id, button => {
|
this._setupButton(message.data.id, button => {
|
||||||
this._mm.sendAsyncMessage("Reader:Clicked-" + button.getAttribute("id"), { article: this._article });
|
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)
|
if (!aEvent.isTrusted)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
switch (aEvent.type) {
|
switch (aEvent.type) {
|
||||||
case "click":
|
case "click":
|
||||||
let target = aEvent.target;
|
let target = aEvent.target;
|
||||||
if (target.classList.contains('dropdown-toggle')) {
|
if (target.classList.contains("dropdown-toggle")) {
|
||||||
this._toggleDropdownClicked(aEvent);
|
this._toggleDropdownClicked(aEvent);
|
||||||
} else if (!target.closest('.dropdown-popup')) {
|
} else if (!target.closest(".dropdown-popup")) {
|
||||||
this._closeDropdowns();
|
this._closeDropdowns();
|
||||||
}
|
}
|
||||||
|
if (target.tagName == "A" && !target.classList.contains("reader-domain")) {
|
||||||
|
this._linkClicked(aEvent);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "scroll":
|
case "scroll":
|
||||||
this._closeDropdowns(true);
|
this._closeDropdowns(true);
|
||||||
|
|
@ -243,7 +257,7 @@ AboutReader.prototype = {
|
||||||
this._updateImageMargins();
|
this._updateImageMargins();
|
||||||
if (this._isToolbarVertical) {
|
if (this._isToolbarVertical) {
|
||||||
this._win.setTimeout(() => {
|
this._win.setTimeout(() => {
|
||||||
for (let dropdown of this._doc.querySelectorAll('.dropdown.open')) {
|
for (let dropdown of this._doc.querySelectorAll(".dropdown.open")) {
|
||||||
this._updatePopupPosition(dropdown);
|
this._updatePopupPosition(dropdown);
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
@ -271,12 +285,12 @@ AboutReader.prototype = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
observe: function(subject, topic, data) {
|
observe(subject, topic, data) {
|
||||||
if (subject.QueryInterface(Ci.nsISupportsPRUint64).data != this._innerWindowId) {
|
if (subject.QueryInterface(Ci.nsISupportsPRUint64).data != this._innerWindowId) {
|
||||||
return;
|
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:CloseDropdown", this);
|
||||||
this._mm.removeMessageListener("Reader:AddButton", this);
|
this._mm.removeMessageListener("Reader:AddButton", this);
|
||||||
|
|
@ -284,12 +298,12 @@ AboutReader.prototype = {
|
||||||
this._windowUnloaded = true;
|
this._windowUnloaded = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
_onReaderClose: function() {
|
_onReaderClose() {
|
||||||
ReaderMode.leaveReaderMode(this._mm.docShell, this._win);
|
ReaderMode.leaveReaderMode(this._mm.docShell, this._win);
|
||||||
},
|
},
|
||||||
|
|
||||||
_setFontSize: function(newFontSize) {
|
_setFontSize(newFontSize) {
|
||||||
let containerClasses = this._doc.getElementById("container").classList;
|
let containerClasses = this._containerElement.classList;
|
||||||
|
|
||||||
if (this._fontSize > 0)
|
if (this._fontSize > 0)
|
||||||
containerClasses.remove("font-size" + this._fontSize);
|
containerClasses.remove("font-size" + this._fontSize);
|
||||||
|
|
@ -299,19 +313,19 @@ AboutReader.prototype = {
|
||||||
return AsyncPrefs.set("reader.font_size", this._fontSize);
|
return AsyncPrefs.set("reader.font_size", this._fontSize);
|
||||||
},
|
},
|
||||||
|
|
||||||
_setupFontSizeButtons: function() {
|
_setupFontSizeButtons() {
|
||||||
const FONT_SIZE_MIN = 1;
|
const FONT_SIZE_MIN = 1;
|
||||||
const FONT_SIZE_MAX = 9;
|
const FONT_SIZE_MAX = 9;
|
||||||
|
|
||||||
// Sample text shown in Android UI.
|
// 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");
|
sampleText.textContent = gStrings.GetStringFromName("aboutReader.fontTypeSample");
|
||||||
|
|
||||||
let currentSize = Services.prefs.getIntPref("reader.font_size");
|
let currentSize = Services.prefs.getIntPref("reader.font_size");
|
||||||
currentSize = Math.max(FONT_SIZE_MIN, Math.min(FONT_SIZE_MAX, currentSize));
|
currentSize = Math.max(FONT_SIZE_MIN, Math.min(FONT_SIZE_MAX, currentSize));
|
||||||
|
|
||||||
let plusButton = this._doc.getElementById("font-size-plus");
|
let plusButton = this._doc.querySelector(".plus-button");
|
||||||
let minusButton = this._doc.getElementById("font-size-minus");
|
let minusButton = this._doc.querySelector(".minus-button");
|
||||||
|
|
||||||
function updateControls() {
|
function updateControls() {
|
||||||
if (currentSize === FONT_SIZE_MIN) {
|
if (currentSize === FONT_SIZE_MIN) {
|
||||||
|
|
@ -360,8 +374,8 @@ AboutReader.prototype = {
|
||||||
}, true);
|
}, true);
|
||||||
},
|
},
|
||||||
|
|
||||||
_setContentWidth: function(newContentWidth) {
|
_setContentWidth(newContentWidth) {
|
||||||
let containerClasses = this._doc.getElementById("container").classList;
|
let containerClasses = this._containerElement.classList;
|
||||||
|
|
||||||
if (this._contentWidth > 0)
|
if (this._contentWidth > 0)
|
||||||
containerClasses.remove("content-width" + this._contentWidth);
|
containerClasses.remove("content-width" + this._contentWidth);
|
||||||
|
|
@ -371,15 +385,15 @@ AboutReader.prototype = {
|
||||||
return AsyncPrefs.set("reader.content_width", this._contentWidth);
|
return AsyncPrefs.set("reader.content_width", this._contentWidth);
|
||||||
},
|
},
|
||||||
|
|
||||||
_setupContentWidthButtons: function() {
|
_setupContentWidthButtons() {
|
||||||
const CONTENT_WIDTH_MIN = 1;
|
const CONTENT_WIDTH_MIN = 1;
|
||||||
const CONTENT_WIDTH_MAX = 9;
|
const CONTENT_WIDTH_MAX = 9;
|
||||||
|
|
||||||
let currentContentWidth = Services.prefs.getIntPref("reader.content_width");
|
let currentContentWidth = Services.prefs.getIntPref("reader.content_width");
|
||||||
currentContentWidth = Math.max(CONTENT_WIDTH_MIN, Math.min(CONTENT_WIDTH_MAX, currentContentWidth));
|
currentContentWidth = Math.max(CONTENT_WIDTH_MIN, Math.min(CONTENT_WIDTH_MAX, currentContentWidth));
|
||||||
|
|
||||||
let plusButton = this._doc.getElementById("content-width-plus");
|
let plusButton = this._doc.querySelector(".content-width-plus-button");
|
||||||
let minusButton = this._doc.getElementById("content-width-minus");
|
let minusButton = this._doc.querySelector(".content-width-minus-button");
|
||||||
|
|
||||||
function updateControls() {
|
function updateControls() {
|
||||||
if (currentContentWidth === CONTENT_WIDTH_MIN) {
|
if (currentContentWidth === CONTENT_WIDTH_MIN) {
|
||||||
|
|
@ -428,8 +442,8 @@ AboutReader.prototype = {
|
||||||
}, true);
|
}, true);
|
||||||
},
|
},
|
||||||
|
|
||||||
_setLineHeight: function(newLineHeight) {
|
_setLineHeight(newLineHeight) {
|
||||||
let contentClasses = this._doc.getElementById("moz-reader-content").classList;
|
let contentClasses = this._contentElement.classList;
|
||||||
|
|
||||||
if (this._lineHeight > 0)
|
if (this._lineHeight > 0)
|
||||||
contentClasses.remove("line-height" + this._lineHeight);
|
contentClasses.remove("line-height" + this._lineHeight);
|
||||||
|
|
@ -439,15 +453,15 @@ AboutReader.prototype = {
|
||||||
return AsyncPrefs.set("reader.line_height", this._lineHeight);
|
return AsyncPrefs.set("reader.line_height", this._lineHeight);
|
||||||
},
|
},
|
||||||
|
|
||||||
_setupLineHeightButtons: function() {
|
_setupLineHeightButtons() {
|
||||||
const LINE_HEIGHT_MIN = 1;
|
const LINE_HEIGHT_MIN = 1;
|
||||||
const LINE_HEIGHT_MAX = 9;
|
const LINE_HEIGHT_MAX = 9;
|
||||||
|
|
||||||
let currentLineHeight = Services.prefs.getIntPref("reader.line_height");
|
let currentLineHeight = Services.prefs.getIntPref("reader.line_height");
|
||||||
currentLineHeight = Math.max(LINE_HEIGHT_MIN, Math.min(LINE_HEIGHT_MAX, currentLineHeight));
|
currentLineHeight = Math.max(LINE_HEIGHT_MIN, Math.min(LINE_HEIGHT_MAX, currentLineHeight));
|
||||||
|
|
||||||
let plusButton = this._doc.getElementById("line-height-plus");
|
let plusButton = this._doc.querySelector(".line-height-plus-button");
|
||||||
let minusButton = this._doc.getElementById("line-height-minus");
|
let minusButton = this._doc.querySelector(".line-height-minus-button");
|
||||||
|
|
||||||
function updateControls() {
|
function updateControls() {
|
||||||
if (currentLineHeight === LINE_HEIGHT_MIN) {
|
if (currentLineHeight === LINE_HEIGHT_MIN) {
|
||||||
|
|
@ -496,7 +510,7 @@ AboutReader.prototype = {
|
||||||
}, true);
|
}, true);
|
||||||
},
|
},
|
||||||
|
|
||||||
_handleDeviceLight: function(newLux) {
|
_handleDeviceLight(newLux) {
|
||||||
// Desired size of the this._luxValues array.
|
// Desired size of the this._luxValues array.
|
||||||
let luxValuesSize = 10;
|
let luxValuesSize = 10;
|
||||||
// Add new lux value at the front of the array.
|
// Add new lux value at the front of the array.
|
||||||
|
|
@ -513,7 +527,7 @@ AboutReader.prototype = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Holds the average of the lux values collected in this._luxValues.
|
// Holds the average of the lux values collected in this._luxValues.
|
||||||
let averageLuxValue = this._totalLux/luxValuesSize;
|
let averageLuxValue = this._totalLux / luxValuesSize;
|
||||||
|
|
||||||
this._updateColorScheme(averageLuxValue);
|
this._updateColorScheme(averageLuxValue);
|
||||||
// Pop the oldest value off the array.
|
// Pop the oldest value off the array.
|
||||||
|
|
@ -522,7 +536,7 @@ AboutReader.prototype = {
|
||||||
this._totalLux -= oldLux;
|
this._totalLux -= oldLux;
|
||||||
},
|
},
|
||||||
|
|
||||||
_handleVisibilityChange: function() {
|
_handleVisibilityChange() {
|
||||||
let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
|
let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
|
||||||
if (colorScheme != "auto") {
|
if (colorScheme != "auto") {
|
||||||
return;
|
return;
|
||||||
|
|
@ -533,19 +547,19 @@ AboutReader.prototype = {
|
||||||
},
|
},
|
||||||
|
|
||||||
// Setup or teardown the ambient light tracking system.
|
// Setup or teardown the ambient light tracking system.
|
||||||
_enableAmbientLighting: function(enable) {
|
_enableAmbientLighting(enable) {
|
||||||
if (enable) {
|
if (enable) {
|
||||||
this._win.addEventListener("devicelight", this, false);
|
this._win.addEventListener("devicelight", this);
|
||||||
this._luxValues = [];
|
this._luxValues = [];
|
||||||
this._totalLux = 0;
|
this._totalLux = 0;
|
||||||
} else {
|
} else {
|
||||||
this._win.removeEventListener("devicelight", this, false);
|
this._win.removeEventListener("devicelight", this);
|
||||||
delete this._luxValues;
|
delete this._luxValues;
|
||||||
delete this._totalLux;
|
delete this._totalLux;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_updateColorScheme: function(luxValue) {
|
_updateColorScheme(luxValue) {
|
||||||
// Upper bound value for "dark" color scheme beyond which it changes to "light".
|
// Upper bound value for "dark" color scheme beyond which it changes to "light".
|
||||||
let upperBoundDark = 50;
|
let upperBoundDark = 50;
|
||||||
// Lower bound value for "light" color scheme beyond which it changes to "dark".
|
// Lower bound value for "light" color scheme beyond which it changes to "dark".
|
||||||
|
|
@ -564,7 +578,7 @@ AboutReader.prototype = {
|
||||||
this._setColorScheme("light");
|
this._setColorScheme("light");
|
||||||
},
|
},
|
||||||
|
|
||||||
_setColorScheme: function(newColorScheme) {
|
_setColorScheme(newColorScheme) {
|
||||||
// "auto" is not a real color scheme
|
// "auto" is not a real color scheme
|
||||||
if (this._colorScheme === newColorScheme || newColorScheme === "auto")
|
if (this._colorScheme === newColorScheme || newColorScheme === "auto")
|
||||||
return;
|
return;
|
||||||
|
|
@ -580,14 +594,14 @@ AboutReader.prototype = {
|
||||||
|
|
||||||
// Pref values include "dark", "light", and "auto", which automatically switches
|
// Pref values include "dark", "light", and "auto", which automatically switches
|
||||||
// between light and dark color schemes based on the ambient light level.
|
// between light and dark color schemes based on the ambient light level.
|
||||||
_setColorSchemePref: function(colorSchemePref) {
|
_setColorSchemePref(colorSchemePref) {
|
||||||
this._enableAmbientLighting(colorSchemePref === "auto");
|
this._enableAmbientLighting(colorSchemePref === "auto");
|
||||||
this._setColorScheme(colorSchemePref);
|
this._setColorScheme(colorSchemePref);
|
||||||
|
|
||||||
AsyncPrefs.set("reader.color_scheme", colorSchemePref);
|
AsyncPrefs.set("reader.color_scheme", colorSchemePref);
|
||||||
},
|
},
|
||||||
|
|
||||||
_setFontType: function(newFontType) {
|
_setFontType(newFontType) {
|
||||||
if (this._fontType === newFontType)
|
if (this._fontType === newFontType)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
@ -602,20 +616,38 @@ AboutReader.prototype = {
|
||||||
AsyncPrefs.set("reader.font_type", this._fontType);
|
AsyncPrefs.set("reader.font_type", this._fontType);
|
||||||
},
|
},
|
||||||
|
|
||||||
_setSystemUIVisibility: function(visible) {
|
_setSystemUIVisibility(visible) {
|
||||||
this._mm.sendAsyncMessage("Reader:SystemUIVisibility", { visible: 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();
|
let url = this._getOriginalUrl();
|
||||||
this._showProgressDelayed();
|
this._showProgressDelayed();
|
||||||
|
|
||||||
let article;
|
let article;
|
||||||
if (this._articlePromise) {
|
if (this._articlePromise) {
|
||||||
article = yield this._articlePromise;
|
article = await this._articlePromise;
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
article = yield this._getArticle(url);
|
article = await this._getArticle(url);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e && e.newURL) {
|
if (e && e.newURL) {
|
||||||
let readerURL = "about:reader?url=" + encodeURIComponent(e.newURL);
|
let readerURL = "about:reader?url=" + encodeURIComponent(e.newURL);
|
||||||
|
|
@ -638,24 +670,26 @@ AboutReader.prototype = {
|
||||||
}
|
}
|
||||||
|
|
||||||
this._showContent(article);
|
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) => {
|
let handleFaviconReturn = (message) => {
|
||||||
this._mm.removeMessageListener("Reader:FaviconReturn", handleFaviconReturn);
|
this._mm.removeMessageListener("Reader:FaviconReturn", handleFaviconReturn);
|
||||||
this._loadFavicon(message.data.url, message.data.faviconUrl);
|
this._loadFavicon(message.data.url, message.data.faviconUrl);
|
||||||
|
|
@ -665,20 +699,20 @@ AboutReader.prototype = {
|
||||||
this._mm.sendAsyncMessage("Reader:FaviconRequest", { url: this._article.url });
|
this._mm.sendAsyncMessage("Reader:FaviconRequest", { url: this._article.url });
|
||||||
},
|
},
|
||||||
|
|
||||||
_loadFavicon: function(url, faviconUrl) {
|
_loadFavicon(url, faviconUrl) {
|
||||||
if (this._article.url !== url)
|
if (this._article.url !== url)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let doc = this._doc;
|
let doc = this._doc;
|
||||||
|
|
||||||
let link = doc.createElement('link');
|
let link = doc.createElement("link");
|
||||||
link.rel = 'shortcut icon';
|
link.rel = "shortcut icon";
|
||||||
link.href = faviconUrl;
|
link.href = faviconUrl;
|
||||||
|
|
||||||
doc.getElementsByTagName('head')[0].appendChild(link);
|
doc.getElementsByTagName("head")[0].appendChild(link);
|
||||||
},
|
},
|
||||||
|
|
||||||
_updateImageMargins: function() {
|
_updateImageMargins() {
|
||||||
let windowWidth = this._win.innerWidth;
|
let windowWidth = this._win.innerWidth;
|
||||||
let bodyWidth = this._doc.body.clientWidth;
|
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 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);
|
img.setAttribute("moz-reader-center", true);
|
||||||
} else {
|
} else {
|
||||||
img.removeAttribute("moz-reader-center");
|
img.removeAttribute("moz-reader-center");
|
||||||
|
|
@ -713,30 +747,32 @@ AboutReader.prototype = {
|
||||||
},
|
},
|
||||||
|
|
||||||
_maybeSetTextDirection: function Read_maybeSetTextDirection(article) {
|
_maybeSetTextDirection: function Read_maybeSetTextDirection(article) {
|
||||||
if (!article.dir)
|
if (article.dir) {
|
||||||
return;
|
// Set "dir" attribute on content
|
||||||
|
this._contentElement.setAttribute("dir", article.dir);
|
||||||
|
this._headerElement.setAttribute("dir", article.dir);
|
||||||
|
|
||||||
// Set "dir" attribute on content
|
// The native locale could be set differently than the article's text direction.
|
||||||
this._contentElement.setAttribute("dir", article.dir);
|
var localeDirection = Services.locale.isAppLocaleRTL ? "rtl" : "ltr";
|
||||||
this._headerElement.setAttribute("dir", article.dir);
|
this._readTimeElement.setAttribute("dir", localeDirection);
|
||||||
},
|
this._readTimeElement.style.textAlign = article.dir == "rtl" ? "right" : "left";
|
||||||
|
|
||||||
_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");
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_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._headerElement.style.display = "none";
|
||||||
this._contentElement.style.display = "none";
|
this._contentElement.style.display = "none";
|
||||||
|
|
||||||
|
|
@ -746,11 +782,16 @@ AboutReader.prototype = {
|
||||||
|
|
||||||
this._doc.title = errorMessage;
|
this._doc.title = errorMessage;
|
||||||
|
|
||||||
|
this._doc.documentElement.dataset.isError = true;
|
||||||
|
|
||||||
this._error = 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.
|
// This function is the JS version of Java's StringUtils.stripCommonSubdomains.
|
||||||
_stripHost: function(host) {
|
_stripHost(host) {
|
||||||
if (!host)
|
if (!host)
|
||||||
return host;
|
return host;
|
||||||
|
|
||||||
|
|
@ -766,17 +807,18 @@ AboutReader.prototype = {
|
||||||
return host.substring(start);
|
return host.substring(start);
|
||||||
},
|
},
|
||||||
|
|
||||||
_showContent: function(article) {
|
_showContent(article) {
|
||||||
this._messageElement.style.display = "none";
|
this._messageElement.style.display = "none";
|
||||||
|
|
||||||
this._article = article;
|
this._article = article;
|
||||||
|
|
||||||
this._domainElement.href = article.url;
|
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._domainElement.textContent = this._stripHost(articleUri.host);
|
||||||
this._creditsElement.textContent = article.byline;
|
this._creditsElement.textContent = article.byline;
|
||||||
|
|
||||||
this._titleElement.textContent = article.title;
|
this._titleElement.textContent = article.title;
|
||||||
|
this._readTimeElement.textContent = this._formatReadTime(article.readingTimeMinsSlow, article.readingTimeMinsFast);
|
||||||
this._doc.title = article.title;
|
this._doc.title = article.title;
|
||||||
|
|
||||||
this._headerElement.style.display = "block";
|
this._headerElement.style.display = "block";
|
||||||
|
|
@ -787,8 +829,8 @@ AboutReader.prototype = {
|
||||||
false, articleUri, this._contentElement);
|
false, articleUri, this._contentElement);
|
||||||
this._contentElement.innerHTML = "";
|
this._contentElement.innerHTML = "";
|
||||||
this._contentElement.appendChild(contentFragment);
|
this._contentElement.appendChild(contentFragment);
|
||||||
this._fixLocalLinks();
|
|
||||||
this._maybeSetTextDirection(article);
|
this._maybeSetTextDirection(article);
|
||||||
|
this._foundLanguage(article.language);
|
||||||
|
|
||||||
this._contentElement.style.display = "block";
|
this._contentElement.style.display = "block";
|
||||||
this._updateImageMargins();
|
this._updateImageMargins();
|
||||||
|
|
@ -804,13 +846,13 @@ AboutReader.prototype = {
|
||||||
new this._win.CustomEvent("AboutReaderContentReady", { bubbles: true, cancelable: false }));
|
new this._win.CustomEvent("AboutReaderContentReady", { bubbles: true, cancelable: false }));
|
||||||
},
|
},
|
||||||
|
|
||||||
_hideContent: function() {
|
_hideContent() {
|
||||||
this._headerElement.style.display = "none";
|
this._headerElement.style.display = "none";
|
||||||
this._contentElement.style.display = "none";
|
this._contentElement.style.display = "none";
|
||||||
},
|
},
|
||||||
|
|
||||||
_showProgressDelayed: function() {
|
_showProgressDelayed() {
|
||||||
this._win.setTimeout(function() {
|
this._win.setTimeout(() => {
|
||||||
// No need to show progress if the article has been loaded,
|
// No need to show progress if the article has been loaded,
|
||||||
// if the window has been unloaded, or if there was an error
|
// if the window has been unloaded, or if there was an error
|
||||||
// trying to load the article.
|
// trying to load the article.
|
||||||
|
|
@ -823,20 +865,20 @@ AboutReader.prototype = {
|
||||||
|
|
||||||
this._messageElement.textContent = gStrings.GetStringFromName("aboutReader.loading2");
|
this._messageElement.textContent = gStrings.GetStringFromName("aboutReader.loading2");
|
||||||
this._messageElement.style.display = "block";
|
this._messageElement.style.display = "block";
|
||||||
}.bind(this), 300);
|
}, 300);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the original article URL for this about:reader view.
|
* 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;
|
let url = win ? win.location.href : this._win.location.href;
|
||||||
return ReaderMode.getOriginalUrl(url) || url;
|
return ReaderMode.getOriginalUrl(url) || url;
|
||||||
},
|
},
|
||||||
|
|
||||||
_setupSegmentedButton: function(id, options, initialValue, callback) {
|
_setupSegmentedButton(id, options, initialValue, callback) {
|
||||||
let doc = this._doc;
|
let doc = this._doc;
|
||||||
let segmentedButton = doc.getElementById(id);
|
let segmentedButton = doc.getElementsByClassName(id)[0];
|
||||||
|
|
||||||
for (let i = 0; i < options.length; i++) {
|
for (let i = 0; i < options.length; i++) {
|
||||||
let option = options[i];
|
let option = options[i];
|
||||||
|
|
@ -867,10 +909,6 @@ AboutReader.prototype = {
|
||||||
|
|
||||||
aEvent.stopPropagation();
|
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;
|
let items = segmentedButton.children;
|
||||||
for (let j = items.length - 1; j >= 0; j--) {
|
for (let j = items.length - 1; j >= 0; j--) {
|
||||||
items[j].classList.remove("selected");
|
items[j].classList.remove("selected");
|
||||||
|
|
@ -878,19 +916,19 @@ AboutReader.prototype = {
|
||||||
|
|
||||||
item.classList.add("selected");
|
item.classList.add("selected");
|
||||||
callback(option.value);
|
callback(option.value);
|
||||||
}.bind(this), true);
|
}, true);
|
||||||
|
|
||||||
if (option.value === initialValue)
|
if (option.value === initialValue)
|
||||||
item.classList.add("selected");
|
item.classList.add("selected");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_setupButton: function(id, callback, titleEntity, textEntity) {
|
_setupButton(id, callback, titleEntity, textEntity) {
|
||||||
if (titleEntity) {
|
if (titleEntity) {
|
||||||
this._setButtonTip(id, titleEntity);
|
this._setButtonTip(id, titleEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
let button = this._doc.getElementById(id);
|
let button = this._doc.getElementsByClassName(id)[0];
|
||||||
if (textEntity) {
|
if (textEntity) {
|
||||||
button.textContent = gStrings.GetStringFromName(textEntity);
|
button.textContent = gStrings.GetStringFromName(textEntity);
|
||||||
}
|
}
|
||||||
|
|
@ -910,17 +948,17 @@ AboutReader.prototype = {
|
||||||
* and dynamically as button state changes.
|
* and dynamically as button state changes.
|
||||||
* @param Localizable string providing UI element usage tip.
|
* @param Localizable string providing UI element usage tip.
|
||||||
*/
|
*/
|
||||||
_setButtonTip: function(id, titleEntity) {
|
_setButtonTip(id, titleEntity) {
|
||||||
let button = this._doc.getElementById(id);
|
let button = this._doc.getElementsByClassName(id)[0];
|
||||||
button.setAttribute("title", gStrings.GetStringFromName(titleEntity));
|
button.setAttribute("title", gStrings.GetStringFromName(titleEntity));
|
||||||
},
|
},
|
||||||
|
|
||||||
_setupStyleDropdown: function() {
|
_setupStyleDropdown() {
|
||||||
let dropdownToggle = this._doc.querySelector("#style-dropdown .dropdown-toggle");
|
let dropdownToggle = this._doc.querySelector(".style-dropdown .dropdown-toggle");
|
||||||
dropdownToggle.setAttribute("title", gStrings.GetStringFromName("aboutReader.toolbar.typeControls"));
|
dropdownToggle.setAttribute("title", gStrings.GetStringFromName("aboutReader.toolbar.typeControls"));
|
||||||
},
|
},
|
||||||
|
|
||||||
_updatePopupPosition: function(dropdown) {
|
_updatePopupPosition(dropdown) {
|
||||||
let dropdownToggle = dropdown.querySelector(".dropdown-toggle");
|
let dropdownToggle = dropdown.querySelector(".dropdown-toggle");
|
||||||
let dropdownPopup = dropdown.querySelector(".dropdown-popup");
|
let dropdownPopup = dropdown.querySelector(".dropdown-popup");
|
||||||
|
|
||||||
|
|
@ -931,8 +969,8 @@ AboutReader.prototype = {
|
||||||
dropdownPopup.style.top = popupTop + "px";
|
dropdownPopup.style.top = popupTop + "px";
|
||||||
},
|
},
|
||||||
|
|
||||||
_toggleDropdownClicked: function(event) {
|
_toggleDropdownClicked(event) {
|
||||||
let dropdown = event.target.closest('.dropdown');
|
let dropdown = event.target.closest(".dropdown");
|
||||||
|
|
||||||
if (!dropdown)
|
if (!dropdown)
|
||||||
return;
|
return;
|
||||||
|
|
@ -952,7 +990,7 @@ AboutReader.prototype = {
|
||||||
/*
|
/*
|
||||||
* If the ReaderView banner font-dropdown is closed, open it.
|
* If the ReaderView banner font-dropdown is closed, open it.
|
||||||
*/
|
*/
|
||||||
_openDropdown: function(dropdown) {
|
_openDropdown(dropdown) {
|
||||||
if (dropdown.classList.contains("open")) {
|
if (dropdown.classList.contains("open")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -969,7 +1007,7 @@ AboutReader.prototype = {
|
||||||
* dropdowns because the page is scrolling, allow popups to stay open with
|
* dropdowns because the page is scrolling, allow popups to stay open with
|
||||||
* the keep-open class.
|
* the keep-open class.
|
||||||
*/
|
*/
|
||||||
_closeDropdowns: function(scrolling) {
|
_closeDropdowns(scrolling) {
|
||||||
let selector = ".dropdown.open";
|
let selector = ".dropdown.open";
|
||||||
if (scrolling) {
|
if (scrolling) {
|
||||||
selector += ":not(.keep-open)";
|
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
|
* 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() {
|
discardNextComment: function() {
|
||||||
if (this.match("--")) {
|
if (this.match("--")) {
|
||||||
this.discardTo("-->");
|
this.discardTo("-->");
|
||||||
|
|
@ -1131,11 +1091,7 @@
|
||||||
|
|
||||||
// If this isn't a void Element, read its child nodes
|
// If this isn't a void Element, read its child nodes
|
||||||
if (!closed) {
|
if (!closed) {
|
||||||
if (localName == "script") {
|
this.readChildren(node);
|
||||||
this.readScript(node);
|
|
||||||
} else {
|
|
||||||
this.readChildren(node);
|
|
||||||
}
|
|
||||||
var closingTag = "</" + localName + ">";
|
var closingTag = "</" + localName + ">";
|
||||||
if (!this.match(closingTag)) {
|
if (!this.match(closingTag)) {
|
||||||
this.error("expected '" + closingTag + "' and got " + this.html.substr(this.currentChar, closingTag.length));
|
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;
|
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||||
|
|
||||||
// Constants for telemetry.
|
// Class names to preserve in the readerized output. We preserve these class
|
||||||
const DOWNLOAD_SUCCESS = 0;
|
// names so that rules in aboutReader.css can match them.
|
||||||
const DOWNLOAD_ERROR_XHR = 1;
|
const CLASSES_TO_PRESERVE = [
|
||||||
const DOWNLOAD_ERROR_NO_DOC = 2;
|
"caption",
|
||||||
|
"hidden",
|
||||||
const PARSE_SUCCESS = 0;
|
"invisble",
|
||||||
const PARSE_ERROR_TOO_MANY_ELEMENTS = 1;
|
"sr-only",
|
||||||
const PARSE_ERROR_WORKER = 2;
|
"visually-hidden",
|
||||||
const PARSE_ERROR_NO_ARTICLE = 3;
|
"visuallyhidden",
|
||||||
|
"wp-caption",
|
||||||
|
"wp-caption-text",
|
||||||
|
];
|
||||||
|
|
||||||
Cu.import("resource://gre/modules/Services.jsm");
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
|
@ -24,17 +27,15 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
Cu.importGlobalProperties(["XMLHttpRequest"]);
|
Cu.importGlobalProperties(["XMLHttpRequest"]);
|
||||||
|
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils", "resource://services-common/utils.js");
|
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, "OS", "resource://gre/modules/osfile.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "ReaderWorker", "resource://gre/modules/reader/ReaderWorker.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() {
|
XPCOMUtils.defineLazyGetter(this, "Readability", function() {
|
||||||
let scope = {};
|
let scope = {};
|
||||||
scope.dump = this.dump;
|
scope.dump = this.dump;
|
||||||
Services.scriptloader.loadSubScript("resource://gre/modules/reader/Readability.js", scope);
|
Services.scriptloader.loadSubScript("resource://gre/modules/reader/Readability.js", scope);
|
||||||
return scope["Readability"];
|
return scope.Readability;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ReaderMode = {
|
this.ReaderMode = {
|
||||||
|
|
@ -61,21 +62,13 @@ this.ReaderMode = {
|
||||||
return this.isEnabledForParseOnLoad = this._getStateForParseOnLoad();
|
return this.isEnabledForParseOnLoad = this._getStateForParseOnLoad();
|
||||||
},
|
},
|
||||||
|
|
||||||
get isOnLowMemoryPlatform() {
|
_getStateForParseOnLoad() {
|
||||||
let memory = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory);
|
|
||||||
delete this.isOnLowMemoryPlatform;
|
|
||||||
return this.isOnLowMemoryPlatform = memory.isLowMemoryPlatform();
|
|
||||||
},
|
|
||||||
|
|
||||||
_getStateForParseOnLoad: function () {
|
|
||||||
let isEnabled = Services.prefs.getBoolPref("reader.parse-on-load.enabled");
|
let isEnabled = Services.prefs.getBoolPref("reader.parse-on-load.enabled");
|
||||||
let isForceEnabled = Services.prefs.getBoolPref("reader.parse-on-load.force-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.
|
return isForceEnabled || isEnabled;
|
||||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=792603 for details.
|
|
||||||
return isForceEnabled || (isEnabled && !this.isOnLowMemoryPlatform);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
observe: function(aMessage, aTopic, aData) {
|
observe(aMessage, aTopic, aData) {
|
||||||
switch (aTopic) {
|
switch (aTopic) {
|
||||||
case "nsPref:changed":
|
case "nsPref:changed":
|
||||||
if (aData.startsWith("reader.parse-on-load.")) {
|
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,
|
* Enter the reader mode by going forward one step in history if applicable,
|
||||||
* if not, append the about:reader page in the history instead.
|
* 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 url = win.document.location.href;
|
||||||
let readerURL = "about:reader?url=" + encodeURIComponent(url);
|
let readerURL = "about:reader?url=" + encodeURIComponent(url);
|
||||||
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
|
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,
|
* Exit the reader mode by going back one step in history if applicable,
|
||||||
* if not, append the original page in the history instead.
|
* if not, append the original page in the history instead.
|
||||||
*/
|
*/
|
||||||
leaveReaderMode: function(docShell, win) {
|
leaveReaderMode(docShell, win) {
|
||||||
let url = win.document.location.href;
|
let url = win.document.location.href;
|
||||||
let originalURL = this.getOriginalUrl(url);
|
let originalURL = this.getOriginalUrl(url);
|
||||||
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
|
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
|
* @return The original URL for the article, or null if we did not find
|
||||||
* a properly formatted about:reader URL.
|
* a properly formatted about:reader URL.
|
||||||
*/
|
*/
|
||||||
getOriginalUrl: function(url) {
|
getOriginalUrl(url) {
|
||||||
if (!url.startsWith("about:reader?")) {
|
if (!url.startsWith("about:reader?")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let outerHash = "";
|
let outerHash = "";
|
||||||
try {
|
try {
|
||||||
let uriObj = Services.io.newURI(url, null, null);
|
let uriObj = Services.io.newURI(url);
|
||||||
url = uriObj.specIgnoringRef;
|
url = uriObj.specIgnoringRef;
|
||||||
outerHash = uriObj.ref;
|
outerHash = uriObj.ref;
|
||||||
} catch (ex) { /* ignore, use the raw string */ }
|
} catch (ex) { /* ignore, use the raw string */ }
|
||||||
|
|
@ -155,27 +148,45 @@ this.ReaderMode = {
|
||||||
let originalUrl = searchParams.get("url");
|
let originalUrl = searchParams.get("url");
|
||||||
if (outerHash) {
|
if (outerHash) {
|
||||||
try {
|
try {
|
||||||
let uriObj = Services.io.newURI(originalUrl, null, null);
|
let uriObj = Services.io.newURI(originalUrl);
|
||||||
uriObj = Services.io.newURI('#' + outerHash, null, uriObj);
|
uriObj = Services.io.newURI("#" + outerHash, null, uriObj);
|
||||||
originalUrl = uriObj.spec;
|
originalUrl = uriObj.spec;
|
||||||
} catch (ex) {}
|
} catch (ex) {}
|
||||||
}
|
}
|
||||||
return originalUrl;
|
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.
|
* Decides whether or not a document is reader-able without parsing the whole thing.
|
||||||
*
|
*
|
||||||
* @param doc A document to parse.
|
* @param doc A document to parse.
|
||||||
* @return boolean Whether or not we should show the reader mode button.
|
* @return boolean Whether or not we should show the reader mode button.
|
||||||
*/
|
*/
|
||||||
isProbablyReaderable: function(doc) {
|
isProbablyReaderable(doc) {
|
||||||
// Only care about 'real' HTML documents:
|
// Only care about 'real' HTML documents:
|
||||||
if (doc.mozSyntheticDocument || !(doc instanceof doc.defaultView.HTMLDocument)) {
|
if (doc.mozSyntheticDocument || !(doc instanceof doc.defaultView.HTMLDocument)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let uri = Services.io.newURI(doc.location.href, null, null);
|
let uri = Services.io.newURI(doc.location.href);
|
||||||
if (!this._shouldCheckUri(uri)) {
|
if (!this._shouldCheckUri(uri)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -187,12 +198,12 @@ this.ReaderMode = {
|
||||||
return new Readability(uri, doc).isProbablyReaderable(this.isNodeVisible.bind(this, utils));
|
return new Readability(uri, doc).isProbablyReaderable(this.isNodeVisible.bind(this, utils));
|
||||||
},
|
},
|
||||||
|
|
||||||
isNodeVisible: function(utils, node) {
|
isNodeVisible(utils, node) {
|
||||||
let bounds = utils.getBoundsWithoutFlushing(node);
|
let bounds = utils.getBoundsWithoutFlushing(node);
|
||||||
return bounds.height > 0 && bounds.width > 0;
|
return bounds.height > 0 && bounds.width > 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
getUtilsForWin: function(win) {
|
getUtilsForWin(win) {
|
||||||
return win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
|
return win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -204,16 +215,14 @@ this.ReaderMode = {
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
* @resolves JS object representing the article, or null if no article is found.
|
* @resolves JS object representing the article, or null if no article is found.
|
||||||
*/
|
*/
|
||||||
parseDocument: Task.async(function* (doc) {
|
parseDocument(doc) {
|
||||||
let documentURI = Services.io.newURI(doc.documentURI, null, null);
|
if (!this._shouldCheckUri(doc.documentURIObject) || !this._shouldCheckUri(doc.baseURIObject, true)) {
|
||||||
let baseURI = Services.io.newURI(doc.baseURI, null, null);
|
|
||||||
if (!this._shouldCheckUri(documentURI) || !this._shouldCheckUri(baseURI, true)) {
|
|
||||||
this.log("Reader mode disabled for URI");
|
this.log("Reader mode disabled for URI");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return yield this._readerParse(baseURI, doc);
|
return this._readerParse(doc);
|
||||||
}),
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads and parses a document from a URL.
|
* Downloads and parses a document from a URL.
|
||||||
|
|
@ -222,19 +231,28 @@ this.ReaderMode = {
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
* @resolves JS object representing the article, or null if no article is found.
|
* @resolves JS object representing the article, or null if no article is found.
|
||||||
*/
|
*/
|
||||||
downloadAndParseDocument: Task.async(function* (url) {
|
async downloadAndParseDocument(url) {
|
||||||
let doc = yield this._downloadDocument(url);
|
let doc = await this._downloadDocument(url);
|
||||||
let uri = Services.io.newURI(doc.baseURI, null, null);
|
if (!doc) {
|
||||||
if (!this._shouldCheckUri(uri, true)) {
|
return null;
|
||||||
|
}
|
||||||
|
if (!this._shouldCheckUri(doc.documentURIObject) || !this._shouldCheckUri(doc.baseURIObject, true)) {
|
||||||
this.log("Reader mode disabled for URI");
|
this.log("Reader mode disabled for URI");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return yield this._readerParse(uri, doc);
|
return await this._readerParse(doc);
|
||||||
}),
|
},
|
||||||
|
|
||||||
_downloadDocument: function (url) {
|
_downloadDocument(url) {
|
||||||
let histogram = Services.telemetry.getHistogramById("READER_MODE_DOWNLOAD_RESULT");
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
let xhr = new XMLHttpRequest();
|
let xhr = new XMLHttpRequest();
|
||||||
xhr.open("GET", url, true);
|
xhr.open("GET", url, true);
|
||||||
|
|
@ -243,14 +261,12 @@ this.ReaderMode = {
|
||||||
xhr.onload = evt => {
|
xhr.onload = evt => {
|
||||||
if (xhr.status !== 200) {
|
if (xhr.status !== 200) {
|
||||||
reject("Reader mode XHR failed with status: " + xhr.status);
|
reject("Reader mode XHR failed with status: " + xhr.status);
|
||||||
histogram.add(DOWNLOAD_ERROR_XHR);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let doc = xhr.responseXML;
|
let doc = xhr.responseXML;
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
reject("Reader mode XHR didn't return a document");
|
reject("Reader mode XHR didn't return a document");
|
||||||
histogram.add(DOWNLOAD_ERROR_NO_DOC);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -261,7 +277,7 @@ this.ReaderMode = {
|
||||||
if (content) {
|
if (content) {
|
||||||
let urlIndex = content.toUpperCase().indexOf("URL=");
|
let urlIndex = content.toUpperCase().indexOf("URL=");
|
||||||
if (urlIndex > -1) {
|
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 newURI = Services.io.newURI(content.substring(urlIndex + 4), null, baseURI);
|
||||||
let newURL = newURI.spec;
|
let newURL = newURI.spec;
|
||||||
let ssm = Services.scriptSecurityManager;
|
let ssm = Services.scriptSecurityManager;
|
||||||
|
|
@ -290,10 +306,10 @@ this.ReaderMode = {
|
||||||
// Convert these to real URIs to make sure the escaping (or lack
|
// Convert these to real URIs to make sure the escaping (or lack
|
||||||
// thereof) is identical:
|
// thereof) is identical:
|
||||||
try {
|
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 */ }
|
} catch (ex) { /* Ignore errors - we'll use what we had before */ }
|
||||||
try {
|
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 */ }
|
} catch (ex) { /* Ignore errors - we'll use what we had before */ }
|
||||||
|
|
||||||
if (responseURL != givenURL) {
|
if (responseURL != givenURL) {
|
||||||
|
|
@ -303,7 +319,6 @@ this.ReaderMode = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resolve(doc);
|
resolve(doc);
|
||||||
histogram.add(DOWNLOAD_SUCCESS);
|
|
||||||
};
|
};
|
||||||
xhr.send();
|
xhr.send();
|
||||||
});
|
});
|
||||||
|
|
@ -318,17 +333,17 @@ this.ReaderMode = {
|
||||||
* @resolves JS object representing the article, or null if no article is found.
|
* @resolves JS object representing the article, or null if no article is found.
|
||||||
* @rejects OS.File.Error
|
* @rejects OS.File.Error
|
||||||
*/
|
*/
|
||||||
getArticleFromCache: Task.async(function* (url) {
|
async getArticleFromCache(url) {
|
||||||
let path = this._toHashedPath(url);
|
let path = this._toHashedPath(url);
|
||||||
try {
|
try {
|
||||||
let array = yield OS.File.read(path);
|
let array = await OS.File.read(path);
|
||||||
return JSON.parse(new TextDecoder().decode(array));
|
return JSON.parse(new TextDecoder().decode(array));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!(e instanceof OS.File.Error) || !e.becauseNoSuchFile)
|
if (!(e instanceof OS.File.Error) || !e.becauseNoSuchFile)
|
||||||
throw e;
|
throw e;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores an article in the cache.
|
* Stores an article in the cache.
|
||||||
|
|
@ -338,14 +353,14 @@ this.ReaderMode = {
|
||||||
* @resolves When the article is stored.
|
* @resolves When the article is stored.
|
||||||
* @rejects OS.File.Error
|
* @rejects OS.File.Error
|
||||||
*/
|
*/
|
||||||
storeArticleInCache: Task.async(function* (article) {
|
async storeArticleInCache(article) {
|
||||||
let array = new TextEncoder().encode(JSON.stringify(article));
|
let array = new TextEncoder().encode(JSON.stringify(article));
|
||||||
let path = this._toHashedPath(article.url);
|
let path = this._toHashedPath(article.url);
|
||||||
yield this._ensureCacheDir();
|
await this._ensureCacheDir();
|
||||||
return OS.File.writeAtomic(path, array, { tmpPath: path + ".tmp" })
|
return OS.File.writeAtomic(path, array, { tmpPath: path + ".tmp" })
|
||||||
.then(success => {
|
.then(success => {
|
||||||
OS.File.stat(path).then(info => {
|
OS.File.stat(path).then(info => {
|
||||||
return Messaging.sendRequest({
|
return EventDispatcher.instance.sendRequest({
|
||||||
type: "Reader:AddedToCache",
|
type: "Reader:AddedToCache",
|
||||||
url: article.url,
|
url: article.url,
|
||||||
size: info.size,
|
size: info.size,
|
||||||
|
|
@ -353,7 +368,7 @@ this.ReaderMode = {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}),
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes an article from the cache given an article URI.
|
* Removes an article from the cache given an article URI.
|
||||||
|
|
@ -363,26 +378,29 @@ this.ReaderMode = {
|
||||||
* @resolves When the article is removed.
|
* @resolves When the article is removed.
|
||||||
* @rejects OS.File.Error
|
* @rejects OS.File.Error
|
||||||
*/
|
*/
|
||||||
removeArticleFromCache: Task.async(function* (url) {
|
async removeArticleFromCache(url) {
|
||||||
let path = this._toHashedPath(url);
|
let path = this._toHashedPath(url);
|
||||||
yield OS.File.remove(path);
|
await OS.File.remove(path);
|
||||||
}),
|
},
|
||||||
|
|
||||||
log: function(msg) {
|
log(msg) {
|
||||||
if (this.DEBUG)
|
if (this.DEBUG)
|
||||||
dump("Reader: " + msg);
|
dump("Reader: " + msg);
|
||||||
},
|
},
|
||||||
|
|
||||||
_blockedHosts: [
|
_blockedHosts: [
|
||||||
"mail.google.com",
|
"amazon.com",
|
||||||
|
"basilisk-browser.org",
|
||||||
"github.com",
|
"github.com",
|
||||||
|
"mail.google.com",
|
||||||
|
"palemoon.org",
|
||||||
"pinterest.com",
|
"pinterest.com",
|
||||||
"reddit.com",
|
"reddit.com",
|
||||||
"twitter.com",
|
"twitter.com",
|
||||||
"youtube.com",
|
"youtube.com",
|
||||||
],
|
],
|
||||||
|
|
||||||
_shouldCheckUri: function (uri, isBaseUri = false) {
|
_shouldCheckUri(uri, isBaseUri = false) {
|
||||||
if (!(uri.schemeIs("http") || uri.schemeIs("https"))) {
|
if (!(uri.schemeIs("http") || uri.schemeIs("https"))) {
|
||||||
this.log("Not parsing URI scheme: " + uri.scheme);
|
this.log("Not parsing URI scheme: " + uri.scheme);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -412,59 +430,77 @@ this.ReaderMode = {
|
||||||
* Attempts to parse a document into an article. Heavy lifting happens
|
* Attempts to parse a document into an article. Heavy lifting happens
|
||||||
* in readerWorker.js.
|
* in readerWorker.js.
|
||||||
*
|
*
|
||||||
* @param uri The base URI of the article.
|
|
||||||
* @param doc The document to parse.
|
* @param doc The document to parse.
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
* @resolves JS object representing the article, or null if no article is found.
|
* @resolves JS object representing the article, or null if no article is found.
|
||||||
*/
|
*/
|
||||||
_readerParse: Task.async(function* (uri, doc) {
|
async _readerParse(doc) {
|
||||||
let histogram = Services.telemetry.getHistogramById("READER_MODE_PARSE_RESULT");
|
|
||||||
if (this.parseNodeLimit) {
|
if (this.parseNodeLimit) {
|
||||||
let numTags = doc.getElementsByTagName("*").length;
|
let numTags = doc.getElementsByTagName("*").length;
|
||||||
if (numTags > this.parseNodeLimit) {
|
if (numTags > this.parseNodeLimit) {
|
||||||
this.log("Aborting parse for " + uri.spec + "; " + numTags + " elements found");
|
this.log("Aborting parse for " + doc.baseURIObject.spec + "; " + numTags + " elements found");
|
||||||
histogram.add(PARSE_ERROR_TOO_MANY_ELEMENTS);
|
|
||||||
return null;
|
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 = {
|
let uriParam = {
|
||||||
spec: uri.spec,
|
spec: doc.baseURIObject.spec,
|
||||||
host: uri.host,
|
host: doc.baseURIObject.host,
|
||||||
prePath: uri.prePath,
|
prePath: doc.baseURIObject.prePath,
|
||||||
scheme: uri.scheme,
|
scheme: doc.baseURIObject.scheme,
|
||||||
pathBase: Services.io.newURI(".", null, uri).spec
|
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"].
|
let serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"].
|
||||||
createInstance(Ci.nsIDOMSerializer);
|
createInstance(Ci.nsIDOMSerializer);
|
||||||
let serializedDoc = serializer.serializeToString(doc);
|
let serializedDoc = serializer.serializeToString(doc);
|
||||||
|
|
||||||
|
let options = {
|
||||||
|
classesToPreserve: CLASSES_TO_PRESERVE,
|
||||||
|
};
|
||||||
|
|
||||||
let article = null;
|
let article = null;
|
||||||
try {
|
try {
|
||||||
article = yield ReaderWorker.post("parseDocument", [uriParam, serializedDoc]);
|
article = await ReaderWorker.post("parseDocument", [uriParam, serializedDoc, options]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Cu.reportError("Error in ReaderWorker: " + 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) {
|
if (!article) {
|
||||||
this.log("Worker did not return an article");
|
this.log("Worker did not return an article");
|
||||||
histogram.add(PARSE_ERROR_NO_ARTICLE);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Readability returns a URI object, but we only care about the URL.
|
// Readability returns a URI object based on the baseURI, but we only care
|
||||||
article.url = article.uri.spec;
|
// 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;
|
delete article.uri;
|
||||||
|
|
||||||
let flags = Ci.nsIDocumentEncoder.OutputSelectionOnly | Ci.nsIDocumentEncoder.OutputAbsoluteLinks;
|
let flags = Ci.nsIDocumentEncoder.OutputSelectionOnly | Ci.nsIDocumentEncoder.OutputAbsoluteLinks;
|
||||||
article.title = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils)
|
article.title = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils)
|
||||||
.convertToPlainText(article.title, flags, 0);
|
.convertToPlainText(article.title, flags, 0);
|
||||||
|
|
||||||
histogram.add(PARSE_SUCCESS);
|
await this._assignLanguage(article, langAttributes);
|
||||||
|
this._maybeAssignTextDirection(article);
|
||||||
|
|
||||||
|
this._assignReadTime(article);
|
||||||
|
|
||||||
return article;
|
return article;
|
||||||
}),
|
},
|
||||||
|
|
||||||
get _cryptoHash() {
|
get _cryptoHash() {
|
||||||
delete this._cryptoHash;
|
delete this._cryptoHash;
|
||||||
|
|
@ -485,7 +521,7 @@ this.ReaderMode = {
|
||||||
* @param url The article URL. This should have referrers removed.
|
* @param url The article URL. This should have referrers removed.
|
||||||
* @return The file path to the cached article.
|
* @return The file path to the cached article.
|
||||||
*/
|
*/
|
||||||
_toHashedPath: function (url) {
|
_toHashedPath(url) {
|
||||||
let value = this._unicodeConverter.convertToByteArray(url);
|
let value = this._unicodeConverter.convertToByteArray(url);
|
||||||
this._cryptoHash.init(this._cryptoHash.MD5);
|
this._cryptoHash.init(this._cryptoHash.MD5);
|
||||||
this._cryptoHash.update(value, value.length);
|
this._cryptoHash.update(value, value.length);
|
||||||
|
|
@ -502,7 +538,7 @@ this.ReaderMode = {
|
||||||
* @resolves When the cache directory exists.
|
* @resolves When the cache directory exists.
|
||||||
* @rejects OS.File.Error
|
* @rejects OS.File.Error
|
||||||
*/
|
*/
|
||||||
_ensureCacheDir: function () {
|
_ensureCacheDir() {
|
||||||
let dir = OS.Path.join(OS.Constants.Path.profileDir, "readercache");
|
let dir = OS.Path.join(OS.Constants.Path.profileDir, "readercache");
|
||||||
return OS.File.exists(dir).then(exists => {
|
return OS.File.exists(dir).then(exists => {
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
|
|
@ -510,5 +546,107 @@ this.ReaderMode = {
|
||||||
}
|
}
|
||||||
return undefined;
|
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,
|
* 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/. */
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
/* eslint-env mozilla/chrome-worker */
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -40,11 +42,12 @@ var Agent = {
|
||||||
*
|
*
|
||||||
* @param {object} uri URI data for the document.
|
* @param {object} uri URI data for the document.
|
||||||
* @param {string} serializedDoc The serialized document.
|
* @param {string} serializedDoc The serialized document.
|
||||||
|
* @param {object} options Options object to pass to Readability.
|
||||||
*
|
*
|
||||||
* @return {object} Article object returned from Readability.
|
* @return {object} Article object returned from Readability.
|
||||||
*/
|
*/
|
||||||
parseDocument: function (uri, serializedDoc) {
|
parseDocument(uri, serializedDoc, options) {
|
||||||
let doc = new JSDOMParser().parse(serializedDoc);
|
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"/>
|
<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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="container" class="container">
|
<div class="container">
|
||||||
<div id="reader-header" class="header">
|
<div class="header reader-header">
|
||||||
<style scoped>
|
<a class="domain reader-domain"></a>
|
||||||
@import url("chrome://global/skin/aboutReaderControls.css");
|
|
||||||
</style>
|
|
||||||
<a id="reader-domain" class="domain"></a>
|
|
||||||
<div class="domain-border"></div>
|
<div class="domain-border"></div>
|
||||||
<h1 id="reader-title"></h1>
|
<h1 class="reader-title"></h1>
|
||||||
<div id="reader-credits" class="credits"></div>
|
<div class="credits reader-credits"></div>
|
||||||
|
<div class="meta-data">
|
||||||
|
<div class="reader-estimated-time"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<style scoped>
|
<div class="moz-reader-content"></div>
|
||||||
@import url("chrome://global/skin/aboutReaderContent.css");
|
|
||||||
</style>
|
|
||||||
<div id="moz-reader-content"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<style scoped>
|
<div class="reader-message"></div>
|
||||||
@import url("chrome://global/skin/aboutReaderControls.css");
|
|
||||||
</style>
|
|
||||||
<div id="reader-message"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul id="reader-toolbar" class="toolbar">
|
<ul class="toolbar reader-toolbar">
|
||||||
<style scoped>
|
<li><button class="button close-button"/></li>
|
||||||
@import url("chrome://global/skin/aboutReaderControls.css");
|
<ul class="dropdown style-dropdown">
|
||||||
</style>
|
|
||||||
<li><button id="close-button" class="button close-button"/></li>
|
|
||||||
<ul id="style-dropdown" class="dropdown">
|
|
||||||
<li><button class="dropdown-toggle button style-button"/></li>
|
<li><button class="dropdown-toggle button style-button"/></li>
|
||||||
<li id="reader-popup" class="dropdown-popup">
|
<li class="dropdown-popup">
|
||||||
<div id="font-type-buttons"></div>
|
<div class="font-type-buttons"></div>
|
||||||
<hr></hr>
|
<hr>
|
||||||
<div id="font-size-buttons">
|
<div class="font-size-buttons">
|
||||||
<button id="font-size-minus" class="minus-button"/>
|
<button class="minus-button"/>
|
||||||
<button id="font-size-sample"/>
|
<button class="font-size-sample"/>
|
||||||
<button id="font-size-plus" class="plus-button"/>
|
<button class="plus-button"/>
|
||||||
</div>
|
</div>
|
||||||
<hr></hr>
|
<hr>
|
||||||
<div id="content-width-buttons">
|
<div class="content-width-buttons">
|
||||||
<button id="content-width-minus" class="content-width-minus-button"/>
|
<button class="content-width-minus-button"/>
|
||||||
<button id="content-width-plus" class="content-width-plus-button"/>
|
<button class="content-width-plus-button"/>
|
||||||
</div>
|
</div>
|
||||||
<hr></hr>
|
<hr>
|
||||||
<div id="line-height-buttons">
|
<div class="line-height-buttons">
|
||||||
<button id="line-height-minus" class="line-height-minus-button"/>
|
<button class="line-height-minus-button"/>
|
||||||
<button id="line-height-plus" class="line-height-plus-button"/>
|
<button class="line-height-plus-button"/>
|
||||||
</div>
|
</div>
|
||||||
<hr></hr>
|
<hr>
|
||||||
<div id="color-scheme-buttons"></div>
|
<div class="color-scheme-buttons"></div>
|
||||||
<div class="dropdown-arrow"/>
|
<div class="dropdown-arrow"/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,6 @@
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
window.addEventListener("DOMContentLoaded", function () {
|
window.addEventListener("DOMContentLoaded", function() {
|
||||||
document.dispatchEvent(new CustomEvent("AboutReaderContentLoaded", { bubbles: true }));
|
document.dispatchEvent(new CustomEvent("AboutReaderContentLoaded", { bubbles: true }));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -594,12 +594,6 @@ var Printing = {
|
||||||
contentElement.setAttribute("class", "content");
|
contentElement.setAttribute("class", "content");
|
||||||
containerElement.appendChild(contentElement);
|
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
|
// Jam the article's content into content div
|
||||||
let readerContent = content.document.createElement("div");
|
let readerContent = content.document.createElement("div");
|
||||||
readerContent.setAttribute("id", "moz-reader-content");
|
readerContent.setAttribute("id", "moz-reader-content");
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,22 @@ aboutReader.colorScheme.dark=Dark
|
||||||
aboutReader.colorScheme.sepia=Sepia
|
aboutReader.colorScheme.sepia=Sepia
|
||||||
aboutReader.colorScheme.auto=Auto
|
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):
|
# LOCALIZATION NOTE (aboutReader.fontType.serif, aboutReader.fontType.sans-serif):
|
||||||
# These are the styles of typeface that are options in the reader view controls.
|
# These are the styles of typeface that are options in the reader view controls.
|
||||||
aboutReader.fontType.serif=Serif
|
aboutReader.fontType.serif=Serif
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,13 @@
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
* 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/. */
|
* 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 {
|
body {
|
||||||
padding: 64px 51px;
|
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 {
|
body.loaded {
|
||||||
transition: color 0.4s, background-color 0.4s;
|
transition: color 0.4s, background-color 0.4s;
|
||||||
}
|
}
|
||||||
|
|
@ -56,80 +46,80 @@ body.serif .remove-button {
|
||||||
font-family: Georgia, "Times New Roman", serif;
|
font-family: Georgia, "Times New Roman", serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
#container {
|
.container {
|
||||||
max-width: 30em;
|
max-width: 30em;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#container.font-size1 {
|
.container.font-size1 {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#container.font-size2 {
|
.container.font-size2 {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#container.font-size3 {
|
.container.font-size3 {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#container.font-size4 {
|
.container.font-size4 {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#container.font-size5 {
|
.container.font-size5 {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#container.font-size6 {
|
.container.font-size6 {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#container.font-size7 {
|
.container.font-size7 {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#container.font-size8 {
|
.container.font-size8 {
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#container.font-size9 {
|
.container.font-size9 {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#container.content-width1 {
|
.container.content-width1 {
|
||||||
max-width: 20em;
|
max-width: 20em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#container.content-width2 {
|
.container.content-width2 {
|
||||||
max-width: 25em;
|
max-width: 25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#container.content-width3 {
|
.container.content-width3 {
|
||||||
max-width: 30em;
|
max-width: 30em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#container.content-width4 {
|
.container.content-width4 {
|
||||||
max-width: 35em;
|
max-width: 35em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#container.content-width5 {
|
.container.content-width5 {
|
||||||
max-width: 40em;
|
max-width: 40em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#container.content-width6 {
|
.container.content-width6 {
|
||||||
max-width: 45em;
|
max-width: 45em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#container.content-width7 {
|
.container.content-width7 {
|
||||||
max-width: 50em;
|
max-width: 50em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#container.content-width8 {
|
.container.content-width8 {
|
||||||
max-width: 55em;
|
max-width: 55em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#container.content-width9 {
|
.container.content-width9 {
|
||||||
max-width: 60em;
|
max-width: 60em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -176,3 +166,575 @@ body:not(.loaded) .toolbar:-moz-locale-dir(ltr) {
|
||||||
body:not(.loaded) .toolbar:-moz-locale-dir(rtl) {
|
body:not(.loaded) .toolbar:-moz-locale-dir(rtl) {
|
||||||
transform: translateX(100%);
|
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/aboutCache.css (../../shared/aboutCache.css)
|
||||||
skin/classic/global/aboutCacheEntry.css (../../shared/aboutCacheEntry.css)
|
skin/classic/global/aboutCacheEntry.css (../../shared/aboutCacheEntry.css)
|
||||||
skin/classic/global/aboutMemory.css (../../shared/aboutMemory.css)
|
skin/classic/global/aboutMemory.css (../../shared/aboutMemory.css)
|
||||||
skin/classic/global/aboutReader.css (../../shared/aboutReader.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/aboutSupport.css (../../shared/aboutSupport.css)
|
skin/classic/global/aboutSupport.css (../../shared/aboutSupport.css)
|
||||||
skin/classic/global/appPicker.css (../../shared/appPicker.css)
|
skin/classic/global/appPicker.css (../../shared/appPicker.css)
|
||||||
skin/classic/global/config.css (../../shared/config.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/icons/blocked.svg (../../shared/incontent-icons/blocked.svg)
|
||||||
skin/classic/global/alerts/alert-common.css (../../shared/alert-common.css)
|
skin/classic/global/alerts/alert-common.css (../../shared/alert-common.css)
|
||||||
skin/classic/global/narrate.css (../../shared/narrate.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/arrow.svg (../../shared/narrate/arrow.svg)
|
||||||
skin/classic/global/narrate/back.svg (../../shared/narrate/back.svg)
|
skin/classic/global/narrate/back.svg (../../shared/narrate/back.svg)
|
||||||
skin/classic/global/narrate/fast.svg (../../shared/narrate/fast.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 {
|
.narrating {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
@ -22,7 +25,7 @@ body.dark .narrating {
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
border-bottom-style: solid;
|
border-bottom-style: solid;
|
||||||
border-bottom-width: 7px;
|
border-bottom-width: 7px;
|
||||||
transition: left 0.1s ease;
|
transition: left 0.1s ease, width 0.1s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.narrating > .narrate-word-highlight {
|
.narrating > .narrate-word-highlight {
|
||||||
|
|
@ -44,3 +47,193 @@ body.sepia .narrate-word-highlight {
|
||||||
body.dark .narrate-word-highlight {
|
body.dark .narrate-word-highlight {
|
||||||
border-bottom-color: #6f6f6f;
|
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"?>
|
<?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
|
<!-- 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
|
- 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/. -->
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
|
||||||
<svg version="1.1"
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
width="42"
|
width="42"
|
||||||
height="16"
|
height="16"
|
||||||
viewBox="0 0 42 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"?>
|
<?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
|
<!-- 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
|
- 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/. -->
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
|
||||||
<svg version="1.1"
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
width="44"
|
width="44"
|
||||||
height="16"
|
height="16"
|
||||||
viewBox="0 0 44 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"?>
|
<?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
|
<!-- 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
|
- 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/. -->
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
|
||||||
<svg version="1.1"
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
width="38"
|
width="38"
|
||||||
height="14"
|
height="14"
|
||||||
viewBox="0 0 38 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"?>
|
<?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
|
<!-- 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
|
- 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/. -->
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
|
||||||
<svg version="1.1"
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
width="38"
|
width="38"
|
||||||
height="24"
|
height="24"
|
||||||
viewBox="0 0 38 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
|
<!-- 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
|
- 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/. -->
|
- 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">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 24">
|
||||||
<polygon opacity="0.15" points="16.583,0.015 16.569,0 4.583,12 16.569,24 16.583,23.985"/>
|
<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"/>
|
||||||
<polygon fill="#fbfbfb" points="16.575,1.021 16.561,1.008 5.583,12 16.577,23.008 16.591,22.994 "/>
|
<polyline points="16.63 1.51 6.08 12.01 16.63 22.5" fill="#fbfbfb"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 513 B After Width: | Height: | Size: 486 B |