From be3c285863bc44d23e2263be4f6fb1f168c520bf Mon Sep 17 00:00:00 2001 From: Alessandro Amella <44754837+alessandroamella@users.noreply.github.com> Date: Sun, 11 Jan 2026 02:17:07 +0100 Subject: [PATCH 1/2] feat: add dark theme support and format files --- background.js | 133 ++++--- biome.json | 35 ++ manifest.json | 119 +++--- popup/popup.js | 98 ++--- preview.css | 84 +++- rsspreview.js | 904 +++++++++++++++++++++--------------------- settings/options.html | 80 ++-- settings/options.js | 105 +++-- 8 files changed, 819 insertions(+), 739 deletions(-) create mode 100644 biome.json diff --git a/background.js b/background.js index eccbb93..dcf7683 100644 --- a/background.js +++ b/background.js @@ -1,23 +1,30 @@ function detectFeed(event) { + let isfeed = false; + let cache_idx = null; - if (event.statusCode == 301 || event.statusCode == 302) + if (event.statusCode === 301 || event.statusCode === 302) return { responseHeaders: event.responseHeaders }; - // force application/rss+xml to text/xml so the browser displays it instead of downloading - let isfeed = false; - for (let header of event.responseHeaders) { - if (header.name.toLowerCase() == 'content-type') { + for (const header of event.responseHeaders) { + if (header.name.toLowerCase() === "content-type") { if (header.value.match(/application\/((x-)?rss|atom)\+xml/)) { header.value = header.value.replace( /application\/((x-)?rss|atom)\+xml/, - 'text/xml' + "text/xml", ); isfeed = true; - } - else if (header.value.toLowerCase() == 'text/xml' || header.value.toLowerCase() == 'application/xml' ) { - if (event.url.endsWith(".rss") || event.url.endsWith(".rss.xml") || event.url.endsWith(".atom") || event.url.endsWith(".atom.xml")) { + } else if ( + header.value.toLowerCase() === "text/xml" || + header.value.toLowerCase() === "application/xml" + ) { + if ( + event.url.endsWith(".rss") || + event.url.endsWith(".rss.xml") || + event.url.endsWith(".atom") || + event.url.endsWith(".atom.xml") + ) { isfeed = true; } } @@ -26,22 +33,21 @@ function detectFeed(event) { } if (isfeed) { - - var cache_idx = null; - for (let i = 0; i < event.responseHeaders.length; i++) { - if (event.responseHeaders[i].name.toLowerCase() == 'cache-control') { + if (event.responseHeaders[i].name.toLowerCase() === "cache-control") { cache_idx = i; - } - else if (event.responseHeaders[i].name.toLowerCase() == 'content-security-policy') { - + } else if ( + event.responseHeaders[i].name.toLowerCase() === + "content-security-policy" + ) { try { - let options = JSON.parse(localStorage.getItem('options')); + const options = JSON.parse(localStorage.getItem("options")); if (options.enableCss && options.bypassCSP) - event.responseHeaders[i].value = patchCSP(event.responseHeaders[i].value); - } - catch(e) { + event.responseHeaders[i].value = patchCSP( + event.responseHeaders[i].value, + ); + } catch (e) { console.log(e); } } @@ -54,63 +60,60 @@ function detectFeed(event) { // don't cache requests we modified // otherwise on reload the content-type won't be modified again event.responseHeaders.push({ - name: 'Cache-Control', - value: 'no-cache, no-store, must-revalidate', + name: "Cache-Control", + value: "no-cache, no-store, must-revalidate", }); } return { responseHeaders: event.responseHeaders }; - - } const browser = window.browser || window.chrome; browser.webRequest.onHeadersReceived.addListener( detectFeed, - { urls: [''], types: ['main_frame'] }, - ['blocking', 'responseHeaders'] + { urls: [""], types: ["main_frame"] }, + ["blocking", "responseHeaders"], ); - -function handleMessage(request, sender, sendResponse) { - +function handleMessage(request, sender, _sendResponse) { browser.runtime.getPlatformInfo().then((info) => { - - let android = info.os == "android" - browser.storage.sync.get({orangeIcon: android}).then(function(options){ - - let popup = new URL(browser.runtime.getURL('popup/popup.html')); - popup.searchParams.set('tabId', sender.tab.id.toString()); - popup.searchParams.set('feeds', JSON.stringify(request)); - - if (options.orangeIcon) { - browser.pageAction.setIcon({tabId: sender.tab.id, path: { - "19": "icons/rss-19.png", - "38": "icons/rss-38.png" - } + const android = info.os === "android"; + browser.storage.sync.get({ orangeIcon: android }).then((options) => { + const popup = new URL(browser.runtime.getURL("popup/popup.html")); + popup.searchParams.set("tabId", sender.tab.id.toString()); + popup.searchParams.set("feeds", JSON.stringify(request)); + + if (options.orangeIcon) { + browser.pageAction.setIcon({ + tabId: sender.tab.id, + path: { + 19: "icons/rss-19.png", + 38: "icons/rss-38.png", + }, + }); + } + browser.pageAction.setPopup({ + tabId: sender.tab.id, + popup: popup.toString(), }); - } - browser.pageAction.setPopup( {tabId: sender.tab.id, popup: popup.toString() }); - browser.pageAction.show(sender.tab.id); - - //sendResponse({response: "Response from background script to tab " + sender.tab.url , id: sender.tab.id }); + browser.pageAction.show(sender.tab.id); - }); + //sendResponse({response: "Response from background script to tab " + sender.tab.url , id: sender.tab.id }); + }); }); } browser.runtime.onMessage.addListener(handleMessage); - function parseCSP(csp) { - let res = {}; + const res = {}; - let directives = csp.split(";"); - for (let directive of directives) { - let kw = directive.trim().split(/\s+/g); - let key = kw.shift(); - let values = res[key] || []; + const directives = csp.split(";"); + for (const directive of directives) { + const kw = directive.trim().split(/\s+/g); + const key = kw.shift(); + const values = res[key] || []; res[key] = values.concat(kw); } @@ -118,25 +121,25 @@ function parseCSP(csp) { } function patchCSP(csp) { - let parsed_csp = parseCSP(csp); + const parsed_csp = parseCSP(csp); - let stylesrc = parsed_csp['style-src'] || []; - if (! stylesrc.includes("'unsafe-inline'") ) { - let newstylesrc = ["'unsafe-inline'"]; + const stylesrc = parsed_csp["style-src"] || []; + if (!stylesrc.includes("'unsafe-inline'")) { + const newstylesrc = ["'unsafe-inline'"]; - for (let src of stylesrc) { - if (!src.startsWith("'nonce") && !src.startsWith('sha')) + for (const src of stylesrc) { + if (!src.startsWith("'nonce") && !src.startsWith("sha")) newstylesrc.push(src); } - parsed_csp['style-src'] = newstylesrc; + parsed_csp["style-src"] = newstylesrc; let new_csp = ""; - for (let kw in parsed_csp) { - new_csp += kw + " " + parsed_csp[kw].join(" ") + "; "; + for (const kw in parsed_csp) { + new_csp += `${kw} ${parsed_csp[kw].join(" ")}; `; } - new_csp = new_csp.substring(0, new_csp.length-2); + new_csp = new_csp.substring(0, new_csp.length - 2); return new_csp; } return csp; diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..ff6a67a --- /dev/null +++ b/biome.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/manifest.json b/manifest.json index 0b66794..28961fb 100644 --- a/manifest.json +++ b/manifest.json @@ -1,59 +1,60 @@ -{ - - "manifest_version": 2, - "name": "RSSPreview", - "version": "3.34", - "author": "Aurelien David", - "homepage_url": "https://github.com/aureliendavid/rsspreview", - - "description": "Preview RSS feeds in browser", - - - "icons": { - "32": "icons/rss-32.png", - "48": "icons/rss-48.png", - "64": "icons/rss-64.png", - "128": "icons/rss-128.png", - "256": "icons/rss-256.png" - }, - - "background": { - "scripts": ["background.js"] - }, - - - "content_scripts": [ - { - "matches": [""], - "js": ["rsspreview.js"] - } - ], - - "web_accessible_resources": ["preview.css", "rss.xsl", "icons/*.png"], - - - "options_ui": { - "page": "settings/options.html" - }, - - "page_action": { - "browser_style": true, - "default_icon": { - "19": "icons/rss-gray-19.png", - "38": "icons/rss-gray-38.png" - }, - "default_title": "Feeds in page" - }, - - "browser_specific_settings": { - "gecko": { - "id": "{7799824a-30fe-4c67-8b3e-7094ea203c94}" - }, - "gecko_android": { - "strict_min_version": "113.0" - } - }, - - "permissions": ["", "webRequest", "webRequestBlocking", "storage", "tabs"] - -} +{ + "manifest_version": 2, + "name": "RSSPreview", + "version": "3.34", + "author": "Aurelien David", + "homepage_url": "https://github.com/aureliendavid/rsspreview", + + "description": "Preview RSS feeds in browser", + + "icons": { + "32": "icons/rss-32.png", + "48": "icons/rss-48.png", + "64": "icons/rss-64.png", + "128": "icons/rss-128.png", + "256": "icons/rss-256.png" + }, + + "background": { + "scripts": ["background.js"] + }, + + "content_scripts": [ + { + "matches": [""], + "js": ["rsspreview.js"] + } + ], + + "web_accessible_resources": ["preview.css", "rss.xsl", "icons/*.png"], + + "options_ui": { + "page": "settings/options.html" + }, + + "page_action": { + "browser_style": true, + "default_icon": { + "19": "icons/rss-gray-19.png", + "38": "icons/rss-gray-38.png" + }, + "default_title": "Feeds in page" + }, + + "browser_specific_settings": { + "gecko": { + "id": "{7799824a-30fe-4c67-8b3e-7094ea203c94}" + }, + "gecko_android": { + "strict_min_version": "113.0" + } + }, + + "permissions": [ + "", + "webRequest", + "webRequestBlocking", + "storage", + "tabs" + ] +} diff --git a/popup/popup.js b/popup/popup.js index 3bb7f5c..cb82435 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -1,80 +1,58 @@ - var android = false; browser.runtime.getPlatformInfo().then((info) => { - android = info.os == "android" + android = info.os === "android"; }); - -document.addEventListener("DOMContentLoaded", function(event) { - - - const feedList = document.getElementById('feedList'); +document.addEventListener("DOMContentLoaded", (_event) => { + const feedList = document.getElementById("feedList"); const url = new URL(location.href); // `+` converts the string to an number - const tabId = +url.searchParams.get('tabId'); - const feeds = JSON.parse(url.searchParams.get('feeds')); + const tabId = +url.searchParams.get("tabId"); + const feeds = JSON.parse(url.searchParams.get("feeds")); browser.runtime.getPlatformInfo().then((info) => { - android = info.os == "android"; - - for (feed_url in feeds) { - if (feeds.hasOwnProperty(feed_url)) { - - let li = document.createElement("div"); - li.classList.add("panel-list-item"); - li.setAttribute("data-href", feed_url); - - let a = document.createElement("div"); - a.classList.add("text"); - a.innerText = feeds[feed_url]; - - li.appendChild(a); - - if (android) - li.classList.add("android-feed-btn"); - - feedList.appendChild(li); - } - } + android = info.os === "android"; + for (feed_url in feeds) { + if (Object.hasOwn(feeds, feed_url)) { + const li = document.createElement("div"); + li.classList.add("panel-list-item"); + li.setAttribute("data-href", feed_url); + const a = document.createElement("div"); + a.classList.add("text"); + a.innerText = feeds[feed_url]; + li.appendChild(a); - browser.storage.sync.get({newTab: !android}).then(function(options) { + if (android) li.classList.add("android-feed-btn"); - document.querySelectorAll(".panel-list-item").forEach( (elem) => { - - function onUpdated(tab) { - } - - function onError(error) { + feedList.appendChild(li); + } } - elem.addEventListener('click', (event) => { - - let url = elem.getAttribute("data-href"); - if (url) { - if (options.newTab) { - var params = { url: url } ; - if (!android) { - params.openerTabId = tabId ; + browser.storage.sync.get({ newTab: !android }).then((options) => { + document.querySelectorAll(".panel-list-item").forEach((elem) => { + function onUpdated(_tab) {} + + function onError(_error) {} + + elem.addEventListener("click", (_event) => { + const url = elem.getAttribute("data-href"); + const params = { url: url }; + if (url) { + if (options.newTab) { + if (!android) { + params.openerTabId = tabId; + } + browser.tabs.create(params); + } else browser.tabs.update({ url: url }).then(onUpdated, onError); } - browser.tabs.create(params); - } - else - browser.tabs.update({url: url}).then(onUpdated, onError); - } - if (android) - window.close(); - - }); - - }); // end forall - - }); // end options - + if (android) window.close(); + }); + }); // end forall + }); // end options }); // and getplatform - }); diff --git a/preview.css b/preview.css index 5baad65..267585c 100644 --- a/preview.css +++ b/preview.css @@ -1,11 +1,17 @@ html { - font: 3mm tahoma,arial,helvetica,sans-serif; + font: + 3mm tahoma, + arial, + helvetica, + sans-serif; background-color: rgb(240, 240, 240); color: rgb(0, 0, 0); box-sizing: border-box; } -*, *:before, *:after { +*, +*:before, +*:after { box-sizing: inherit; } @@ -18,7 +24,7 @@ body { h1 { font-size: 160%; border-bottom: 2px solid ThreeDLightShadow; - margin: 0 0 .2em 0; + margin: 0 0 0.2em 0; } h1 a { @@ -34,7 +40,7 @@ h2 { color: GrayText; font-size: 110%; font-weight: normal; - margin: 0 0 .6em 0; + margin: 0 0 0.6em 0; } a[href] img { @@ -46,13 +52,13 @@ pre { word-break: break-all; } - code { overflow: auto; white-space: inherit; } -code > pre, pre > code { +code > pre, +pre > code { display: block; overflow: auto; white-space: pre; @@ -72,7 +78,7 @@ img { #feedTitleLink { float: right; - margin-inline-start: .6em; + margin-inline-start: 0.6em; margin-inline-end: 0; margin-top: 0; margin-bottom: 0; @@ -80,13 +86,13 @@ img { #feedTitleContainer { margin-inline-start: 0; - margin-inline-end: .6em; + margin-inline-end: 0.6em; margin-top: 0; margin-bottom: 0; } #feedTitleImage { - margin-inline-start: .6em; + margin-inline-start: 0.6em; margin-inline-end: 0; margin-top: 0; margin-bottom: 0; @@ -99,13 +105,13 @@ img { } .link { - color: #0000FF; + color: #0000ff; text-decoration: underline; cursor: pointer; } .link:hover:active { - color: #FF0000; + color: #ff0000; } .lastUpdated { @@ -160,7 +166,6 @@ img { #feedLastUpdate { color: GrayText; font-size: 87%; - } .author { @@ -170,14 +175,10 @@ img { padding-top: 3px; } - - - - /** -* browser style classes not present on android and might be deprecated at some point -* (here as reference for now) -*/ + * browser style classes not present on android and might be deprecated at some point + * (here as reference for now) + */ /* .panel-section { @@ -256,3 +257,48 @@ img { .panel-section-list .panel-section-separator { margin: 4px 0; } */ + +/* Dark theme overrides */ +html.dark { + background-color: #1a1a1a; + color: #e0e0e0; + scrollbar-color: #444 #1a1a1a; +} + +html.dark body { + background-color: #1a1a1a; +} + +html.dark h1 { + border-bottom: 2px solid #444; +} + +html.dark h2 { + color: #aaa; +} + +html.dark #feedBody { + background-color: #242424; + border: 1px solid #444; + color: #ddd; +} + +html.dark .link, +html.dark a { + color: #66b3ff; +} + +html.dark .link:hover:active, +html.dark a:hover:active { + color: #ff6666; +} + +html.dark .enclosures { + background-color: #2a2a2a; + border: 1px solid #444; +} + +html.dark #feedLastUpdate, +html.dark .lastUpdated { + color: #999; +} diff --git a/rsspreview.js b/rsspreview.js index 31869c2..9b89dfc 100644 --- a/rsspreview.js +++ b/rsspreview.js @@ -1,449 +1,455 @@ -(function() { - /** - * Check and set a global guard variable. - * If this content script is injected into the same page again, - * it will do nothing next time. - */ - if (window.hasRun) { - console.log('already run'); - return; - } - - window.hasRun = true; - - // defaults - var options = { - doThumb: false, - doMaxWidth: true, - valMaxWidth: "900px", - doDetect: true, - preventPreview: false, - fullPreview: false, - doAuthor: false, - enableCss: false, - bypassCSP: false, - customCss: null, - newTab: true - }; - - let xml_parser = new XMLSerializer(); - let html_parser = new DOMParser(); - - function xhrdoc(url, type, cb) { - let xhr = new XMLHttpRequest(); - xhr.open('GET', url, true); - - xhr.responseType = 'document'; - xhr.overrideMimeType('text/' + type); - - xhr.onload = () => { - if (xhr.readyState === xhr.DONE) { - if (xhr.status === 200) { - let resp = type == 'xml' ? xhr.responseXML : xhr.response; - cb(resp); - } - } - }; - - xhr.send(null); - } - - function applyxsl(xmlin, xsl, node, doc = document) { - let xsltProcessor = new XSLTProcessor(); - xsltProcessor.importStylesheet(xsl); - xsltProcessor.setParameter(null, 'fullPreview', options.fullPreview); - xsltProcessor.setParameter(null, 'doAuthor', options.doAuthor); - let fragment = xsltProcessor.transformToFragment(xmlin, doc); - node.appendChild(fragment); - } - - function getlang() { - return browser.i18n.getUILanguage(); - } - - function formatsubtitle() { - try { - let feed_desc = document.getElementById('feedSubtitleRaw'); - - let html_desc = html_parser.parseFromString( - '

' + feed_desc.innerText + '

', - 'text/html' - ); - let xml_desc = xml_parser.serializeToString(html_desc.body.firstChild); - - feed_desc.insertAdjacentHTML('afterend', xml_desc); - - feed_desc.parentNode.removeChild(feed_desc); - } catch (e) { - console.error(e); - } - } - - function formatdescriptions(el = document) { - // unescapes descriptions to html then to xml - let tohtml = el.getElementsByClassName('feedRawContent'); - - for (let i = 0; i < tohtml.length; i++) { - - try { - let html_txt = ''; - if (tohtml[i].getAttribute('desctype') == 'text/plain') { - html_txt = '
' + tohtml[i].innerHTML + '
'; - } - else if (tohtml[i].getAttribute('desctype') == 'xhtml') { - html_txt = '
' + tohtml[i].innerHTML + '
'; - } - else { - html_txt = '
' + tohtml[i].textContent + '
'; - } - - let html_desc = html_parser.parseFromString(html_txt, 'text/html'); - let xml_desc = xml_parser.serializeToString( - html_desc.body.firstChild - ); - - tohtml[i].insertAdjacentHTML('afterend', xml_desc); - tohtml[i].setAttribute('todel', 1); - } catch (e) { - console.error(e); - console.log(tohtml[i]); - } - - } - - el.querySelectorAll('.feedRawContent').forEach(a => { - if (a.getAttribute('todel') == '1') { - a.remove(); - } - }); - } - - function removeemptyenclosures(el = document) { - let encs = el.getElementsByClassName('enclosures'); - - for (let i = 0; i < encs.length; i++) - if (!encs[i].firstChild) encs[i].style.display = 'none'; - } - - function formatfilenames(el = document) { - let encfn = el.getElementsByClassName('enclosureFilename'); - - for (let i = 0; i < encfn.length; i++) { - let url = new URL(encfn[i].innerText); - - if (url) { - let fn = url.pathname.split('/').pop(); - - if (fn != '') encfn[i].innerText = fn; - } - } - } - - function formatfilesizes(el = document) { - function humanfilesize(size) { - let i = 0; - - if (size && size != '' && size > 0) - i = Math.floor(Math.log(size) / Math.log(1024)); - - return ( - (size / Math.pow(1024, i)).toFixed(2) * 1 + - ' ' + - ['B', 'kB', 'MB', 'GB', 'TB'][i] - ); - } - - let encsz = el.getElementsByClassName('enclosureSize'); - for (let i = 0; i < encsz.length; i++) { - let hsize = humanfilesize(encsz[i].innerText); - - if (hsize) encsz[i].innerText = hsize; - } - } - - function formattitles(el = document) { - let et = el.getElementsByClassName('entrytitle'); - - for (let i = 0; i < et.length; i++) { - //basically removes html content if there is some - //only do it if there's a tag to avoid doing it when text titles cointain a '&' - //(which can be caught but still displays an error in console, which is annoying) - if (et[i].innerText.indexOf('<') >= 0 || et[i].innerText.indexOf('&')) { - - let tmp = document.createElement('span'); - try { - tmp.innerHTML = et[i].innerText; - et[i].innerText = tmp.textContent; - } catch (e) { - // if not parsable, display as text - console.error(e); - console.log(et[i].innerText); - } - } - } - } - - function formatdates(el = document) { - let lang = getlang(); - if (!lang) return; - - let opts = { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - }; - - let ed = el.getElementsByClassName('lastUpdated'); - for (let i = 0; i < ed.length; i++) { - let d = new Date(ed[i].innerText); - if (isNaN(d)) continue; - - let dstr = - d.toLocaleDateString(lang, opts) + ' ' + d.toLocaleTimeString(lang); - - ed[i].innerText = dstr; - } - - let lu = el.getElementById('feedLastUpdate'); - if (lu && lu.innerText.trim() != "") { - lu.innerText = "Last updated: " + lu.innerText; - } - } - - function extensionimages(el = document) { - let extimgs = el.getElementsByClassName('extImg'); - - for (let i = 0; i < extimgs.length; i++) - extimgs[i].src = chrome.runtime.getURL( - extimgs[i].attributes['data-src'].nodeValue - ); - } - - function applysettings() { - - document.querySelectorAll('.mediaThumb').forEach((elem) => { - elem.style.display = options.doThumb ? "block" : "none"; - }); - - - document.querySelectorAll('img').forEach((elem) => { - if (options.doMaxWidth) - elem.style["max-width"] = options.valMaxWidth; - }); - - } - - function makepreviewhtml() { - let doc = document.implementation.createHTMLDocument(''); - doc.body.id = "rsspreviewBody"; - - let feedBody = doc.createElement('div'); - feedBody.id = 'feedBody'; - doc.body.appendChild(feedBody); - - let css = doc.createElement('link'); - css.setAttribute('rel', 'stylesheet'); - css.setAttribute('href', chrome.runtime.getURL('preview.css')); - doc.head.appendChild(css); - - if (options.enableCss && options.customCss) { - let node = doc.createElement('style'); - node.innerHTML = options.customCss; - doc.head.appendChild(node); - } - - return doc; - } - - function detect() { - let rootNode = document.getRootNode(); - - // for chrome - let d = document.getElementById('webkit-xml-viewer-source-xml'); - if (d && d.firstChild) rootNode = d.firstChild; - - const rootName = rootNode.documentElement.nodeName.toLowerCase(); - - let isRSS1 = false; - - if (rootName == 'rdf' || rootName == 'rdf:rdf') { - if (rootNode.documentElement.attributes['xmlns']) { - isRSS1 = rootNode.documentElement.attributes['xmlns'].nodeValue.search('rss') > 0; - } - - } - - if ( - rootName == 'rss' || - rootName == 'channel' || // rss2 - rootName == 'feed' || // atom - isRSS1 - ) - return rootNode; - - return null; - } - - function main(feedNode) { - let feed_url = window.location.href; - let preview = makepreviewhtml(); - - xhrdoc(chrome.runtime.getURL('rss.xsl'), 'xml', xsl_xml => { - applyxsl(feedNode, xsl_xml, preview.getElementById('feedBody'), preview); - - // replace the content with the preview document - document.replaceChild( - document.importNode(preview.documentElement, true), - document.documentElement - ); - - let t0 = performance.now(); - - formatsubtitle(); - - formatdescriptions(); - removeemptyenclosures(); - formatfilenames(); - formatfilesizes(); - formattitles(); - formatdates(); - extensionimages(); - applysettings(); - - let t1 = performance.now(); - //console.log("exec in: " + (t1 - t0) + "ms"); - - document.title = document.getElementById('feedTitleText').innerText; - }); - } - - - function onOptions(opts) { - options = opts; - - let feedRoot = detect(); - - if (feedRoot && !options.preventPreview) { - - main(feedRoot); - } - - else if (options.doDetect) { - - findFeeds(); - } - - } - - function onError(error) { - console.log(`Error on get options: ${error}`); - } - - let getting = browser.storage.sync.get(options); - getting.then(onOptions, onError); - - - function registerFeeds(feeds) { - if (Object.keys(feeds).length > 0) { - function handleResponse(message) { - } - - function handleError(error) { - //console.log(error); - } - - browser.runtime.sendMessage(feeds).then(handleResponse, handleError); - } - } - - - function findiTunesPodcastsFeeds() { - let match = document.URL.match(/id(\d+)/) - if (match) { - let feeds = {}; - let itunesid = match[1]; - - var xhr = new XMLHttpRequest(); - xhr.open('GET', "https://itunes.apple.com/lookup?id="+itunesid+"&entity=podcast"); - - xhr.onload = function () { - if (xhr.readyState === xhr.DONE) { - if (xhr.status === 200) { - let res = JSON.parse(xhr.responseText); - - if ("results" in res) { - let pod = res["results"][0]; - let title = pod["collectionName"] || document.title; - let url = pod["feedUrl"]; - if (url) { - feeds[url] = title; - } - } - } - } - - registerFeeds(feeds); - }; - xhr.send(); - } - } - - function findYouTubeFeeds() { - // YouTube's canonical channel URLs look like /channel/AlphaNumericID - // It also supports named channels of the form /c/MyChannelName - // and handle links of the form /@MyChannelHandle. - // Match also on '%' to handle non-latin character codes - // Match on both of these to autodetect channel feeds on either URL - let idPattern = /channel\/([a-zA-Z0-9%_-]+)/; - let namePattern = /(?:c|user)\/[a-zA-Z0-9%_-]+/; - let handlePattern = /@[a-zA-Z0-9%_-]+/; - let urlPattern = new RegExp(`${idPattern.source}|${namePattern.source}|${handlePattern.source}`); - if (document.URL.match(urlPattern)) { - let feeds = {}; - let canonicalUrl = document.querySelector("link[rel='canonical']").href; - let channelId = canonicalUrl.match(idPattern)[1]; - let url = `https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`; - let title = document.title; - feeds[url] = title; - registerFeeds(feeds); - } - } - - // The default function used to find feeds if a domain-specific function doesn't exist. - // Parse the document's HTML looking for link tags pointing to the feed URL. - function defaultFindFeeds() { - let feeds = {}; - document.querySelectorAll("link[rel='alternate']").forEach( (elem) => { - let type_attr = elem.getAttribute('type'); - if (!type_attr) { - return; - } - - let type = type_attr.toLowerCase(); - if (type.includes('rss') || type.includes('atom') || type.includes('feed')) { - let title = elem.getAttribute('title'); - let url = elem.href; - - if (url) { - feeds[url] = (title ? title : url); - } - } - }); - registerFeeds(feeds); - } - - const domainFeedFinders = new Map([ - ["itunes.apple.com", findiTunesPodcastsFeeds], - ["podcasts.apple.com", findiTunesPodcastsFeeds], - ["www.youtube.com", findYouTubeFeeds], - ]); - - function findFeeds() { - // Look up a feed detection function based on the domain. - // If a domain-specific function doesn't exist, fall back to a default. - let finder = domainFeedFinders.get(document.domain) || defaultFindFeeds; - finder(); - } - -})(); +(() => { + /** + * Check and set a global guard variable. + * If this content script is injected into the same page again, + * it will do nothing next time. + */ + if (window.hasRun) { + console.log("already run"); + return; + } + + window.hasRun = true; + + // Detect system theme for the initial default + const systemDarkDefault = window.matchMedia( + "(prefers-color-scheme: dark)", + ).matches; + + // defaults + var options = { + doThumb: false, + doMaxWidth: true, + valMaxWidth: "900px", + doDetect: true, + preventPreview: false, + fullPreview: false, + doAuthor: false, + darkTheme: systemDarkDefault, // DYNAMIC DEFAULT + enableCss: false, + bypassCSP: false, + customCss: null, + newTab: true, + }; + + const xml_parser = new XMLSerializer(); + const html_parser = new DOMParser(); + + function xhrdoc(url, type, cb) { + const xhr = new XMLHttpRequest(); + xhr.open("GET", url, true); + + xhr.responseType = "document"; + xhr.overrideMimeType(`text/${type}`); + + xhr.onload = () => { + if (xhr.readyState === xhr.DONE) { + if (xhr.status === 200) { + const resp = type === "xml" ? xhr.responseXML : xhr.response; + cb(resp); + } + } + }; + + xhr.send(null); + } + + function applyxsl(xmlin, xsl, node, doc = document) { + const xsltProcessor = new XSLTProcessor(); + xsltProcessor.importStylesheet(xsl); + xsltProcessor.setParameter(null, "fullPreview", options.fullPreview); + xsltProcessor.setParameter(null, "doAuthor", options.doAuthor); + const fragment = xsltProcessor.transformToFragment(xmlin, doc); + node.appendChild(fragment); + } + + function getlang() { + return browser.i18n.getUILanguage(); + } + + function formatsubtitle() { + try { + const feed_desc = document.getElementById("feedSubtitleRaw"); + + const html_desc = html_parser.parseFromString( + `

${feed_desc.innerText}

`, + "text/html", + ); + const xml_desc = xml_parser.serializeToString(html_desc.body.firstChild); + + feed_desc.insertAdjacentHTML("afterend", xml_desc); + + feed_desc.parentNode.removeChild(feed_desc); + } catch (e) { + console.error(e); + } + } + + function formatdescriptions(el = document) { + // unescapes descriptions to html then to xml + const tohtml = el.getElementsByClassName("feedRawContent"); + + for (let i = 0; i < tohtml.length; i++) { + try { + let html_txt = ""; + if (tohtml[i].getAttribute("desctype") === "text/plain") { + html_txt = + '
' + + tohtml[i].innerHTML + + "
"; + } else if (tohtml[i].getAttribute("desctype") === "xhtml") { + html_txt = `
${tohtml[i].innerHTML}
`; + } else { + html_txt = `
${tohtml[i].textContent}
`; + } + + const html_desc = html_parser.parseFromString(html_txt, "text/html"); + const xml_desc = xml_parser.serializeToString( + html_desc.body.firstChild, + ); + + tohtml[i].insertAdjacentHTML("afterend", xml_desc); + tohtml[i].setAttribute("todel", 1); + } catch (e) { + console.error(e); + console.log(tohtml[i]); + } + } + + el.querySelectorAll(".feedRawContent").forEach((a) => { + if (a.getAttribute("todel") === "1") { + a.remove(); + } + }); + } + + function removeemptyenclosures(el = document) { + const encs = el.getElementsByClassName("enclosures"); + + for (let i = 0; i < encs.length; i++) + if (!encs[i].firstChild) encs[i].style.display = "none"; + } + + function formatfilenames(el = document) { + const encfn = el.getElementsByClassName("enclosureFilename"); + + for (let i = 0; i < encfn.length; i++) { + const url = new URL(encfn[i].innerText); + + if (url) { + const fn = url.pathname.split("/").pop(); + + if (fn !== "") encfn[i].innerText = fn; + } + } + } + + function formatfilesizes(el = document) { + function humanfilesize(size) { + let i = 0; + + if (size && size !== "" && size > 0) + i = Math.floor(Math.log(size) / Math.log(1024)); + + return ( + (size / 1024 ** i).toFixed(2) * 1 + + " " + + ["B", "kB", "MB", "GB", "TB"][i] + ); + } + + const encsz = el.getElementsByClassName("enclosureSize"); + for (let i = 0; i < encsz.length; i++) { + const hsize = humanfilesize(encsz[i].innerText); + + if (hsize) encsz[i].innerText = hsize; + } + } + + function formattitles(el = document) { + const et = el.getElementsByClassName("entrytitle"); + + for (let i = 0; i < et.length; i++) { + //basically removes html content if there is some + //only do it if there's a tag to avoid doing it when text titles cointain a '&' + //(which can be caught but still displays an error in console, which is annoying) + if ( + et[i].innerText.indexOf("<") >= 0 || + et[i].innerText.indexOf("&") + ) { + const tmp = document.createElement("span"); + try { + tmp.innerHTML = et[i].innerText; + et[i].innerText = tmp.textContent; + } catch (e) { + // if not parsable, display as text + console.error(e); + console.log(et[i].innerText); + } + } + } + } + + function formatdates(el = document) { + const lang = getlang(); + if (!lang) return; + + const opts = { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }; + + const ed = el.getElementsByClassName("lastUpdated"); + for (let i = 0; i < ed.length; i++) { + const d = new Date(ed[i].innerText); + if (Number.isNaN(d)) continue; + + const dstr = `${d.toLocaleDateString(lang, opts)} ${d.toLocaleTimeString(lang)}`; + + ed[i].innerText = dstr; + } + + const lu = el.getElementById("feedLastUpdate"); + if (lu && lu.innerText.trim() !== "") { + lu.innerText = `Last updated: ${lu.innerText}`; + } + } + + function extensionimages(el = document) { + const extimgs = el.getElementsByClassName("extImg"); + + for (let i = 0; i < extimgs.length; i++) + extimgs[i].src = chrome.runtime.getURL( + extimgs[i].attributes["data-src"].nodeValue, + ); + } + + function applysettings() { + document.querySelectorAll(".mediaThumb").forEach((elem) => { + elem.style.display = options.doThumb ? "block" : "none"; + }); + + document.querySelectorAll("img").forEach((elem) => { + if (options.doMaxWidth) elem.style["max-width"] = options.valMaxWidth; + }); + } + + function makepreviewhtml() { + const doc = document.implementation.createHTMLDocument(""); + doc.body.id = "rsspreviewBody"; + + if (options.darkTheme) { + doc.documentElement.classList.add("dark"); + } + + const feedBody = doc.createElement("div"); + feedBody.id = "feedBody"; + doc.body.appendChild(feedBody); + + const css = doc.createElement("link"); + css.setAttribute("rel", "stylesheet"); + css.setAttribute("href", chrome.runtime.getURL("preview.css")); + doc.head.appendChild(css); + + if (options.enableCss && options.customCss) { + const node = doc.createElement("style"); + node.innerHTML = options.customCss; + doc.head.appendChild(node); + } + + return doc; + } + + function detect() { + let rootNode = document.getRootNode(); + + // for chrome + const d = document.getElementById("webkit-xml-viewer-source-xml"); + if (d?.firstChild) rootNode = d.firstChild; + + const rootName = rootNode.documentElement.nodeName.toLowerCase(); + + let isRSS1 = false; + + if (rootName === "rdf" || rootName === "rdf:rdf") { + if (rootNode.documentElement.attributes.xmlns) { + isRSS1 = + rootNode.documentElement.attributes.xmlns.nodeValue.search("rss") > 0; + } + } + + if ( + rootName === "rss" || + rootName === "channel" || // rss2 + rootName === "feed" || // atom + isRSS1 + ) + return rootNode; + + return null; + } + + function main(feedNode) { + const _feed_url = window.location.href; + const preview = makepreviewhtml(); + + xhrdoc(chrome.runtime.getURL("rss.xsl"), "xml", (xsl_xml) => { + applyxsl(feedNode, xsl_xml, preview.getElementById("feedBody"), preview); + + // replace the content with the preview document + document.replaceChild( + document.importNode(preview.documentElement, true), + document.documentElement, + ); + + const _t0 = performance.now(); + + formatsubtitle(); + + formatdescriptions(); + removeemptyenclosures(); + formatfilenames(); + formatfilesizes(); + formattitles(); + formatdates(); + extensionimages(); + applysettings(); + + const _t1 = performance.now(); + //console.log("exec in: " + (t1 - t0) + "ms"); + + document.title = document.getElementById("feedTitleText").innerText; + }); + } + + function onOptions(opts) { + options = opts; + + const feedRoot = detect(); + + if (feedRoot && !options.preventPreview) { + main(feedRoot); + } else if (options.doDetect) { + findFeeds(); + } + } + + function onError(error) { + console.log(`Error on get options: ${error}`); + } + + const getting = browser.storage.sync.get(options); + getting.then(onOptions, onError); + + function registerFeeds(feeds) { + if (Object.keys(feeds).length > 0) { + function handleResponse(_message) {} + + function handleError(_error) { + //console.log(error); + } + + browser.runtime.sendMessage(feeds).then(handleResponse, handleError); + } + } + + function findiTunesPodcastsFeeds() { + var xhr; + const match = document.URL.match(/id(\d+)/); + if (match) { + const feeds = {}; + const itunesid = match[1]; + + xhr = new XMLHttpRequest(); + xhr.open( + "GET", + `https://itunes.apple.com/lookup?id=${itunesid}&entity=podcast`, + ); + + xhr.onload = () => { + if (xhr.readyState === xhr.DONE) { + if (xhr.status === 200) { + const res = JSON.parse(xhr.responseText); + + if ("results" in res) { + const pod = res.results[0]; + const title = pod.collectionName || document.title; + const url = pod.feedUrl; + if (url) { + feeds[url] = title; + } + } + } + } + + registerFeeds(feeds); + }; + xhr.send(); + } + } + + function findYouTubeFeeds() { + // YouTube's canonical channel URLs look like /channel/AlphaNumericID + // It also supports named channels of the form /c/MyChannelName + // and handle links of the form /@MyChannelHandle. + // Match also on '%' to handle non-latin character codes + // Match on both of these to autodetect channel feeds on either URL + const idPattern = /channel\/([a-zA-Z0-9%_-]+)/; + const namePattern = /(?:c|user)\/[a-zA-Z0-9%_-]+/; + const handlePattern = /@[a-zA-Z0-9%_-]+/; + const urlPattern = new RegExp( + `${idPattern.source}|${namePattern.source}|${handlePattern.source}`, + ); + if (document.URL.match(urlPattern)) { + const feeds = {}; + const canonicalUrl = document.querySelector("link[rel='canonical']").href; + const channelId = canonicalUrl.match(idPattern)[1]; + const url = `https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`; + const title = document.title; + feeds[url] = title; + registerFeeds(feeds); + } + } + + // The default function used to find feeds if a domain-specific function doesn't exist. + // Parse the document's HTML looking for link tags pointing to the feed URL. + function defaultFindFeeds() { + const feeds = {}; + document.querySelectorAll("link[rel='alternate']").forEach((elem) => { + const type_attr = elem.getAttribute("type"); + if (!type_attr) { + return; + } + + const type = type_attr.toLowerCase(); + if ( + type.includes("rss") || + type.includes("atom") || + type.includes("feed") + ) { + const title = elem.getAttribute("title"); + const url = elem.href; + + if (url) { + feeds[url] = title ? title : url; + } + } + }); + registerFeeds(feeds); + } + + const domainFeedFinders = new Map([ + ["itunes.apple.com", findiTunesPodcastsFeeds], + ["podcasts.apple.com", findiTunesPodcastsFeeds], + ["www.youtube.com", findYouTubeFeeds], + ]); + + function findFeeds() { + // Look up a feed detection function based on the domain. + // If a domain-specific function doesn't exist, fall back to a default. + const finder = domainFeedFinders.get(document.domain) || defaultFindFeeds; + finder(); + } +})(); diff --git a/settings/options.html b/settings/options.html index d6f4b82..0644084 100644 --- a/settings/options.html +++ b/settings/options.html @@ -1,39 +1,51 @@ - - - - - - - -
- -
- -
- -
- -
- -
- - - - -
- -
- -
- - - - + + + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ + + + +
+ +
+ +
+ + + + diff --git a/settings/options.js b/settings/options.js index 3b99414..0d7fd97 100644 --- a/settings/options.js +++ b/settings/options.js @@ -1,14 +1,13 @@ - var android = false; browser.runtime.getPlatformInfo().then((info) => { - android = info.os == "android" + android = info.os === "android"; }); function saveOptions(e) { e.preventDefault(); - let options = { + const options = { doThumb: document.querySelector("#doThumb").checked, doMaxWidth: document.querySelector("#doMaxWidth").checked, valMaxWidth: document.querySelector("#valMaxWidth").value, @@ -16,70 +15,70 @@ function saveOptions(e) { preventPreview: document.querySelector("#preventPreview").checked, fullPreview: document.querySelector("#fullPreview").checked, doAuthor: document.querySelector("#doAuthor").checked, + darkTheme: document.querySelector("#darkTheme").checked, orangeIcon: document.querySelector("#orangeIcon").checked, enableCss: document.querySelector("#enableCss").checked, bypassCSP: document.querySelector("#bypassCSP").checked, customCss: document.querySelector("#customCss").value, - newTab: document.querySelector("#newTab").checked + newTab: document.querySelector("#newTab").checked, }; browser.storage.sync.set(options); - localStorage.setItem('options', JSON.stringify(options)); - + localStorage.setItem("options", JSON.stringify(options)); } - function restoreOptions() { - browser.runtime.getPlatformInfo().then((info) => { - android = info.os == "android" - - - function onResult(result) { - document.querySelector("#doThumb").checked = result.doThumb; - document.querySelector("#doMaxWidth").checked = result.doMaxWidth; - document.querySelector("#valMaxWidth").value = result.valMaxWidth; - document.querySelector("#doDetect").checked = result.doDetect; - document.querySelector("#preventPreview").checked = result.preventPreview; - document.querySelector("#fullPreview").checked = result.fullPreview; - document.querySelector("#doAuthor").checked = result.doAuthor; - document.querySelector("#orangeIcon").checked = result.orangeIcon; - document.querySelector("#enableCss").checked = result.enableCss; - document.querySelector("#bypassCSP").checked = result.bypassCSP; - document.querySelector("#customCss").value = result.customCss; - document.querySelector("#newTab").checked = result.newTab; - - localStorage.setItem('options', JSON.stringify(result)); - } - - function onError(error) { - console.log(`Error: ${error}`); - } - - var getting = browser.storage.sync.get({ - doThumb: false, - doMaxWidth: true, - valMaxWidth: "900px", - doDetect: true, - preventPreview: false, - fullPreview: false, - doAuthor: false, - orangeIcon: android, - enableCss: false, - bypassCSP: false, - customCss: null, - newTab: !android - }); - getting.then(onResult, onError); - + android = info.os === "android"; + + // Detect if the system is currently in dark mode + const systemDarkDefault = window.matchMedia( + "(prefers-color-scheme: dark)", + ).matches; + + function onResult(result) { + document.querySelector("#doThumb").checked = result.doThumb; + document.querySelector("#doMaxWidth").checked = result.doMaxWidth; + document.querySelector("#valMaxWidth").value = result.valMaxWidth; + document.querySelector("#doDetect").checked = result.doDetect; + document.querySelector("#preventPreview").checked = result.preventPreview; + document.querySelector("#fullPreview").checked = result.fullPreview; + document.querySelector("#doAuthor").checked = result.doAuthor; + document.querySelector("#darkTheme").checked = result.darkTheme; + document.querySelector("#orangeIcon").checked = result.orangeIcon; + document.querySelector("#enableCss").checked = result.enableCss; + document.querySelector("#bypassCSP").checked = result.bypassCSP; + document.querySelector("#customCss").value = result.customCss; + document.querySelector("#newTab").checked = result.newTab; + + localStorage.setItem("options", JSON.stringify(result)); + } + + function onError(error) { + console.log(`Error: ${error}`); + } + + var getting = browser.storage.sync.get({ + doThumb: false, + doMaxWidth: true, + valMaxWidth: "900px", + doDetect: true, + preventPreview: false, + fullPreview: false, + doAuthor: false, + darkTheme: systemDarkDefault, // DYNAMIC DEFAULT + orangeIcon: android, + enableCss: false, + bypassCSP: false, + customCss: null, + newTab: !android, + }); + getting.then(onResult, onError); }); - } - - document.addEventListener("DOMContentLoaded", restoreOptions); -document.querySelectorAll('.validate').forEach((elem) => { - elem.addEventListener('change', saveOptions); +document.querySelectorAll(".validate").forEach((elem) => { + elem.addEventListener("change", saveOptions); }); From 8c38f3507d01e9c0cfdf6a089fb68bed2b9d3b55 Mon Sep 17 00:00:00 2001 From: Alessandro Amella <44754837+alessandroamella@users.noreply.github.com> Date: Sun, 11 Jan 2026 02:25:20 +0100 Subject: [PATCH 2/2] feat: support dark theme in options.html --- settings/options.html | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/settings/options.html b/settings/options.html index 0644084..8056091 100644 --- a/settings/options.html +++ b/settings/options.html @@ -4,6 +4,37 @@ +