about summary refs log tree commit diff stats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--package.json8
-rw-r--r--src/_locales/es/messages.json2
-rw-r--r--src/_locales/fr/messages.json7
-rw-r--r--src/assets/images/19-ekim-yet_8a72cdffcc924121a3a962b2a9794860.xlsxbin19616 -> 0 bytes
-rw-r--r--src/assets/images/bluesky-icon.svg63
-rw-r--r--src/assets/images/tekstowo-icon.svg115
-rw-r--r--src/assets/javascripts/localise.js3
-rw-r--r--src/assets/javascripts/services.js83
-rw-r--r--src/assets/javascripts/utils.js65
-rw-r--r--src/config.json60
-rw-r--r--src/pages/background/background.js2
-rw-r--r--src/pages/options/index.js21
-rw-r--r--src/pages/options/widgets/general.js22
-rw-r--r--src/pages/options/widgets/general.pug18
-rw-r--r--src/pages/stylesheets/styles.css2
15 files changed, 443 insertions, 28 deletions
diff --git a/package.json b/package.json
index 76eb63eb..d388da1f 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
 	},
 	"scripts": {
 		"start": "web-ext run",
-		"start_ar": "web-ext run --firefox=/home/esmail/Downloads/ar/firefox/firefox --pref font.language.group=ar",
+		"start-nightly": "web-ext run --firefox=/home/manerakai/Downloads/firefox/firefox",
 		"build": "web-ext build",
 		"test": "web-ext lint",
 		"html": "pug --basedir ./ --obj ./src/config.json src/pages/options/index.pug --out src/pages/options/ && pug --basedir ./ --obj ./src/config.json src/pages/popup/popup.pug --out src/pages/popup/"
@@ -23,8 +23,8 @@
 	},
 	"homepage": "https://libredirect.github.io",
 	"devDependencies": {
-		"web-ext": "^7.2.0",
-		"pug-cli": "^1.0.0-alpha6"
+		"pug-cli": "^1.0.0-alpha6",
+		"web-ext": "^7.2.0"
 	},
 	"webExt": {
 		"sourceDir": "./src/",
@@ -35,4 +35,4 @@
 			"overwriteDest": true
 		}
 	}
-}
\ No newline at end of file
+}
diff --git a/src/_locales/es/messages.json b/src/_locales/es/messages.json
index 9e63899e..dfe6e093 100644
--- a/src/_locales/es/messages.json
+++ b/src/_locales/es/messages.json
@@ -36,7 +36,7 @@
         "description": "used in the settings page"
     },
     "excludeFromRedirecting": {
-        "message": "Excluido de la redirección",
+        "message": "Excluidos del redireccionamiento",
         "description": "used in the settings page"
     },
     "importSettings": {
diff --git a/src/_locales/fr/messages.json b/src/_locales/fr/messages.json
index daae5dc0..993271aa 100644
--- a/src/_locales/fr/messages.json
+++ b/src/_locales/fr/messages.json
@@ -103,10 +103,11 @@
     "unsupportedIframesHandling": {
         "message": "Unsupported iframes handling"
     },
+    },
     "fetchPublicInstances": {
-        "message": "Fetch public instances"
+        "message": "Rechercher des instances publiques"
     },
     "disable": {
-        "message": "Disable"
+        "message": "Désactiver"
     }
