├── .gitignore ├── LICENSE.md ├── README.md ├── TODO.md └── auto-reader-view ├── background.js ├── data └── panel.js ├── icons ├── miu-book-icon-16.png ├── miu-book-icon-32.png ├── miu-book-icon-48.png └── miu-book-icon-64.png ├── index.js ├── lib └── url-utils.js ├── manifest.json ├── options ├── options.html └── options.js ├── popup ├── panel.css ├── panel.html └── panel.js └── test └── test-url-utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.xpi 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Patrick Marchwiak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Auto Reader View 2 | A Firefox add-on for loading Reader View automatically on chosen websites. 3 | 4 | Get it here: https://addons.mozilla.org/en-US/firefox/addon/auto-reader-view/ 5 | 6 | ## Usage 7 | Firefox's Reader View feature strips away clutter from web pages for improved readabilty. In order to switch to this view, users must manually click the Reader View icon in the address bar. This can be a minor inconvenience when Reader View is consistently used for the same web sites. 8 | 9 | This add-on allows users to automatically open Reader View for chosen websites. When the add-on button is clicked, a panel will open that allows the user to change the preference for the current domain. 10 | 11 | To enable Auto Reader View For a new site, click the Auto Reader View button [!book icon](auto-reader-view/data/miu-book-icon-32.png) (it looks like a book) to open the panel, then click "Enable". When clicked, this will save the preference for that domain and a green checkbox will appear on the add-on button. Any future tabs opened for that domain will switch to Reader View automatically. Note that the Close Reader View button continues to work if the user needs to switch back to the normal view for that page. 12 | 13 | To remove the preference for a domain, navigate to a page from that site. Click the Auto Reader View button to open the panel, then click "Disable". 14 | 15 | ## Issues 16 | 17 | Please report any issues at https://github.com/pmarchwiak/auto-reader-view/issues and thank you for using this add-on! 18 | 19 | ## Debugging 20 | Logging for SDK add-ons is disabled by default. Set it info level by setting the `extensions.sdk.console.logLevel` Firefox preference to `info` in `about:config`. See more details at https://developer.mozilla.org/en-US/Add-ons/SDK/Tools/console#Logging_Levels . 21 | 22 | ## Credits 23 | All code by Patrick Marchwiak. 24 | 25 | Icon from Miu theme by Linh Pham Thi Dieu 26 | http://linhpham.me/ 27 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | ## Bugs 4 | - [ ] Fix "Error: The specified tab cannot be placed into reader mode." when opening enabled tabs in background 5 | - [ ] Prevent loop when reader view doesn't work 6 | - [ ] Fix whitelisted domain, readerView -> exit -> readerView -> exit in same tab 7 | - [ ] Allow enabling for a domain when a page is already in reader view 8 | - [ ] Prevent addition of special pages such as about:addons 9 | - [x] Badge doesn't update to red minus occasionally 10 | 11 | ## Features 12 | - [ ] Add help docs 13 | - [ ] Add dialog for viewing saved domains 14 | - [ ] Add dialog for modifying saved domains 15 | - [ ] Support (regex?) patterns for urls 16 | - [ ] Add unit tests 17 | - [ ] Don't use global variables 18 | - [ ] Firefox on Android support? 19 | - [ ] Add hotkey - Cmd-Opt-R? 20 | - [ ] Disable on site home pages, this will happen automatically? 21 | - [x] Allow exiting reader view from whitelisted domains 22 | -------------------------------------------------------------------------------- /auto-reader-view/background.js: -------------------------------------------------------------------------------- 1 | // Track previous tab URLs 2 | var tabPast = new Set(); 3 | 4 | /* 5 | * Updates the browserAction icon. 6 | */ 7 | function updateIcon(enabled) { 8 | console.log("Updating icon"); 9 | if (enabled) { 10 | browser.browserAction.setBadgeText({text: "✓"}); 11 | browser.browserAction.setBadgeBackgroundColor({color: "green"}); 12 | } 13 | else { 14 | browser.browserAction.setBadgeText({text: ""}); 15 | browser.browserAction.setBadgeBackgroundColor({color: null}); 16 | } 17 | } 18 | 19 | function handleMessage(msg) { 20 | console.log("received message", msg); 21 | if (msg.type == 'domainState') { 22 | // Sent by the panel when it loads to determine the current domain and 23 | // its state. 24 | return browser.tabs.query({active: true, windowId: browser.windows.WINDOW_ID_CURRENT}) 25 | .then(tabs => browser.tabs.get(tabs[0].id)) 26 | .then(tab => { 27 | if (isNonReaderAboutPage(tab.url)) { 28 | return {"valid": false}; 29 | } 30 | var domain = domainFromUrl(tab.url); 31 | console.log(`Checking enabled status for ${domain}`); 32 | return isDomainEnabled(domain).then(enabled => { 33 | updateIcon(enabled); 34 | return {"enabled": enabled, "domain": domain, "valid": true}; 35 | }); 36 | }); 37 | } 38 | else if (msg.type == 'domainChange') { 39 | // Sent by the panel to indicate the new state of the given domain. 40 | browser.tabs.query({active: true, windowId: browser.windows.WINDOW_ID_CURRENT}) 41 | .then(tabs => browser.tabs.get(tabs[0].id)) 42 | .then(tab => { 43 | if (msg.enabled) { 44 | addDomain(msg.domain); 45 | tryToggleReaderView(tab); 46 | } 47 | else { 48 | removeDomain(msg.domain); 49 | } 50 | updateIcon(msg.enabled); 51 | }); 52 | } 53 | } 54 | 55 | function handleTabSwitch(activeInfo) { 56 | console.log(`Tab ${activeInfo.tabId} was activated`); 57 | if (activeInfo.tabId) { 58 | return browser.tabs.get(activeInfo.tabId) 59 | .then(tab => { 60 | var domain = domainFromUrl(tab.url); 61 | console.log(`Checking enabled status for ${domain}`); 62 | return isDomainEnabled(domain).then(enabled => { 63 | updateIcon(enabled); 64 | return enabled; 65 | }); 66 | }); 67 | } 68 | } 69 | 70 | 71 | // Check storage for the domain 72 | // @return {Promise} 73 | function isDomainEnabled(domain) { 74 | return getStorage().get("enabledDomains").then(result => { 75 | console.log("Enabled domains are:", result.enabledDomains); 76 | var isEnabled = result.enabledDomains.indexOf(domain) >= 0; 77 | console.log(`${domain} enabled: ${isEnabled}`); 78 | return isEnabled; 79 | }); 80 | } 81 | 82 | // Add a domain to storage 83 | function addDomain(domain) { 84 | console.log("Adding domain " + domain); 85 | getStorage().get("enabledDomains").then(domains => { 86 | console.log("retrieved domains", domains); 87 | if (domains.enabledDomains.indexOf(domain) === -1) { 88 | domains.enabledDomains.push(domain); 89 | } 90 | getStorage().set({"enabledDomains": domains.enabledDomains}); 91 | console.log("Stored domains:", domains.enabledDomains); 92 | }); 93 | } 94 | 95 | // Remove a domain from storage 96 | function removeDomain(domain) { 97 | console.log("Removing domain " + domain); 98 | getStorage().get("enabledDomains").then(result => { 99 | var i = result.enabledDomains.indexOf(domain); 100 | delete result.enabledDomains[i]; 101 | result.enabledDomains = result.enabledDomains.filter(Boolean); 102 | getStorage().set({"enabledDomains": result.enabledDomains}); 103 | console.log("Updated domains:"); 104 | console.log(result.enabledDomains); 105 | }); 106 | } 107 | 108 | function saveDomainsList(domainsList) { 109 | console.log("Saving domains list", domainsList); 110 | getStorage().set({"enabledDomains": domainsList}); 111 | console.log("Saved domains list") 112 | } 113 | 114 | function getStorage() { 115 | return browser.storage.local; 116 | } 117 | 118 | // Initialize storage if not already done so. 119 | // @return {Promise} 120 | function initStorage() { 121 | var store = getStorage(); 122 | return store.get("enabledDomains").then(domains => { 123 | if (isObjectEmpty(domains)) { 124 | console.log("Initializing storage"); 125 | store.set({"enabledDomains": new Array()}); 126 | } 127 | else { 128 | console.log("Storage already intialized"); 129 | } 130 | }); 131 | } 132 | 133 | function isObjectEmpty(obj) { 134 | return Object.keys(obj).length === 0; 135 | } 136 | 137 | // Extract domain from a url 138 | function domainFromUrl(url) { 139 | if (url.startsWith("about:reader?")) { 140 | url = decodeURIComponent(url.substr("about:reader?url=".length)); 141 | } 142 | var r = /:\/\/(.[^/]+)/; 143 | var matches = url.match(r); 144 | if (matches && matches.length >= 2) { 145 | return matches[1]; 146 | } 147 | return null; 148 | } 149 | 150 | function handleTabUpdate(tabId, changeInfo, tab) { 151 | // console.log(`Handling tab update for tab ${tabId} ${tab.url}, status: ${changeInfo.status}`, changeInfo); 152 | if ((changeInfo && changeInfo.isArticle) || 153 | (tab && tab.isArticle)) { 154 | var domain = domainFromUrl(tab.url); 155 | console.log(`Domain for updated tab ${tab.id} is ${domain}`); 156 | isDomainEnabled(domain).then(isEnabled => { 157 | updateIcon(isEnabled); 158 | if(isEnabled) { 159 | console.log(`Auto reader enabled for ${domain}`); 160 | tryToggleReaderView(tab); 161 | } 162 | }) 163 | } 164 | } 165 | 166 | function tryToggleReaderView(tab) { 167 | // Detect user exiting reader view temporarily, don't toggle back 168 | if (!tab.isInReaderMode && tabPast.has(normalToReaderUrl(tab.url))) { 169 | console.log("Was previously in Reader View"); 170 | tabPast.delete(normalToReaderUrl(tab.url)); 171 | } 172 | // Already in reader view 173 | else if (tab.isInReaderMode) { 174 | // do nothing 175 | } 176 | else { 177 | console.log(`Toggling reader mode for ${tab.id} ${tab.url} (isArticle? ${tab.isArticle})`); 178 | browser.tabs.toggleReaderMode(tab.id).catch(onError); 179 | } 180 | 181 | // Store the previous urls in order to detect reader view "exits" 182 | tabPast.add(normalToReaderUrl(tab.url)); 183 | 184 | // Housekeeping to prevent unbounded memory use 185 | if (tabPast.size > 50) { 186 | // TODO use an LRU cache instead 187 | console.log("tabPast size is " + tabPast.size + ". Clearing entries."); 188 | freeCache(tabPast, 5); 189 | } 190 | console.log("New tab past: " + setToString(tabPast)); 191 | } 192 | 193 | function freeCache(set, numToRemove) { 194 | // remove least recently inserted entries 195 | // TODO use an LRU cache instead 196 | var iter = tabPast.values() 197 | for (var i = 0; i < numToRemove; i++) { 198 | var val = iter.next().value; 199 | set.delete(val); 200 | } 201 | } 202 | 203 | // No built-in pretty printing for Set :( 204 | function setToString(s) { 205 | return JSON.stringify([...tabPast]); 206 | } 207 | 208 | function normalToReaderUrl(url) { 209 | if (!url.startsWith("about:reader")) { 210 | url = "about:reader?url=" + encodeURIComponent(url); 211 | } 212 | return url; 213 | } 214 | 215 | function readerToNormalUrl(readerUrl) { 216 | return decodeURIComponent(readerUrl.substr("about:reader?url=".length)); 217 | } 218 | 219 | function isNonReaderAboutPage(url) { 220 | return url.startsWith("about:") && !url.startsWith("about:reader"); 221 | } 222 | 223 | function isUrlHomePage(url) { 224 | var domain = domainFromUrl(url); 225 | var endOfDomainPartIdx = url.indexOf(domain) + domain.length; 226 | var pathPart = url.substr(endOfDomainPartIdx); 227 | 228 | return pathPart.length < 2; // 2 in case of trailing '/' 229 | } 230 | 231 | function onError(err) { 232 | console.log(err); 233 | } 234 | 235 | console.log("background script started"); 236 | console.log("add on click"); 237 | 238 | updateIcon(); 239 | initStorage().then(() => { 240 | // Listen to messages sent from the panel 241 | browser.runtime.onMessage.addListener(handleMessage); 242 | 243 | // Listen to tab URL changes 244 | browser.tabs.onUpdated.addListener(handleTabUpdate); 245 | 246 | // listen to tab switching 247 | browser.tabs.onActivated.addListener(handleTabSwitch); 248 | 249 | // listen for window switching 250 | browser.windows.onFocusChanged.addListener(handleTabSwitch); 251 | }); 252 | -------------------------------------------------------------------------------- /auto-reader-view/data/panel.js: -------------------------------------------------------------------------------- 1 | document.getElementById("submitButton").addEventListener("click", submitClicked); 2 | 3 | var btn = document.getElementById("submitButton"); 4 | var prompt = document.getElementById("panelPrompt"); 5 | var domain = ""; 6 | 7 | self.port.on("panelOpened", function(state) { 8 | console.log("panelOpened event received"); 9 | console.log(state); 10 | domain = state.domain; 11 | if (domain) { 12 | if (state.enabled) { 13 | setEnabledState(domain, btn, prompt); 14 | } 15 | else { 16 | setDisabledState(domain, btn, prompt); 17 | } 18 | } 19 | else { 20 | setUnsupportedState(btn, prompt); 21 | } 22 | }); 23 | 24 | function submitClicked(event) { 25 | var btn = event.target; 26 | var isEnabled = (btn.value == "Enable"); 27 | 28 | if (isEnabled) { 29 | setEnabledState(domain, btn, prompt); 30 | } 31 | else { 32 | setDisabledState(domain, btn, prompt); 33 | } 34 | 35 | self.port.emit("domainChange", { 36 | domain: domain, 37 | enabled: isEnabled 38 | }); 39 | } 40 | 41 | function setEnabledState(domain, btn, prompt) { 42 | btn.style.display = ''; 43 | btn.value = "Disable"; 44 | replacePromptText(prompt, "Pages from ", domain, 45 | " will automatically open in Reader View."); 46 | } 47 | 48 | function setDisabledState(domain, btn, prompt) { 49 | btn.style.display = ''; 50 | btn.value = "Enable"; 51 | replacePromptText(prompt, "Always open pages from ", domain, " in Reader View?") 52 | } 53 | 54 | function setUnsupportedState(btn, prompt) { 55 | btn.style.display = 'none'; 56 | replacePromptText(prompt, "Reader View not available on this page.", "", ""); 57 | } 58 | 59 | function replacePromptText(prompt, part1, domain, part2) { 60 | // clear out prompt text 61 | while (prompt.firstChild) prompt.removeChild(prompt.firstChild); 62 | 63 | // TODO use a library / templating here instead 64 | 65 | var text1 = document.createTextNode(part1); 66 | 67 | var b = document.createElement("b"); 68 | var domainText = document.createTextNode(domain); 69 | b.appendChild(domainText); 70 | 71 | var text2 = document.createTextNode(part2); 72 | 73 | prompt.appendChild(text1); 74 | prompt.appendChild(b); 75 | prompt.appendChild(text2); 76 | } 77 | -------------------------------------------------------------------------------- /auto-reader-view/icons/miu-book-icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmarchwiak/auto-reader-view/616c0aec39e67a4dbc49253e621c11276a0f3d6e/auto-reader-view/icons/miu-book-icon-16.png -------------------------------------------------------------------------------- /auto-reader-view/icons/miu-book-icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmarchwiak/auto-reader-view/616c0aec39e67a4dbc49253e621c11276a0f3d6e/auto-reader-view/icons/miu-book-icon-32.png -------------------------------------------------------------------------------- /auto-reader-view/icons/miu-book-icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmarchwiak/auto-reader-view/616c0aec39e67a4dbc49253e621c11276a0f3d6e/auto-reader-view/icons/miu-book-icon-48.png -------------------------------------------------------------------------------- /auto-reader-view/icons/miu-book-icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmarchwiak/auto-reader-view/616c0aec39e67a4dbc49253e621c11276a0f3d6e/auto-reader-view/icons/miu-book-icon-64.png -------------------------------------------------------------------------------- /auto-reader-view/index.js: -------------------------------------------------------------------------------- 1 | var buttons = require('sdk/ui/button/action'); 2 | var pageMod = require("sdk/page-mod"); 3 | var panels = require("sdk/panel"); 4 | var self = require("sdk/self"); 5 | var ss = require("sdk/simple-storage"); 6 | var tabs = require("sdk/tabs"); 7 | var uu = require("./lib/url-utils.js"); 8 | 9 | var ABOUT_READER_PREFIX = uu.ABOUT_READER_PREFIX; 10 | 11 | var button = buttons.ActionButton({ 12 | id: "auto-reader-view-link", 13 | label: "Auto Reader View", 14 | icon: { 15 | "16": "./miu-book-icon-16.png", 16 | "32": "./miu-book-icon-32.png", 17 | "64": "./miu-book-icon-64.png" 18 | }, 19 | onClick: openPanel, 20 | }); 21 | 22 | var tabPast = new Set(); 23 | 24 | var panel = panels.Panel({ 25 | contentURL: self.data.url("panel.html"), 26 | height: 100, 27 | width: 250, 28 | contentScriptFile: self.data.url("panel.js") 29 | }); 30 | 31 | // When tabs load check if the domain is enabled and if so, switch view. 32 | tabs.on('load', function(tab) { 33 | console.log("tab load: " + tab.url); 34 | if (tab.url && isDomainEnabled(tab.url)) { 35 | console.log("Auto reader enabled for " + tab.url); 36 | setEnabledButtonState(button, tab); 37 | if (!uu.isUrlHomePage(tab.url)) { 38 | redirectToReaderView(tab); 39 | } 40 | } 41 | else { 42 | setDisabledButtonState(button, tab); 43 | } 44 | }); 45 | 46 | // Send state for current tab when the button is clicked 47 | function openPanel(btnState) { 48 | panel.port.emit("panelOpened", { 49 | enabled: isDomainEnabled(tabs.activeTab.url), 50 | domain: uu.domainFromUrl(tabs.activeTab.url) 51 | }); 52 | panel.show({ 53 | position: button 54 | }); 55 | } 56 | 57 | // Save the preference when the enable/disable button is clicked 58 | panel.port.on("domainChange", function(data) { 59 | console.log("change event received"); 60 | console.log(data); 61 | if (data.enabled) { 62 | addDomain(data.domain); 63 | setEnabledButtonState(button, tabs.activeTab); 64 | if (!uu.isUrlHomePage(tabs.activeTab.url)) { 65 | redirectToReaderView(tabs.activeTab); 66 | } 67 | } 68 | else { 69 | removeDomain(data.domain); 70 | setDisabledButtonState(button, tabs.activeTab); 71 | } 72 | }); 73 | 74 | function redirectToReaderView(tab) { 75 | console.log("Tab past: " + setToString(tabPast)); 76 | var origUrl = tab.url; 77 | 78 | // Detect user exiting reader view temporarily, don't do a redirect 79 | if (!origUrl.startsWith(ABOUT_READER_PREFIX) && 80 | tabPast.has(ABOUT_READER_PREFIX + origUrl)) { 81 | console.log("Was already in Reader View, exit"); 82 | tabPast.delete(ABOUT_READER_PREFIX + origUrl); 83 | } 84 | // Already in reader view or another about page 85 | else if (origUrl.startsWith("about:")) { 86 | // do nothing 87 | } 88 | // Redirect to reader view 89 | else { 90 | var newUrl = ABOUT_READER_PREFIX + tab.url; 91 | console.log("Setting new url to " + newUrl); 92 | tab.url = newUrl; 93 | } 94 | 95 | // Store the previous urls in order to detect reader view "exits" 96 | tabPast.add(tab.url); 97 | 98 | // Housekeeping to prevent unbounded memory use 99 | if (tabPast.size > 50) { 100 | // TODO use an LRU cache instead 101 | console.log("tabPast size is " + tabPast.size + ". Clearing entries."); 102 | freeCache(tabPast, 5); 103 | } 104 | console.log("New tab past: " + setToString(tabPast)); 105 | } 106 | 107 | function freeCache(set, numToRemove) { 108 | // remove least recently inserted entries 109 | // TODO use an LRU cache instead 110 | var iter = tabPast.values() 111 | for (var i = 0; i < numToRemove; i++) { 112 | var val = iter.next().value; 113 | set.delete(val); 114 | } 115 | } 116 | 117 | // No built-in pretty printing for Set :( 118 | function setToString(s) { 119 | return JSON.stringify([...tabPast]); 120 | } 121 | 122 | 123 | // Add a checkmark badge to indicate the reader view is enabled 124 | function setEnabledButtonState(button, tab, panel) { 125 | button.state(tab, { 126 | "badge" : "✓", 127 | "badgeColor" : "green" 128 | }); 129 | } 130 | 131 | // Clear the badge 132 | function setDisabledButtonState(button, tab) { 133 | button.state(tab, { 134 | badge : "", 135 | badgeColor : "" 136 | }); 137 | } 138 | 139 | 140 | 141 | // Check storage for the domain 142 | function isDomainEnabled(url) { 143 | initStorage(); 144 | var domain = uu.domainFromUrl(url); 145 | return ss.storage.domains.indexOf(domain) != -1; 146 | } 147 | 148 | // Add a domain to storage 149 | function addDomain(domain) { 150 | initStorage(); 151 | console.log("Adding domain " + domain); 152 | ss.storage.domains.push(domain); 153 | console.log("Stored domains:"); 154 | console.log(ss.storage.domains); 155 | } 156 | 157 | // Remove a domain from storage 158 | function removeDomain(domain) { 159 | initStorage(); 160 | console.log("Removing domain " + domain); 161 | var i = ss.storage.domains.indexOf(domain); 162 | delete ss.storage.domains[i]; 163 | console.log(ss.storage.domains); 164 | } 165 | 166 | // Initialize storage if not already done so. 167 | function initStorage() { 168 | if (!ss.storage.domains) { 169 | ss.storage.domains = []; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /auto-reader-view/lib/url-utils.js: -------------------------------------------------------------------------------- 1 | var ABOUT_READER_PREFIX = "about:reader?url="; 2 | 3 | // Strip reader prefix from url 4 | function urlWithoutReader(url) { 5 | if (url.startsWith(ABOUT_READER_PREFIX)) { 6 | return url.substring(ABOUT_READER_PREFIX.length); 7 | } 8 | return url; 9 | } 10 | 11 | // Extract domain from a url 12 | function domainFromUrl(url) { 13 | if (url.startsWith(ABOUT_READER_PREFIX)) { 14 | url = urlWithoutReader(url); 15 | } 16 | var r = /:\/\/(.[^/]+)/; 17 | var matches = url.match(r); 18 | if (matches && matches.length >= 2) { 19 | return matches[1]; 20 | } 21 | return null; 22 | } 23 | 24 | function isUrlHomePage(url) { 25 | var domain = domainFromUrl(url); 26 | var endOfDomainPartIdx = url.indexOf(domain) + domain.length; 27 | var pathPart = url.substr(endOfDomainPartIdx); 28 | 29 | return pathPart.length < 2; // 2 in case of trailing '/' 30 | } 31 | 32 | exports.domainFromUrl = domainFromUrl; 33 | exports.isUrlHomePage = isUrlHomePage; 34 | exports.ABOUT_READER_PREFIX = ABOUT_READER_PREFIX; 35 | -------------------------------------------------------------------------------- /auto-reader-view/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "auto-reader-view", 4 | "description": "Enable Reader Mode automatically on chosen websites", 5 | "version": "1.0.3", 6 | "homepage_url": "https://github.com/pmarchwiak/auto-reader-view", 7 | "applications": { 8 | "gecko": { 9 | "id": "@auto-reader-view" 10 | } 11 | }, 12 | 13 | "icons": { 14 | "48": "icons/miu-book-icon-48.png" 15 | }, 16 | 17 | "permissions": ["storage", "tabs"], 18 | 19 | "background": { 20 | "scripts": ["background.js"] 21 | }, 22 | 23 | "browser_action": { 24 | "browser_style": true, 25 | "default_icon": { 26 | "16": "icons/miu-book-icon-16.png", 27 | "32": "icons/miu-book-icon-32.png", 28 | "64": "icons/miu-book-icon-64.png" 29 | }, 30 | "default_title": "Auto Reader Mode", 31 | "default_popup": "popup/panel.html" 32 | }, 33 | 34 | "options_ui": { 35 | "page": "options/options.html", 36 | "browser_style": true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /auto-reader-view/options/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 |

