# HG changeset patch # User Guido Berhoerster # Date 1544220041 -3600 # Node ID ff5e5e3eba329d6723b00c736d902522fc8f1fc0 # Parent fcd65cf3f634b53307731ba00eb387f4024fcec4 Implement feed subscription for web-based feed readers Add options page for configuring web-based feed readers which allow for subscribing to feeds via GET requests. Track tabs containing feed previews and inject a content script which retrieves the configured feed readers and keeps them in sync. diff -r fcd65cf3f634 -r ff5e5e3eba32 Makefile --- a/Makefile Tue Nov 27 16:05:14 2018 +0100 +++ b/Makefile Fri Dec 07 23:00:41 2018 +0100 @@ -38,11 +38,15 @@ $(wildcard _locales/*/messages.json) \ background.html \ content_scripts/feed-probe.js \ + content_scripts/feed-readers.js \ icons/feed-preview.svg \ $(BITMAP_ICONS) \ js/background.js \ js/feed-parser.js \ js/feed-preview.js \ + options/options.css \ + options/options.html \ + options/options.js \ popup/feed-selection.js \ popup/feed-selection.html \ web_resources/style/feed-preview.css \ diff -r fcd65cf3f634 -r ff5e5e3eba32 _locales/de/messages.json --- a/_locales/de/messages.json Tue Nov 27 16:05:14 2018 +0100 +++ b/_locales/de/messages.json Fri Dec 07 23:00:41 2018 +0100 @@ -4,9 +4,17 @@ "description": "Name of the extension." }, "extensionDescription": { - "message": "Signalisiert verfügbare RSS und/oder Atom Feeds und zeigt eine Vorschaui an.", + "message": "Signalisiert verfügbare RSS und/oder Atom Feeds und zeigt eine Vorschau an.", "description": "Description of the extension." }, + "feedReaderSelectionLabel": { + "message": "Abonnieren mit", + "description": "Label for the feed reader menu." + }, + "subscribeButtonLabel": { + "message": "Jetzt Abonnieren", + "description": "Label for the subscibe button." + }, "defaultFeedTitle": { "message": "Feed ohne Titel", "description": "Default title for feeds." @@ -30,5 +38,57 @@ "filesTitle": { "message": "Mediendateien:", "description": "Title of the list of media files." + }, + "feedReadersTitle": { + "message": "Feedreader", + "description": "Title of the feed reader options." + }, + "feedReaderMoveUpButton": { + "message": "Rauf", + "description": "Label of the button for deleting a feed reader up." + }, + "feedReaderMoveDownButton": { + "message": "Runter", + "description": "Label of the button for moving a feed reader down." + }, + "feedReaderRemoveButton": { + "message": "Entfernen", + "description": "Label of the button for deleting a feed reader." + }, + "feedReaderAddButton": { + "message": "Hinzufügen", + "description": "Label of the button for adding a feed reader." + }, + "feedReaderTitleLabel": { + "message": "Titel", + "description": "Label of the text input field for the feed reader title." + }, + "feedReaderTitlePlaceholder": { + "message": "z.B. My Feed Reader", + "description": "Placeholder displayed in the text input field for the feed reader title." + }, + "feedReaderUrlTemplateLabel": { + "message": "URL-Vorlage", + "description": "Label of the text input field for the feed reader URL template." + }, + "feedReaderUrlTemplatePlaceholder": { + "message": "z.B. https://feedreader.example.org/?subscribe=%s", + "description": "Placeholder displayed in the text input field for the feed reader URL template." + }, + "feedReaderUrlTemplateCaption": { + "message": "URL zum Abonnieren von Feeds mit einem Platzhalter %s, der durch die Feed-URL ersetzt wird.", + "description": "Caption for the the text input field for the feed reader URL template." + }, + "invalidURLError": { + "message": "Bitte eine gültige URL eingeben.", + "description": "Error message if the subscription URL template is invalid." + }, + "invalidProtocolError": { + "message": "Bitte eine URL eingeben, die entweder das HTTP- oder HTTPS-Protokoll nutzt.", + "description": "Error message if the protocol of the subscription URL template is neither HTTP nor HTTPS." + }, + "missingPlaceholderError": { + "message": "Bitte die URL zum Abonnieren von Feeds eingeben, die einen Platzhalter \"%s\" für die URL das Feeds enthält.", + "description": "Error message if the placholder is missing from the subscription URL template." } } diff -r fcd65cf3f634 -r ff5e5e3eba32 _locales/en/messages.json --- a/_locales/en/messages.json Tue Nov 27 16:05:14 2018 +0100 +++ b/_locales/en/messages.json Fri Dec 07 23:00:41 2018 +0100 @@ -7,6 +7,14 @@ "message": "Indicates available RSS and Atom feeds and renders previews.", "description": "Description of the extension." }, + "feedReaderSelectionLabel": { + "message": "Subscribe to this feed using", + "description": "Label for the feed reader menu." + }, + "subscribeButtonLabel": { + "message": "Subscribe Now", + "description": "Label for the subscibe button." + }, "defaultFeedTitle": { "message": "Untitled Feed", "description": "Default title for feeds." @@ -30,5 +38,57 @@ "filesTitle": { "message": "Media Files:", "description": "Title of the list of media files." + }, + "feedReadersTitle": { + "message": "Feed Readers", + "description": "Title of the feed reader options." + }, + "feedReaderMoveUpButton": { + "message": "Move Up", + "description": "Label of the button for deleting a feed reader up." + }, + "feedReaderMoveDownButton": { + "message": "Move Down", + "description": "Label of the button for moving a feed reader down." + }, + "feedReaderRemoveButton": { + "message": "Remove", + "description": "Label of the button for deleting a feed reader." + }, + "feedReaderAddButton": { + "message": "Add", + "description": "Label of the button for adding a feed reader." + }, + "feedReaderTitleLabel": { + "message": "Title", + "description": "Label of the text input field for the feed reader title." + }, + "feedReaderTitlePlaceholder": { + "message": "e.g. My Feed Reader", + "description": "Placeholder displayed in the text input field for the feed reader title." + }, + "feedReaderUrlTemplateLabel": { + "message": "URL Template", + "description": "Label of the text input field for the feed reader URL template." + }, + "feedReaderUrlTemplatePlaceholder": { + "message": "e.g. https://feedreader.example.org/?subscribe=%s", + "description": "Placeholder displayed in the text input field for the feed reader URL template." + }, + "feedReaderUrlTemplateCaption": { + "message": "URL for subscribing to feeds with a placeholder %s which will be substituted with the feed URL.", + "description": "Caption for the the text input field for the feed reader URL template." + }, + "invalidURLError": { + "message": "Please enter a valid URL.", + "description": "Error message if the subscription URL template is invalid." + }, + "invalidProtocolError": { + "message": "Please enter a URL which uses either the HTTP or HTTPS protocol.", + "description": "Error message if the protocol of the subscription URL template is neither HTTP nor HTTPS." + }, + "missingPlaceholderError": { + "message": "Please enter a subscription URL which contains a placeholder \"%s\" for the feed URL.", + "description": "Error message if the placholder is missing from the subscription URL template." } } diff -r fcd65cf3f634 -r ff5e5e3eba32 content_scripts/feed-readers.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/content_scripts/feed-readers.js Fri Dec 07 23:00:41 2018 +0100 @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2018 Guido Berhoerster + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +'use strict'; + +function updateFeedReaders(feedReaders) { + let feedReaderSelectionElement = document.forms['feed-subscription'] + .elements['feed-reader-selection']; + for (let optionElement of + feedReaderSelectionElement.querySelectorAll('option')) { + optionElement.remove(); + } + + for (let feedReader of feedReaders) { + let optionElement = document.createElement('option'); + optionElement.value = feedReader.urlTemplate; + optionElement.textContent = feedReader.title; + feedReaderSelectionElement.append(optionElement); + } + + document.forms['feed-subscription'].elements['main'].disabled = + feedReaders.length === 0; +} + +document.addEventListener('submit', ev => { + if (ev.target.id !== 'feed-subscription') { + return; + } + + ev.preventDefault(); + let subscribeUrl = ev.target.elements['feed-reader-selection'].value + .replace('%s', encodeURIComponent(document.documentURI)); + console.log(`subscribing to feed using ${subscribeUrl}`); + window.location.href = subscribeUrl; +}); + +function onStorageChanged(changes, areaName) { + if (areaName !== 'sync' || changes.feedReaders === 'undefined') { + return; + } + + // stored feed readers have been changed or deleted + let feedReaders = typeof changes.feedReaders.newValue !== 'undefined' ? + changes.feedReaders.newValue : []; + console.log('feedReaders changed to', feedReaders); + updateFeedReaders(feedReaders); +} + +(async () => { + // initialize subscription form + let {feedReaders = []} = await browser.storage.sync.get('feedReaders'); + updateFeedReaders(feedReaders); + + browser.storage.onChanged.addListener(onStorageChanged); +})(); diff -r fcd65cf3f634 -r ff5e5e3eba32 js/background.js --- a/js/background.js Tue Nov 27 16:05:14 2018 +0100 +++ b/js/background.js Fri Dec 07 23:00:41 2018 +0100 @@ -17,6 +17,7 @@ ...Object.values(feedParser.XMLNS) ]; var tabsFeeds = new Map(); +var tabsFeedPreviews = new Map(); var fetchingFeedPreview = fetch('web_resources/feed-preview.xhtml') .then(response => response.text()); @@ -38,7 +39,7 @@ return contentType; } -async function handleFeed(inputText, url) { +async function handleFeed(inputText, tabId, url) { // fast-path: eliminate XML documents which cannot be Atom nor RSS feeds let inputTextStart = inputText.substring(0, 512); if (!FEED_MAGIC.some(element => inputTextStart.includes(element))) { @@ -59,6 +60,9 @@ } console.log(`parsed feed ${url}:\n`, feed); + // mark this feed preview for content script injection + tabsFeedPreviews.set(tabId, url); + // render the preview document let feedPreviewDocument = new DOMParser() .parseFromString(await fetchingFeedPreview, 'text/html'); @@ -118,7 +122,7 @@ inputText += decoder.decode(ev.data, {stream: true}); }); filter.addEventListener('stop', async ev => { - let result = await handleFeed(inputText, details.url); + let result = await handleFeed(inputText, details.tabId, details.url); filter.write(encoder.encode(result)); filter.close(); }); @@ -145,7 +149,7 @@ } }); -browser.tabs.onUpdated.addListener((id, changeInfo, tab) => { +browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { if (typeof changeInfo.url === 'undefined') { // filter out updates which do not change the URL return; @@ -153,9 +157,18 @@ // hide the page action when the URL changes since it is no longer valid, // it will be shown again if the content script detects a feed - browser.pageAction.hide(tab.id); + browser.pageAction.hide(tabId); + + // inject content script once if the requested URL is a feed preview + if (tabsFeedPreviews.get(tabId) === changeInfo.url) { + browser.tabs.executeScript(tabId, { + file: 'content_scripts/feed-readers.js' + }); + tabsFeedPreviews.delete(tabId); + } }); browser.tabs.onRemoved.addListener((tabId, removeInfo) => { tabsFeeds.delete(tabId); + tabsFeedPreviews.delete(tabId); }); diff -r fcd65cf3f634 -r ff5e5e3eba32 js/feed-preview.js --- a/js/feed-preview.js Tue Nov 27 16:05:14 2018 +0100 +++ b/js/feed-preview.js Fri Dec 07 23:00:41 2018 +0100 @@ -17,10 +17,16 @@ `type="application/xslt+xml" href="${xslFilename}"`); feedPreviewDocument.firstChild.after(xmlStylesheetNode); - feedPreviewDocument.querySelector('link[rel=stylesheet]').href = + feedPreviewDocument.querySelector('#default-stylesheet').href = browser.runtime.getURL('web_resources/style/feed-preview.css'); feedPreviewDocument.querySelector('title').textContent = feed.title; + + feedPreviewDocument.querySelector('label[for="feed-reader-selection"]') + .textContent = browser.i18n.getMessage('feedReaderSelectionLabel'); + feedPreviewDocument.querySelector('[name="subscribe"]').textContent = + browser.i18n.getMessage('subscribeButtonLabel'); + feedPreviewDocument.querySelector('#feed-title').textContent = feed.title; feedPreviewDocument.querySelector('#feed-subtitle').textContent = feed.subtitle; diff -r fcd65cf3f634 -r ff5e5e3eba32 manifest.json.in --- a/manifest.json.in Tue Nov 27 16:05:14 2018 +0100 +++ b/manifest.json.in Fri Dec 07 23:00:41 2018 +0100 @@ -17,6 +17,7 @@ "96": "icons/feed-preview-96.png" }, "permissions": [ + "storage", "tabs", "http://*/*", "https://*/*", @@ -42,5 +43,9 @@ "default_icon": "icons/feed-preview.svg", "default_title": "Feeds", "default_popup": "popup/feed-selection.html" + }, + "options_ui": { + "page": "options/options.html", + "browser_style": true } } diff -r fcd65cf3f634 -r ff5e5e3eba32 options/options.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/options/options.css Fri Dec 07 23:00:41 2018 +0100 @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2018 Guido Berhoerster + * + * 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/. + */ + +@import url("chrome://browser/content/extension.css"); +@import url("../web_resources/style/photon-colors.css"); + +:root { + --blue-50-a30: rgba(10, 132, 255, 0.3); + --red-50-a30: rgba(255, 0, 57, 0.3); + --selection-item-background-color: var(--grey-90-a10); + --selection-selected-background-color: var(--blue-50); + --selection-selected-color: var(--white-100); + --text-input-border-color: var(--grey-90-a30); + --text-input-hover-border-color: var(--grey-90-a50); + --text-input-focus-border-color: var(--blue-50); + --text-input-invalid-border-color: var(--red-50); + --secondary-color: var(--grey-50); + --text-input-shadow: 0 0 0 4px var(--blue-50-a30); + --invalid-shadow: 0 0 0 4px var(--red-50-a30); + --selection-border: 1px solid var(--grey-90-a30); +} + +body { + padding: 0 4px; +} + +h1 { + margin: 0 0 16px 0; + font-size: 1.29em; + font-weight: bold; +} + +input[type="text"] { + border-radius: 2px; + font-size: 15px; + padding: 8px; + min-height: 32px; +} + +.browser-style > input[type=text] { + border-color: var(--text-input-border-color); + box-shadow: none; +} + +.browser-style > input[type=text]:hover { + border-color: var(--text-input-hover-border-color); + box-shadow: none; +} + +.browser-style > input[type=text]:focus, +.browser-style > input[type=text]:focus:hover { + border-color: var(--text-input-focus-border-color); + box-shadow: var(--text-input-shadow); +} + +.browser-style > input[type=text]:invalid, +.browser-style > input[type=text]:invalid:hover, +.browser-style > input[type=text]:invalid:focus:hover, +.browser-style > input[type=text]:invalid:focus { + border-color: var(--text-input-invalid-border-color); + box-shadow: var(--invalid-shadow); +} + +button.browser-style { + border-radius: 2px; + padding: 2px 16px; +} + +#feed-reader-selection { + margin: 4px 0; + padding: 0; + border: var(--selection-border); + border-radius: 2px; + width: 100%; + height: 10em; + overflow: auto; +} + +#feed-reader-selection:focus-within { + border: 1px dotted var(--selection-selected-background-color); +} + +.feed-reader-item { + list-style: none; +} + +.feed-reader-item label { + position: relative; +} + +.feed-reader-item input[type=radio] { + /* + * take the actual radio button out of the page flow and make it invisible + * without hiding it (using "display: none" or "visibility: hidden") so that + * keyboard focus handling keeps working as expected + */ + position: absolute; + -moz-appearance: none; + appearance: none; + width: 0; + height: 0; + opacity: 0; +} + +.feed-reader-item input[type=radio]:checked + .feed-reader-content { + background-color: var(--selection-selected-background-color); + color: var(--selection-selected-color); +} + +.feed-reader-content { + padding: 4px 8px; + white-space: nowrap; +} + +.feed-reader-item:nth-child(even) .feed-reader-content { + background-color: var(--selection-item-background-color); +} + +.feed-reader-content .feed-reader-title, +.feed-reader-content .feed-reader-url-template { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; +} + +.feed-reader-content .feed-reader-title { + width: 30%; +} + +.feed-reader-content .feed-reader-url-template { + width: 70%; +} + +.button-box { + border: none; + padding: 0; + margin: 4px 0 0 0; + text-align: right; +} + +.button-box button + button { + margin-left: 8px; +} + +#add-feed-reader label { + display: block; + font-size: 13px; + margin: 4px 0; +} + +#add-feed-reader input[type="text"] { + display: block; + width: 100%; +} + +.caption { + font-size: 11px; + margin: 4px 0; + color: var(--secondary-color); +} diff -r fcd65cf3f634 -r ff5e5e3eba32 options/options.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/options/options.html Fri Dec 07 23:00:41 2018 +0100 @@ -0,0 +1,54 @@ + + + + + + + + + + +