-}
\ No newline at end of file
+}
diff --git a/src/assets/images/19-ekim-yet_8a72cdffcc924121a3a962b2a9794860.xlsx b/src/assets/images/19-ekim-yet_8a72cdffcc924121a3a962b2a9794860.xlsx
deleted file mode 100644
index deb7381b..00000000
--- a/src/assets/images/19-ekim-yet_8a72cdffcc924121a3a962b2a9794860.xlsx
+++ /dev/null
Binary files differdiff --git a/src/assets/images/bluesky-icon.svg b/src/assets/images/bluesky-icon.svg
new file mode 100644
index 00000000..8e916784
--- /dev/null
+++ b/src/assets/images/bluesky-icon.svg
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="512"
+   height="512"
+   viewBox="0 0 135.46666 135.46667"
+   version="1.1"
+   id="svg1"
+   xml:space="preserve"
+   inkscape:version="1.3.1 (91b66b0783, 2023-11-16)"
+   sodipodi:docname="bluesky-icon.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
+     id="namedview1"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:document-units="px"
+     inkscape:zoom="1.1452094"
+     inkscape:cx="358.01312"
+     inkscape:cy="227.46931"
+     inkscape:window-width="1888"
+     inkscape:window-height="1060"
+     inkscape:window-x="32"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1" /><defs
+     id="defs1"><linearGradient
+       id="linearGradient1"
+       inkscape:collect="always"><stop
+         style="stop-color:#0062ff;stop-opacity:1;"
+         offset="0"
+         id="stop1" /><stop
+         style="stop-color:#0090fe;stop-opacity:1;"
+         offset="1"
+         id="stop2" /></linearGradient><linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient1"
+       id="linearGradient2"
+       x1="-16.737301"
+       y1="0.19602649"
+       x2="-16.737301"
+       y2="136.34718"
+       gradientUnits="userSpaceOnUse" /></defs><g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"><rect
+       style="fill:url(#linearGradient2);stroke-width:1.165;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.53144"
+       id="rect1"
+       width="135.46667"
+       height="136.65152"
+       x="-4.9023438e-06"
+       y="-0.59242737"
+       ry="24.716606"
+       rx="24.716606" /></g></svg>
diff --git a/src/assets/images/tekstowo-icon.svg b/src/assets/images/tekstowo-icon.svg
new file mode 100644
index 00000000..c5bc024d
--- /dev/null
+++ b/src/assets/images/tekstowo-icon.svg
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   viewBox="0 0 100 100"
+   version="1.1"
+   id="svg12"
+   sodipodi:docname="tekstowo-icon.svg"
+   width="100"
+   height="100"
+   inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs12" />
+  <sodipodi:namedview
+     id="namedview12"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:zoom="3.3499322"
+     inkscape:cx="35.821621"
+     inkscape:cy="41.34412"
+     inkscape:window-width="1888"
+     inkscape:window-height="1060"
+     inkscape:window-x="32"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="logo_1_" />
+  <style
+     id="style1">.st0{fill:#62ae25}.st1{fill:#999}</style>
+  <g
+     id="logo_1_"
+     transform="translate(0,-24)">
+    <circle
+       style="fill:#ff6600;fill-opacity:1;stroke-width:4.40315;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.53144"
+       id="path12"
+       cx="50"
+       cy="74"
+       r="50" />
+    <linearGradient
+       id="SVGID_1_"
+       gradientUnits="userSpaceOnUse"
+       x1="39.888"
+       y1="92.283997"
+       x2="52.096001"
+       y2="125.824"
+       gradientTransform="translate(0,1.24036)">
+      <stop
+         offset="0"
+         stop-color="#8fe132"
+         id="stop5" />
+      <stop
+         offset=".362"
+         stop-color="#65bd23"
+         id="stop6" />
+      <stop
+         offset=".668"
+         stop-color="#49a519"
+         id="stop7" />
+      <stop
+         offset=".844"
+         stop-color="#3e9c15"
+         id="stop8" />
+    </linearGradient>
+    <path
+       d="m 57.782805,105.82283 c -4.54864,6.22445 -11.4913,8.91772 -15.381584,6.04489 -3.890284,-2.87282 -3.41148,-10.29428 1.137159,-16.518734 4.548641,-6.224454 11.491301,-8.917729 15.381584,-6.044903 3.950135,2.932676 3.411481,10.294291 -1.137159,16.518747 z"
+       fill="url(#SVGID_1_)"
+       id="path8"
+       style="fill:#8fe132;fill-opacity:1;stroke-width:0.999999" />
+    <linearGradient
+       id="SVGID_2_"
+       gradientUnits="userSpaceOnUse"
+       x1="-3.5739999"
+       y1="24.316"
+       x2="82.773003"
+       y2="74.168999"
+       gradientTransform="translate(0,1.24036)">
+      <stop
+         offset=".184"
+         stop-color="#8fe132"
+         id="stop9" />
+      <stop
+         offset="1"
+         stop-color="#3e9c15"
+         id="stop10" />
+    </linearGradient>
+    <path
+       d="M 65.563373,59.857627 C 61.792791,55.308986 56.585794,51.837656 51.43865,50.161842 42.820174,47.348868 40.665555,45.912455 36.59572,42.680526 34.26155,40.82516 32.645587,37.29398 30.670519,36.276521 c -0.957609,-0.478804 -1.556114,-0.05985 -1.915217,0.299252 -0.538654,0.538655 -1.077309,1.675815 -0.418954,3.112227 5.206996,12.389059 25.855425,48.538774 25.855425,48.538774 1.795516,-0.179552 3.41148,0.1197 4.728191,1.077309 0.598505,0.478804 1.07731,1.017459 1.496263,1.675815 L 42.461071,57.463605 c 0,0 -2.154619,-2.992526 1.615964,-3.591031 2.693275,-0.418954 8.319223,0.658356 11.251898,2.453871 3.172078,1.975069 10.533693,8.199522 11.970105,16.458894 0.658355,3.830433 0,5.8055 0.239402,9.21698 0.119701,1.556113 1.735664,3.052376 3.231928,0.957608 0.778057,-1.077309 1.19701,-5.027443 1.19701,-8.13967 0,-4.069837 -2.932675,-10.713244 -6.404005,-14.96263 z"
+       fill="url(#SVGID_2_)"
+       id="path10"
+       style="fill:#8fe132;fill-opacity:1;stroke-width:0.999999" />
+    <radialGradient
+       id="SVGID_3_"
+       cx="21.504999"
+       cy="103.861"
+       r="14.934"
+       gradientTransform="matrix(0.2966,0.4025,-0.805,0.5933,123.22,30.33236)"
+       gradientUnits="userSpaceOnUse">
+      <stop
+         offset="0"
+         stop-color="#f4ff72"
+         id="stop11" />
+      <stop
+         offset="1"
+         stop-color="#73c928"
+         stop-opacity="0"
+         id="stop12" />
+    </radialGradient>
+  </g>
+</svg>
diff --git a/src/assets/javascripts/localise.js b/src/assets/javascripts/localise.js
index 34ccd66b..c0936873 100644
--- a/src/assets/javascripts/localise.js
+++ b/src/assets/javascripts/localise.js
@@ -1,6 +1,9 @@
 window.browser = window.browser || window.chrome
 
 function localisePage() {
+	/**
+	 * @param {string} tag
+	 */
 	function getMessage(tag) {
 		return tag.replace(/__MSG_(\w+)__/g, (_match, v1) => {
 			return v1 ? browser.i18n.getMessage(v1) : null
diff --git a/src/assets/javascripts/services.js b/src/assets/javascripts/services.js
index 446926c0..0aea56cc 100644
--- a/src/assets/javascripts/services.js
+++ b/src/assets/javascripts/services.js
@@ -30,6 +30,12 @@ function all(service, frontend, options, config) {
 	return instances
 }
 
+/**
+ * @param {string} service
+ * @param {URL} url
+ * @param {{}} config
+ * @param {string} frontend
+ */
 function regexArray(service, url, config, frontend) {
 	let targetList = config.services[service].targets
 	if (frontend && 'excludeTargets' in config.services[service].frontends[frontend]) {
@@ -44,15 +50,29 @@ function regexArray(service, url, config, frontend) {
 	return false
 }
 
+/**
+ * @param {URL} url
+ * @param {string} type
+ * @param {URL} initiator
+ * @param {boolean} forceRedirection
+ */
 async function redirectAsync(url, type, initiator, forceRedirection) {
 	await init()
 	return redirect(url, type, initiator, forceRedirection)
 }
 
-function redirect(url, type, initiator, forceRedirection) {
+/**
+ * @param {URL} url
+ * @param {string} type
+ * @param {URL} initiator
+ * @param {boolean} forceRedirection
+ * @returns {string | undefined}
+ */
+function redirect(url, type, initiator, forceRedirection, incognito) {
 	if (type != "main_frame" && type != "sub_frame" && type != "image") return
 	let randomInstance
 	let frontend
+	if (!forceRedirection && options.redirectOnlyInIncognito == true && !incognito) return
 	for (const service in config.services) {
 		if (!forceRedirection && !options[service].enabled) continue
 
@@ -147,6 +167,10 @@ function redirect(url, type, initiator, forceRedirection) {
 		case "freetube": {
 			return 'freetube://' + url.href
 		}
+		case "freetubePwa": {
+			return 'freetube://' + url.href
+		}
+
 		case "poketube": {
 			if (url.pathname.startsWith('/channel')) {
 				const reg = /\/channel\/(.*)\/?$/.exec(url.pathname)
@@ -375,7 +399,7 @@ function redirect(url, type, initiator, forceRedirection) {
 		}
 		case "anonymousOverflow": {
 			if (url.hostname == "stackoverflow.com") {
-				const threadID = /\/(\d+)\/?$/.exec(url.pathname)
+				const threadID = /^\/a\/(\d+)\/?/.exec(url.pathname)
 				if (threadID) return `${randomInstance}/questions/${threadID[1]}${url.search}`
 				return `${randomInstance}${url.pathname}${url.search}`
 			}
@@ -524,26 +548,39 @@ function redirect(url, type, initiator, forceRedirection) {
 			return `${randomInstance}`
 		}
 		case "tuboSoundcloud": {
-			if (url.pathname.match(/\/user[^\/]+(\/$|$)/)) {
+			if (url.pathname == '/') return `${randomInstance}?kiosk?serviceId=1`
+			if (url.pathname.match(/^\/[^\/]+(\/$|$)/)) {
 				return `${randomInstance}/channel?url=${encodeURIComponent(url.href)}`
 			}
-			if (url.pathname.match(/\/user[^\/]+\/[^\/]+/)) {
+			if (url.pathname.match(/^\/[^\/]+\/[^\/]+/)) {
 				return `${randomInstance}/stream?url=${encodeURIComponent(url.href)}`
 			}
 			return `${randomInstance}`
 		}
+		case "twineo":
 		case "safetwitch": {
 			if (url.hostname.startsWith("clips.")) {
 				return `${randomInstance}/clip${url.pathname}${url.search}`
 			}
 			return `${randomInstance}${url.pathname}${url.search}`
 		}
+		case "tekstoLibre": {
+			return `${randomInstance}/?${url.pathname.slice(1)}`;
+		}
+		case "skyview": {
+			if (url.pathname == '/') return randomInstance
+			return `${randomInstance}?url=${encodeURIComponent(url.href)}`
+		}
 		default: {
 			return `${randomInstance}${url.pathname}${url.search}`
 		}
 	}
 }
 
+/**
+ * @param {URL} url
+ * @param {*} returnFrontend
+ */
 function computeService(url, returnFrontend) {
 	return new Promise(async resolve => {
 		const config = await utils.getConfig()
@@ -568,6 +605,10 @@ function computeService(url, returnFrontend) {
 	})
 }
 
+/**
+ * @param {URL} url
+ * @param {string} customService
+ */
 function switchInstance(url, customService) {
 	return new Promise(async resolve => {
 		let options = await utils.getOptions()
@@ -577,7 +618,7 @@ function switchInstance(url, customService) {
 		if (customService) {
 			const instancesList = options[options[customService].frontend]
 			if (instancesList !== undefined) {
-				resolve(`${utils.getRandomInstance(instancesList)}${url.pathname}${url.search}`)
+				resolve(`${utils.getNextInstance(url.origin, instancesList)}${url.pathname}${url.search}`)
 			}
 		} else {
 			for (const service in config.services) {
@@ -590,7 +631,7 @@ function switchInstance(url, customService) {
 					resolve()
 					return
 				}
-				resolve(`${utils.getRandomInstance(instancesList)}${url.pathname}${url.search}`)
+				resolve(`${utils.getNextInstance(url.origin, instancesList)}${url.pathname}${url.search}`)
 				return
 			}
 		}
@@ -598,6 +639,9 @@ function switchInstance(url, customService) {
 	})
 }
 
+/**
+ * @param {URL} url
+ */
 async function reverse(url) {
 	let options = await utils.getOptions()
 	let config = await utils.getConfig()
@@ -640,6 +684,9 @@ async function reverse(url) {
 				}
 				return
 			}
+			case "tekstowo": {
+				return `${config.services[service].url}/${url.search.slice(1)}`
+			}
 			default:
 				return
 		}
@@ -689,9 +736,12 @@ const defaultInstances = {
 	'indestructables': ['https://indestructables.private.coffee'],
 	'destructables': ['https://ds.vern.cc'],
 	'safetwitch': ['https://safetwitch.drgns.space'],
+	'twineo': ['https://twineo.exozy.me'],
 	'proxigram': ['https://proxigram.privacyfrontends.repl.co'],
 	'tuboYoutube': ['https://tubo.migalmoreno.com'],
 	'tuboSoundcloud': ['https://tubo.migalmoreno.com'],
+	'tekstoLibre': ['https://davilarek.github.io/TekstoLibre'],
+	'skyview': ['https://skyview.social'],
 }
 
 function initDefaults() {
@@ -714,9 +764,10 @@ function initDefaults() {
 				url: [],
 				regex: [],
 			}
-			options['theme'] = "detect"
-			options['popupServices'] = ["youtube", "twitter", "tiktok", "imgur", "reddit", "quora", "translate", "maps"]
-			options['fetchInstances'] = 'github'
+			options.theme = "detect"
+			options.popupServices = ["youtube", "twitter", "tiktok", "imgur", "reddit", "quora", "translate", "maps"]
+			options.fetchInstances = 'github'
+			options.redirectOnlyInIncognito = false
 
 			options = { ...options, ...defaultInstances }
 
@@ -765,6 +816,13 @@ function processUpdate() {
 					delete options[frontend]
 				}
 			}
+
+			for (const frontend of options.popupServices) {
+				if (!Object.keys(config.services).includes(frontend)) {
+					const i = options.popupServices.indexOf(frontend);
+					if (i > -1) options.popupServices.splice(i, 1);
+				}
+			}
 		}
 		browser.storage.local.set({ options }, () => {
 			resolve()
@@ -772,6 +830,10 @@ function processUpdate() {
 	})
 }
 
+/**
+ * @param {URL} url
+ * @param {boolean} test
+ */
 async function copyRaw(url, test) {
 	const newUrl = await reverse(url)
 	if (newUrl) {
@@ -792,6 +854,9 @@ async function copyRaw(url, test) {
 	}
 }
 
+/**
+ * @param {URL} url
+ */
 function isException(url) {
 	if (!options.exceptions) return false
 	let exceptions = options.exceptions
diff --git a/src/assets/javascripts/utils.js b/src/assets/javascripts/utils.js
index e85b1115..fe08e576 100644
--- a/src/assets/javascripts/utils.js
+++ b/src/assets/javascripts/utils.js
@@ -1,18 +1,68 @@
 window.browser = window.browser || window.chrome
 
+/**
+ * @param {Array.<T>} instances 
+ * @returns {T}
+ */
 function getRandomInstance(instances) {
 	return instances[~~(instances.length * Math.random())]
 }
 
+/**
+ * @param {string} currentInstanceUrl
+ * @param {Array.<T>} instances
+ * @returns {T}
+ */
+function getNextInstance(currentInstanceUrl, instances) {
+	const currentInstanceIndex = instances.indexOf(currentInstanceUrl);
+
+	if (currentInstanceIndex === -1){
+		return getRandomInstance(instances);
+	}
+
+	const nextInstanceIndex = (currentInstanceIndex + 1) % instances.length;
+
+	return instances[nextInstanceIndex];
+}
+
+/**
+ * @param {string} str
+ */
 function camelCase(str) {
 	return str.charAt(0).toUpperCase() + str.slice(1)
 }
 
+/**
+ * @param {URL} url
+ */
 function protocolHost(url) {
 	if (url.username && url.password) return `${url.protocol}//${url.username}:${url.password}@${url.host}`
+	if (url.pathname == "/TekstoLibre/" && url.host.endsWith("github.io")) // workaround
+		return `${url.protocol}//${url.host}${url.pathname.slice(0, -1)}`
 	return `${url.protocol}//${url.host}`
 }
 
+/**
+ * @typedef FrontendInfo
+ * @prop {boolean} instanceList
+ * @prop {string} name
+ * @prop {string} url
+ */
+
+/**
+ * @typedef {Object} Service
+ * @prop {Object.<string, FrontendInfo>} frontends
+ * @prop {Object} options
+ */
+
+/**
+ * @typedef {Object} Config
+ * @prop {Object.<string, Service>} services
+ */
+
+/**
+ * @returns {Promise<Config>}
+ */
 function getConfig() {
 	return new Promise(resolve => {
 		fetch("/config.json")
@@ -24,6 +74,14 @@ function getConfig() {
 	})
 }
 
+/**
+ * @typedef {Object} Option
+ * @prop {string} frontend
+ */
+
+/**
+ * @returns {Promise<Object.<string, Option | string[]>>}
+ */
 function getOptions() {
 	return new Promise(resolve =>
 		browser.storage.local.get("options", r => {
@@ -106,6 +164,9 @@ function getList(options) {
 	})
 }
 
+/**
+ * @param {string} href
+ */
 function pingOnce(href) {
 	return new Promise(async resolve => {
 		let started
@@ -130,6 +191,9 @@ function pingOnce(href) {
 	})
 }
 
+/**
+ * @param {string} href
+ */
 function ping(href) {
 	return new Promise(async resolve => {
 		let average = 0
@@ -150,6 +214,7 @@ function ping(href) {
 
 export default {
 	getRandomInstance,
+	getNextInstance,
 	protocolHost,
 	getList,
 	getBlacklist,
diff --git a/src/config.json b/src/config.json
index 9ff73dae..5bd39ee4 100644
--- a/src/config.json
+++ b/src/config.json
@@ -97,6 +97,16 @@
 					"desktopApp": true,
 					"instanceList": false,
 					"url": "https://github.com/yattee/yattee"
+				},
+				"freetubePwa": {
+					"excludeTargets": [
+						2,
+						3
+					],
+					"name": "FreeTube PWA",
+					"embeddable": false,
+					"instanceList": false,
+					"url": "https://github.com/MarmadileManteater/FreeTubeCordova"
 				}
 			},
 			"targets": [
@@ -183,6 +193,13 @@
 					"instanceList": true,
 					"url": "https://codeberg.org/dragongoose/safetwitch",
 					"localhost": false
+				},
+				"twineo": {
+					"name": "Twineo",
+					"embeddable": true,
+					"instanceList": true,
+					"url": "https://codeberg.org/CloudyyUw/twineo",
+					"localhost": false
 				}
 			},
 			"targets": [
@@ -693,7 +710,8 @@
 			},
 			"targets": [
 				"^https?:\\/{2}(www\\.)?stackoverflow\\.com\\/",
-				"^https?:\\/{2}([a-zA-Z0-9-]+\\.)?stackexchange\\.com\\/"
+				"^https?:\\/{2}([a-zA-Z0-9-]+\\.)?stackexchange\\.com\\/",
+				"^https?:\\/{2}(www\\.)?superuser\\.com\\/"
 			],
 			"name": "Stack Overflow",
 			"options": {
@@ -936,6 +954,46 @@
 			},
 			"imageType": "svg",
 			"url": "https://www.wolframalpha.com"
+		},
+		"tekstowo": {
+			"frontends": {
+				"tekstoLibre": {
+					"name": "TekstoLibre",
+					"instanceList": true,
+					"url": "https://github.com/Davilarek/TekstoLibre"
+				}
+			},
+			"targets": [
+				"^https?:\\/{2}(www\\.)?tekstowo\\.pl\\/"
+			],
+			"name": "Tekstowo.pl",
+			"options": {
+				"enabled": false,
+				"unsupportedUrls": "bypass",
+				"frontend": "tekstoLibre"
+			},
+			"imageType": "svg",
+			"url": "https://www.tekstowo.pl"
+		},
+		"bluesky": {
+			"frontends": {
+				"skyview": {
+					"name": "Skyview",
+					"instanceList": true,
+					"url": "https://github.com/badlogic/skyview"
+				}
+			},
+			"targets": [
+				"^https?:\\/{2}bsky\\.app\\/"
+			],
+			"name": "Bluesky",
+			"options": {
+				"enabled": false,
+				"unsupportedUrls": "bypass",
+				"frontend": "skyview"
+			},
+			"imageType": "svg",
+			"url": "https://bsky.app/"
 		}
 	}
 }
diff --git a/src/pages/background/background.js b/src/pages/background/background.js
index 542ef03b..4b8f1ca2 100644
--- a/src/pages/background/background.js
+++ b/src/pages/background/background.js
@@ -41,7 +41,7 @@ browser.webRequest.onBeforeRequest.addListener(
 			return null
 		}
 		if (tabIdRedirects[details.tabId] == false) return null
-		let newUrl = servicesHelper.redirect(url, details.type, initiator, tabIdRedirects[details.tabId], details.tabId)
+		let newUrl = servicesHelper.redirect(url, details.type, initiator, tabIdRedirects[details.tabId], details.incognito)
 
 		if (details.frameAncestors && details.frameAncestors.length > 0 && servicesHelper.isException(new URL(details.frameAncestors[0].url))) newUrl = null
 
diff --git a/src/pages/options/index.js b/src/pages/options/index.js
index dd1a6ff8..fcc51298 100644
--- a/src/pages/options/index.js
+++ b/src/pages/options/index.js
@@ -17,6 +17,9 @@ for (const a of document.getElementById("links").getElementsByTagName("a")) {
 config = await utils.getConfig()
 options = await utils.getOptions()
 
+/**
+ * @param {string} service
+ */
 async function changeFrontendsSettings(service) {
 	options = await utils.getOptions()
 	const opacityDiv = document.getElementById(`${service}-opacity`)
@@ -95,6 +98,9 @@ async function changeFrontendsSettings(service) {
 	frontend_name_element.href = config.services[service].frontends[divs[service].frontend.value].url
 }
 
+/**
+ * @param {string} path
+ */
 async function loadPage(path) {
 	options = await utils.getOptions()
 	for (const section of document.getElementById("pages").getElementsByTagName("section")) section.style.display = "none"
@@ -251,6 +257,13 @@ async function processCustomInstances(frontend, document) {
 	})
 }
 
+/**
+ * @param {string} frontend
+ * @param {*} networks
+ * @param {*} document
+ * @param {*} redirects
+ * @param {*} blacklist
+ */
 async function createList(frontend, networks, document, redirects, blacklist) {
 	const pingCache = await utils.getPingCache()
 	const options = await utils.getOptions()
@@ -331,6 +344,9 @@ const r = window.location.href.match(/#(.*)/)
 if (r) loadPage(r[1])
 else loadPage("general")
 
+/**
+ * @param {string} frontend
+ */
 async function ping(frontend) {
 	const instanceElements = [
 		...document.getElementById(frontend).getElementsByClassName("custom-checklist")[0].getElementsByTagName('x'),
@@ -357,6 +373,9 @@ async function ping(frontend) {
 	}
 }
 
+/**
+ * @param {number} time
+ */
 function processTime(time) {
 	let text
 	let color
@@ -377,4 +396,4 @@ function processTime(time) {
 	return {
 		color, text
 	}
-}
\ No newline at end of file
+}
diff --git a/src/pages/options/widgets/general.js b/src/pages/options/widgets/general.js
index ed61440b..6f2852a9 100644
--- a/src/pages/options/widgets/general.js
+++ b/src/pages/options/widgets/general.js
@@ -64,7 +64,6 @@ importSettingsElement.addEventListener("change", () => {
 	}
 })
 
-
 const exportSettingsSync = document.getElementById("export-settings-sync")
 const importSettingsSync = document.getElementById("import-settings-sync")
 const importSettingsSyncText = document.getElementById("import_settings_sync_text")
@@ -99,12 +98,29 @@ resetSettings.addEventListener("click", async () => {
 	location.reload()
 })
 
-let fetchInstancesElement = document.getElementById('fetch-instances')
+const fetchInstancesElement = document.getElementById('fetch-instances')
 fetchInstancesElement.addEventListener('change', event => {
 	setOption('fetchInstances', 'select', event)
 	location.reload()
 })
 
+const redirectOnlyInIncognitoElement = document.getElementById('redirectOnlyInIncognito')
+redirectOnlyInIncognitoElement.addEventListener('change', event => {
+	setOption('redirectOnlyInIncognito', 'checkbox', event)
+})
+
+const bookmarksMenuElement = document.getElementById('bookmarksMenu')
+bookmarksMenuElement.addEventListener('change', async event => {
+	if (event.target.checked)
+		bookmarksMenuElement.checked = await browser.permissions.request({
+			permissions: ["bookmarks"]
+		})
+	else
+		bookmarksMenuElement.checked = !await browser.permissions.remove({
+			permissions: ["bookmarks"]
+		})
+})
+
 let themeElement = document.getElementById("theme")
 themeElement.addEventListener("change", event => {
 	setOption("theme", "select", event)
@@ -132,6 +148,8 @@ for (const service in config.services) {
 let options = await utils.getOptions()
 themeElement.value = options.theme
 fetchInstancesElement.value = options.fetchInstances
+redirectOnlyInIncognitoElement.checked = options.redirectOnlyInIncognito
+bookmarksMenuElement.checked = await browser.permissions.contains({ permissions: ["bookmarks"] })
 for (const service in config.services) document.getElementById(service).checked = options.popupServices.includes(service)
 
 instanceTypeElement.addEventListener("change", event => {
diff --git a/src/pages/options/widgets/general.pug b/src/pages/options/widgets/general.pug
index 2e7c07da..70316473 100644
--- a/src/pages/options/widgets/general.pug
+++ b/src/pages/options/widgets/general.pug
@@ -18,6 +18,14 @@ section(class="block-option" id="general_page")
             option(value="disable" data-localise="__MSG_disable__") Disable
 
     div(class="block block-option")
+        label(for='redirectOnlyInIncognito' data-localise="__MSG_redirectOnlyInIncognito__") Redirect Only in Incognito
+        input(id='redirectOnlyInIncognito' type="checkbox")
+
+    div(class="block block-option")
+        label(for='bookmarksMenu' data-localise="__MSG_bookmarksMenu__") Bookmarks menu
+        input(id='bookmarksMenu' type="checkbox")
+
+    div(class="block block-option")
         label(data-localise="__MSG_excludeFromRedirecting__") Excluded from redirecting
 
     form(id="custom-exceptions-instance-form")
@@ -44,15 +52,15 @@ section(class="block-option" id="general_page")
             |&nbsp;
             x(data-localise="__MSG_importSettings__") Import Settings
         input(id="import-settings" type="file" style="display: none")
-        
+
         |&nbsp;&nbsp;
-        
+
         a(class="button button-inline" id="export-settings")
             svg(xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="currentColor")
                 path(d="M10.09 15.59L11.5 17l5-5-5-5-1.41 1.41L12.67 11H3v2h9.67l-2.58 2.59zM19 3H5c-1.11 0-2 .9-2 2v4h2V5h14v14H5v-4H3v4c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z")
             |&nbsp;
             x(data-localise="__MSG_exportSettings__") Export Settings
-        
+
         |&nbsp;&nbsp;
 
         button(class="button button-inline" id="export-settings-sync")
@@ -60,7 +68,7 @@ section(class="block-option" id="general_page")
                 path(d="M10.09 15.59L11.5 17l5-5-5-5-1.41 1.41L12.67 11H3v2h9.67l-2.58 2.59zM19 3H5c-1.11 0-2 .9-2 2v4h2V5h14v14H5v-4H3v4c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z")
             |&nbsp;
             x() Export Settings to Sync
-        
+
         |&nbsp;&nbsp;
 
         button(class="button button-inline" id="import-settings-sync")
@@ -70,7 +78,7 @@ section(class="block-option" id="general_page")
             x(id="import_settings_sync_text") Import Settings from Sync
 
         |&nbsp;&nbsp;
-        
+
         button(class="button button-inline" id="reset-settings")
             svg(xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="currentColor")
                 path(d="M12,5V2L8,6l4,4V7c3.31,0,6,2.69,6,6c0,2.97-2.17,5.43-5,5.91v2.02c3.95-0.49,7-3.85,7-7.93C20,8.58,16.42,5,12,5z")
diff --git a/src/pages/stylesheets/styles.css b/src/pages/stylesheets/styles.css
index 225023ec..2519a05f 100644
--- a/src/pages/stylesheets/styles.css
+++ b/src/pages/stylesheets/styles.css
@@ -131,7 +131,7 @@ section.links {
 	flex-wrap: wrap;
 	flex-direction: column;
 	width: 350px;
-	max-height: 890px;
+	max-height: 930px;
 }
 
 section.links div {