Saved domains

12 |

Web pages from any of the domains listed below will open in 13 | Reader Mode automatically. To add new ones, simply edit the list, 14 | with a new line between each entry, then click the Save button. 15 |

16 | 19 |
20 | 21 | 22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /auto-reader-view/options/options.js: -------------------------------------------------------------------------------- 1 | 2 | function handleSaveButtonClick(e) { 3 | var text = document.querySelector("#domainsInput").value; 4 | var domainsList = text.split("\n"); 5 | 6 | browser.runtime.getBackgroundPage().then( 7 | (backgroundPage) => { 8 | backgroundPage.saveDomainsList(domainsList); 9 | document.querySelector("#saveMessage").innerText = "List saved successfully." 10 | }, 11 | (error) => { 12 | console.log("Error getting backgroundPage", error) 13 | } 14 | ) 15 | 16 | e.preventDefault(); 17 | } 18 | 19 | function restoreOptions() { 20 | // TODO refresh after a tab switch 21 | console.log("Reading domains"); 22 | browser.storage.local.get("enabledDomains").then(storageObject => { 23 | console.log("retrieved domains", storageObject); 24 | var domainsString = storageObject.enabledDomains.join("\n"); 25 | document.querySelector("#domainsInput").value = domainsString; 26 | }); 27 | } 28 | 29 | document.addEventListener('DOMContentLoaded', restoreOptions); 30 | document.querySelector("form").addEventListener("submit", handleSaveButtonClick); -------------------------------------------------------------------------------- /auto-reader-view/popup/panel.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | /*width: 100px;*/ 3 | } 4 | 5 | div { 6 | margin: 10px; 7 | } 8 | 9 | .hidden { 10 | display: none; 11 | } 12 | 13 | .button { 14 | margin: 3% auto; 15 | padding: 4px; 16 | text-align: center; 17 | /*font-size: 1em;*/ 18 | cursor: pointer; 19 | } 20 | -------------------------------------------------------------------------------- /auto-reader-view/popup/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 16 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /auto-reader-view/popup/panel.js: -------------------------------------------------------------------------------- 1 | function getButton() { 2 | return document.getElementById("submitButton"); 3 | } 4 | 5 | function getPrompt() { 6 | return document.getElementById("panelPrompt"); 7 | } 8 | 9 | function getDomainInput() { 10 | return document.getElementById("domainInput"); 11 | } 12 | 13 | function submitClicked(event) { 14 | console.log("submitButton clicked"); 15 | var btn = event.target; 16 | var isEnabled = (btn.value == "Enable"); 17 | var domain = getDomainInput().value; 18 | updatePanelUi(true, domain, isEnabled); 19 | 20 | browser.runtime.sendMessage({ 21 | type: "domainChange", 22 | enabled: isEnabled, 23 | "domain": domain 24 | }); 25 | } 26 | 27 | function updatePanelUi(isValid, domain, isEnabled) { 28 | if (!isValid) { 29 | setInvalidState(); 30 | } 31 | else if (isEnabled) { 32 | setEnabledState(domain); 33 | } 34 | else { 35 | setDisabledState(domain); 36 | } 37 | } 38 | 39 | function setInvalidState() { 40 | var btn = getButton(); 41 | btn.value = "Enable"; 42 | btn.setAttribute("disabled", ""); 43 | replacePromptText(`"about:" pages cannot be opened in Reader View`); 44 | } 45 | 46 | function setEnabledState(domain) { 47 | var btn = getButton(); 48 | // btn.style.display = ''; 49 | btn.value = "Disable"; 50 | btn.removeAttribute("disabled"); 51 | getDomainInput().value = domain; 52 | replacePromptText("Pages from ", domain, " will automatically open in Reader View"); 53 | } 54 | 55 | function setDisabledState(domain) { 56 | var btn = getButton(); 57 | // btn.style.display = ''; 58 | btn.value = "Enable"; 59 | btn.removeAttribute("disabled"); 60 | getDomainInput().value = domain; 61 | replacePromptText("Always open pages from ", domain, " in Reader View?"); 62 | } 63 | 64 | function replacePromptText(part1, domain = null, part2 = null) { 65 | var prompt = getPrompt(); 66 | // clear out prompt text 67 | while (prompt.firstChild) prompt.removeChild(prompt.firstChild); 68 | 69 | // TODO use a library / templating here instead 70 | 71 | var text1 = document.createTextNode(part1); 72 | prompt.appendChild(text1); 73 | 74 | if (domain) { 75 | var b = document.createElement("b"); 76 | var domainText = document.createTextNode(domain); 77 | b.appendChild(domainText); 78 | prompt.appendChild(b); 79 | } 80 | 81 | if (part2) { 82 | var text2 = document.createTextNode(part2); 83 | prompt.appendChild(text2); 84 | } 85 | } 86 | 87 | function handlePanelOpened(msg) { 88 | console.log("Received message", msg); 89 | if (msg.type === "browserActionClicked") { 90 | updatePanelUi(msg.domain, msg.isEnabled); 91 | } 92 | } 93 | 94 | console.log("Loaded panel.js"); 95 | 96 | getButton().addEventListener("click", submitClicked); 97 | browser.runtime.onMessage.addListener(handlePanelOpened); 98 | 99 | browser.runtime.sendMessage({"type": "domainState"}).then(resp => { 100 | console.log("received resp", resp); 101 | var domain = null; 102 | var isEnabled = null; 103 | if (resp.valid) { 104 | domain = resp.domain; 105 | isEnabled = resp.enabled; 106 | } 107 | updatePanelUi(resp.valid, resp.domain, resp.enabled); 108 | }); 109 | -------------------------------------------------------------------------------- /auto-reader-view/test/test-url-utils.js: -------------------------------------------------------------------------------- 1 | var uu = require("../lib/url-utils"); 2 | 3 | exports["test domainFromUrl"] = function(assert) { 4 | assert.ok(uu.domainFromUrl("http://google.com") == "google.com"); 5 | assert.ok(uu.domainFromUrl("http://google.com/blah") == "google.com"); 6 | assert.ok(uu.domainFromUrl("https://google.com/blah") == "google.com"); 7 | }; 8 | 9 | exports["test isUrlHomePage"] = function(assert) { 10 | assert.ok(uu.isUrlHomePage("http://google.com")); 11 | assert.ok(uu.isUrlHomePage("http://google.com/")); 12 | assert.ok(!uu.isUrlHomePage("http://google.com/search")); 13 | assert.ok(!uu.isUrlHomePage("https://google.com/search")); 14 | }; 15 | 16 | 17 | require("sdk/test").run(exports); 18 | --------------------------------------------------------------------------------