+
+
    +
+
+
+
+
+
+ + +
+
+ + +

+
+
+
+
+ + diff -r fcd65cf3f634 -r ff5e5e3eba32 options/options.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/options/options.js Fri Dec 07 23:00:41 2018 +0100 @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2018 Guido Berhoerster + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +'use strict'; + +function normalizeURL(text) { + return new URL(text).toString(); +} + +class OptionsPage { + constructor() { + this.selectedFeedReader = -1; + + document.querySelector('#feed-readers-title').textContent = + browser.i18n.getMessage('feedReadersTitle'); + + let feedReadersForm = document.forms['feed-readers']; + feedReadersForm.elements['move-up'].textContent = + browser.i18n.getMessage('feedReaderMoveUpButton'); + feedReadersForm.elements['move-down'].textContent = + browser.i18n.getMessage('feedReaderMoveDownButton'); + feedReadersForm.elements['remove'].textContent = + browser.i18n.getMessage('feedReaderRemoveButton'); + feedReadersForm.addEventListener('change', this); + + let addFeedReaderForm = document.forms['add-feed-reader']; + addFeedReaderForm.elements['add'].textContent = + browser.i18n.getMessage('feedReaderAddButton'); + let titleElement = addFeedReaderForm.elements['title']; + titleElement.labels[0].textContent = + browser.i18n.getMessage('feedReaderTitleLabel'); + titleElement.placeholder = + browser.i18n.getMessage('feedReaderTitlePlaceholder'); + let urlTemplateElement = + addFeedReaderForm.elements['url-template']; + urlTemplateElement.labels[0].textContent = + browser.i18n.getMessage('feedReaderUrlTemplateLabel'); + urlTemplateElement.placeholder = + browser.i18n.getMessage('feedReaderUrlTemplatePlaceholder'); + document.querySelector('#feed-reader-url-caption').textContent = + browser.i18n.getMessage('feedReaderUrlTemplateCaption'); + addFeedReaderForm.addEventListener('focusout', this); + + document.addEventListener('submit', this); + + this.initOptions(); + } + + async initOptions() { + let {feedReaders} = await browser.storage.sync.get('feedReaders'); + if (Array.isArray(feedReaders)) { + console.log('initialized feedReaders from storage', feedReaders); + this.updateFeedReaders(feedReaders); + } + + browser.storage.onChanged.addListener(this.onStorageChanged.bind(this)); + } + + validateURLTemplate(text) { + let url; + try { + url = new URL(text); + } catch(e) { + if (e instanceof TypeError) { + return browser.i18n.getMessage('invalidURLError'); + } + throw e; + } + + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + return browser.i18n.getMessage('invalidProtocolError'); + } + + if (!(url.pathname.includes('%s') || url.search.includes('%s'))) { + return browser.i18n.getMessage('missingPlaceholderError'); + } + + return ''; + } + + updateFeedReaders(feedReaders) { + let feedReadersForm = document.forms['feed-readers']; + let feedReaderItemElements = + feedReadersForm.querySelectorAll('.feed-reader-item'); + for (let feedReaderItemElement of feedReaderItemElements) { + feedReaderItemElement.remove(); + } + + let feedReaderItemTemplateElement = + document.querySelector('#feed-reader-item-template'); + let feedReaderSelectionElement = + feedReadersForm.querySelector('#feed-reader-selection') + for (let feedReader of feedReaders) { + let feedReaderItemNode = + document.importNode(feedReaderItemTemplateElement.content, + true); + let feedReaderInputElement = + feedReaderItemNode.querySelector('input[name=feed-reader]'); + feedReaderInputElement.dataset.title = feedReader.title; + feedReaderInputElement.value = feedReader.urlTemplate; + feedReaderItemNode.querySelector('.feed-reader-title') + .textContent = feedReader.title; + feedReaderItemNode.querySelector('.feed-reader-url-template') + .textContent = feedReader.urlTemplate; + feedReaderSelectionElement.append(feedReaderItemNode); + } + + feedReadersForm.elements['buttons'].disabled = true; + } + + getFeedReaders() { + let feedReaderInput = + document.forms['feed-readers'].elements['feed-reader']; + if (feedReaderInput instanceof RadioNodeList) { + return Array.from(feedReaderInput); + } else if (typeof feedReaderInput === 'undefined') { + return []; + } + return Array.from([feedReaderInput]); + } + + selectFeedReader() { + console.debug('selected:', this.selectedFeedReader); + if (this.selectedFeedReader < 0) { + return; + } + + let feedReadersForm = document.forms['feed-readers']; + let feedReaderElements = this.getFeedReaders(); + feedReaderElements[this.selectedFeedReader].checked = true; + // ensure that the checked element will also be the focused one the + // next time the radio input group receives focus + let activeElement = document.activeElement; + feedReaderElements[this.selectedFeedReader].focus(); + activeElement.focus(); + + feedReadersForm.elements['buttons'].disabled = false; + } + + serializeFeedReaders() { + return this.getFeedReaders().map(element => ({ + title: element.dataset.title, + urlTemplate: element.value + })); + } + + onStorageChanged(changes, areaName) { + if (areaName !== 'sync' || typeof changes.feedReaders === 'undefined') { + return; + } + + let feedReaders; + if (typeof changes.feedReaders.newValue !== 'undefined' && + Array.isArray(changes.feedReaders.newValue)) { + feedReaders = changes.feedReaders.newValue; + console.log('feedReaders changed to', feedReaders); + } else { + // list of feed readers was removed or set to nonsensical value + feedReaders = []; + console.log('feedReaders was removed'); + } + if (this.selectedFeedReader >= feedReaders.length) { + // save selected feed reader is no longer valid + this.selectedFeedReader = -1; + } + this.updateFeedReaders(feedReaders); + this.selectFeedReader(); + } + + handleEvent(ev) { + console.log('previously selected:', this.selectedFeedReader); + if (ev.type === 'change' && ev.target.name === 'feed-reader') { + // feed reader was selected by user interaction + console.debug(ev); + this.selectedFeedReader = this.getFeedReaders().indexOf(ev.target); + console.log('now selected:', this.selectedFeedReader); + + document.forms['feed-readers'].elements['buttons'].disabled = false; + } else if (ev.type === 'submit' && ev.target.id === 'feed-readers') { + // remove feed reader or move feed reader up or down + ev.preventDefault(); + + let feedReaders = this.serializeFeedReaders(); + if (ev.explicitOriginalTarget.name === 'move-up') { + if (this.selectedFeedReader - 1 < 0) { + // the first feed reader is selected + return; + } + [feedReaders[this.selectedFeedReader - 1], + feedReaders[this.selectedFeedReader]] = + [feedReaders[this.selectedFeedReader], + feedReaders[this.selectedFeedReader - 1]]; + this.selectedFeedReader--; + } else if (ev.explicitOriginalTarget.name === 'move-down') { + if (this.selectedFeedReader + 1 === feedReaders.length) { + // the last feed reader is selected + return; + } + [feedReaders[this.selectedFeedReader + 1], + feedReaders[this.selectedFeedReader]] = + [feedReaders[this.selectedFeedReader], + feedReaders[this.selectedFeedReader + 1]]; + this.selectedFeedReader++; + } else if (ev.explicitOriginalTarget.name === 'remove') { + feedReaders.splice(this.selectedFeedReader, 1); + this.selectedFeedReader--; + } + browser.storage.sync.set({feedReaders}); + console.log('set feedReaders to ', feedReaders); + } else if (ev.type === 'focusout' && + ev.target.name === 'url-template') { + // url template was changed + let validity = this.validateURLTemplate(ev.target.value); + ev.target.setCustomValidity(validity); + } else if (ev.type === 'submit' && + ev.target.id === 'add-feed-reader') { + // feed reader added + ev.preventDefault(); + + let urlTemplate = ev.target.elements['url-template'].value; + let isValid = this.validateURLTemplate(urlTemplate); + ev.target.elements['url-template'].setCustomValidity(isValid); + if (!ev.target.reportValidity()) { + return; + } + + let feedReaders = this.serializeFeedReaders(); + feedReaders.push({ + title: ev.target.elements['title'].value, + urlTemplate: normalizeURL(urlTemplate) + }); + browser.storage.sync.set({feedReaders}); + console.log('set feedReaders to', feedReaders); + + document.forms['add-feed-reader'].reset(); + } + } +} + +var page = new OptionsPage(); diff -r fcd65cf3f634 -r ff5e5e3eba32 web_resources/feed-preview.xhtml --- a/web_resources/feed-preview.xhtml Tue Nov 27 16:05:14 2018 +0100 +++ b/web_resources/feed-preview.xhtml Fri Dec 07 23:00:41 2018 +0100 @@ -11,7 +11,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - + @@ -48,6 +48,14 @@
  • +
    +
    + +
    +

    diff -r fcd65cf3f634 -r ff5e5e3eba32 web_resources/images/arrow.svg --- a/web_resources/images/arrow.svg Tue Nov 27 16:05:14 2018 +0100 +++ b/web_resources/images/arrow.svg Fri Dec 07 23:00:41 2018 +0100 @@ -6,5 +6,5 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - + diff -r fcd65cf3f634 -r ff5e5e3eba32 web_resources/style/feed-preview.css --- a/web_resources/style/feed-preview.css Tue Nov 27 16:05:14 2018 +0100 +++ b/web_resources/style/feed-preview.css Fri Dec 07 23:00:41 2018 +0100 @@ -9,9 +9,21 @@ @import url("common.css"); :root { + --blue-50-a30: rgba(10, 132, 255, 0.3); --default-background: var(--grey-10); --entry-background: var(--white-100); + --primary-color: var(--grey-90); --secondary-color: var(--grey-50); + --button-focus-shadow: 0 0 0 1px var(--blue-50) inset, + 0 0 0 1px var(--blue-50), 0 0 0 4px var(--blue-50-a30); + --primary-button-color: var(--white-100); + --primary-button-background-color: var(--blue-60); + --primary-button-hover-background-color: var(--blue-70); + --primary-button-active-background-color: var(--blue-80); + --secondary-button-color: var(--primary-color); + --secondary-button-background-color: var(--grey-90-a10); + --secondary-button-hover-background-color: var(--grey-90-a20); + --secondary-button-active-background-color: var(--grey-90-a30); --entry-content-border: 1px solid var(--grey-90-a10); --font-family-default: "Segoe UI", "San Fancisco", "Ubuntu", sans-serif; --font-display-20: 300 36px var(--font-family-default); @@ -57,12 +69,95 @@ } } +#feed-subscription { + width: 100%; + max-width: 80ch; + margin: 0 auto 32px auto; + padding: 0 16px; + white-space: nowrap; +} + +#feed-subscription fieldset[name="main"] { + display: flex; + align-items: baseline; + margin: 0; + padding: 0; + border: none; +} + +#feed-subscription fieldset[name="main"] > * + * { + margin-left: 8px; +} + +#feed-reader-selection { + -moz-appearance: none; + appearance: none; + border: none; + border-radius: 2px; + padding: 0 28px 0 8px; + height: 32px; + min-width: 20ch; + color: var(--secondary-button-color); + background-color: var(--secondary-button-background-color); + background-image: url('../images/arrow.svg'); + background-repeat: no-repeat; + background-position: center right 8px; + background-size: 12px; + flex-grow: 1; + flex-shrink: 1; + text-overflow: ellipsis; +} + +#feed-reader-selection:not(:disabled):hover { + background-color: var(--secondary-button-hover-background-color); +} + +#feed-reader-selection:not(:disabled):active { + background-color: var(--secondary-button-active-background-color); +} + +#feed-reader-selection option:hover, +#feed-reader-selection option:active, +#feed-reader-selection option:focus { + background-color: red; +} + +#subscribe { + color: var(--primary-button-color); + background-color: var(--primary-button-background-color); + border-radius: 2px; + padding: 0 8px; + height: 32px; + min-width: 132px; + text-align: center; + border: none; +} + +#subscribe:not(:disabled):hover { + background-color: var(--primary-button-hover-background-color); +} + +#feed-reader-selection:not(:disabled):focus, +#subscribe:not(:disabled):focus { + box-shadow: var(--button-focus-shadow); +} + +#subscribe:not(:disabled):active { + background-color: var(--primary-button-active-background-color); +} + +#feed-reader-selection:disabled, +#subscribe:disabled { + opacity: .4; +} + #feed-header { width: 100%; max-width: 80ch; min-width: 40ch; padding: 16px; margin: 0 auto; + border-top: 1px solid var(--grey-90-a10); } #feed-logo { @@ -136,6 +231,7 @@ width: 16px; height: 16px; flex: 0 0 16px; + transform: rotate(-90deg); transition: 100ms; } @@ -144,7 +240,7 @@ } details.entry[open] > summary::before { - transform: rotate(90deg); + transform: rotate(0deg); transition: 100ms; }