aboutsummaryrefslogtreecommitdiffstats
path: root/src/pages/options_src
diff options
context:
space:
mode:
Diffstat (limited to 'src/pages/options_src')
-rw-r--r--src/pages/options_src/App.svelte101
-rw-r--r--src/pages/options_src/General/Exceptions.svelte110
-rw-r--r--src/pages/options_src/General/General.svelte98
-rw-r--r--src/pages/options_src/General/SettingsButtons.svelte112
-rw-r--r--src/pages/options_src/Services/FrontendIcon.svelte41
-rw-r--r--src/pages/options_src/Services/Instances.svelte261
-rw-r--r--src/pages/options_src/Services/RedirectType.svelte102
-rw-r--r--src/pages/options_src/Services/ServiceIcon.svelte40
-rw-r--r--src/pages/options_src/Services/Services.svelte260
-rw-r--r--src/pages/options_src/Sidebar.svelte69
-rw-r--r--src/pages/options_src/main.js7
-rw-r--r--src/pages/options_src/stores.js4
-rw-r--r--src/pages/options_src/url.js38
13 files changed, 1243 insertions, 0 deletions
diff --git a/src/pages/options_src/App.svelte b/src/pages/options_src/App.svelte
new file mode 100644
index 00000000..1c4830bf
--- /dev/null
+++ b/src/pages/options_src/App.svelte
@@ -0,0 +1,101 @@
+<script>
+ const browser = window.browser || window.chrome
+
+ import General from "./General/General.svelte"
+ import url from "./url"
+ import utils from "../../assets/javascripts/utils.js"
+ import { onDestroy } from "svelte"
+ import servicesHelper from "../../assets/javascripts/services.js"
+ import { onMount } from "svelte"
+ import Sidebar from "./Sidebar.svelte"
+ import { options, config } from "./stores"
+ import Services from "./Services/Services.svelte"
+
+ let _options
+ const unsubscribeOptions = options.subscribe(val => {
+ if (val) {
+ _options = val
+ browser.storage.local.set({ options: val })
+ }
+ })
+
+ let _config
+ const unsubscribeConfig = config.subscribe(val => (_config = val))
+
+ onDestroy(() => {
+ unsubscribeOptions()
+ unsubscribeConfig()
+ })
+
+ onMount(async () => {
+ let opts = await utils.getOptions()
+ if (!opts) {
+ await servicesHelper.initDefaults()
+ opts = await utils.getOptions()
+ }
+ options.set(opts)
+ config.set(await utils.getConfig())
+ })
+
+ let style
+ $: if (_options) style = utils.style(_options, window)
+
+ const dir = ["ar", "iw", "ku", "fa", "ur"].includes(browser.i18n.getUILanguage()) ? "rtl" : "ltr"
+ document.body.dir = dir
+</script>
+
+{#if _options && _config}
+ <div class={dir} {dir} {style}>
+ <Sidebar />
+ {#if !$url.hash || $url.hash == "#general"}
+ <General />
+ {:else if $url.hash.startsWith("#services")}
+ <Services />
+ {/if}
+ </div>
+{:else}
+ <p>Loading...</p>
+{/if}
+
+<style>
+ :global(body) {
+ width: 100vw;
+ height: 100vh;
+ margin: 0;
+ padding: 0;
+ }
+
+ div {
+ height: 100%;
+ display: grid;
+ grid-template-columns: min-content 800px;
+ margin: 0;
+ padding-top: 50px;
+ justify-content: center;
+ font-family: "Inter", sans-serif;
+ box-sizing: border-box;
+ font-size: 16px;
+ background-color: var(--bg-main);
+ color: var(--text);
+ overflow: scroll;
+ }
+
+ @media (max-width: 1250px) {
+ div {
+ grid-template-columns: auto;
+ grid-template-rows: min-content auto;
+ padding-left: 5vw;
+ padding-right: 5vw;
+ }
+ }
+
+ @media (max-width: 715px) {
+ div {
+ font-size: 14px;
+ grid-template-columns: auto;
+ grid-template-rows: min-content auto;
+ padding-left: 5vw;
+ padding-right: 5vw;
+ }
+ }
+</style>
diff --git a/src/pages/options_src/General/Exceptions.svelte b/src/pages/options_src/General/Exceptions.svelte
new file mode 100644
index 00000000..7315877d
--- /dev/null
+++ b/src/pages/options_src/General/Exceptions.svelte
@@ -0,0 +1,110 @@
+<script>
+ const browser = window.browser || window.chrome
+
+ import Row from "../../components/Row.svelte"
+ import Select from "../../components/Select.svelte"
+ import AddIcon from "../../icons/AddIcon.svelte"
+ import CloseIcon from "../../icons/CloseIcon.svelte"
+ import Input from "../../components/Input.svelte"
+ import Label from "../../components/Label.svelte"
+ import { options, config } from "../stores"
+ import { onDestroy } from "svelte"
+
+ let _options
+ let _config
+
+ const unsubscribeOptions = options.subscribe(val => (_options = val))
+ const unsubscribeConfig = config.subscribe(val => (_config = val))
+ onDestroy(() => {
+ unsubscribeOptions()
+ unsubscribeConfig()
+ })
+ let inputType = "url"
+ let inputValue = ""
+
+ $: inputPlaceholder = inputType == "url" ? "https://www.google.com" : "https?://(www.|)youtube.com/"
+
+ function removeException(exception) {
+ let index
+ index = _options.exceptions.url.indexOf(exception)
+ if (index > -1) {
+ _options.exceptions.url.splice(index, 1)
+ } else {
+ index = _options.exceptions.regex.indexOf(exception)
+ if (index > -1) _options.exceptions.regex.splice(index, 1)
+ }
+ options.set(_options)
+ }
+
+ function addException() {
+ let valid = false
+ if (inputType == "url" && /^(ftp|http|https):\/\/[^ "]+$/.test(inputValue)) {
+ valid = true
+ if (!_options.exceptions.url.includes(inputValue)) {
+ _options.exceptions.url.push(inputValue)
+ }
+ } else if (inputType == "regex") {
+ valid = true
+ if (!_options.exceptions.regex.includes(inputValue)) {
+ _options.exceptions.regex.push(inputValue)
+ }
+ }
+ if (valid) {
+ options.set(_options)
+ inputValue = ""
+ }
+ }
+</script>
+
+<Row>
+ <Label>{browser.i18n.getMessage("excludeFromRedirecting") || "Excluded from redirecting"}</Label>
+</Row>
+<div dir="ltr">
+ <Row>
+ <div>
+ <Input
+ placeholder={inputPlaceholder}
+ aria-label="Add url exception input"
+ bind:value={inputValue}
+ on:keydown={e => {
+ if (e.key === "Enter") addException()
+ }}
+ />
+ <Select
+ bind:value={inputType}
+ values={[
+ { value: "url", name: "URL" },
+ { value: "regex", name: "Regex" },
+ ]}
+ />
+ </div>
+ <button class="add" on:click={addException} aria-label="Add the url exception">
+ <AddIcon />
+ </button>
+ </Row>
+ <hr />
+ <div class="checklist">
+ {#each [..._options.exceptions.url, ..._options.exceptions.regex] as exception}
+ <Row>
+ {exception}
+ <button class="add" on:click={() => removeException(exception)}>
+ <CloseIcon />
+ </button>
+ </Row>
+ <hr />
+ {/each}
+ </div>
+</div>
+
+<style>
+ .add {
+ background-color: transparent;
+ border: none;
+ color: var(--text);
+ padding: 0;
+ margin: 0;
+ text-decoration: none;
+ display: inline-block;
+ cursor: pointer;
+ }
+</style>
diff --git a/src/pages/options_src/General/General.svelte b/src/pages/options_src/General/General.svelte
new file mode 100644
index 00000000..b6ed1b46
--- /dev/null
+++ b/src/pages/options_src/General/General.svelte
@@ -0,0 +1,98 @@
+<script>
+ const browser = window.browser || window.chrome
+
+ import Exceptions from "./Exceptions.svelte"
+ import SettingsButtons from "./SettingsButtons.svelte"
+ import { options } from "../stores"
+ import { onDestroy } from "svelte"
+ import Row from "../../components/Row.svelte"
+ import Label from "../../components/Label.svelte"
+ import Select from "../../components/Select.svelte"
+ import Checkbox from "../../components/Checkbox.svelte"
+
+ let _options
+ const unsubscribe = options.subscribe(val => (_options = val))
+ onDestroy(unsubscribe)
+
+ let disableBookmarks = null
+ browser.runtime.getPlatformInfo(r => {
+ switch (r.os) {
+ case "fuchsia":
+ case "ios":
+ case "android":
+ disableBookmarks = true
+ break
+ default:
+ disableBookmarks = false
+ }
+ if (!disableBookmarks) {
+ browser.permissions.contains({ permissions: ["bookmarks"] }, r => (bookmarksPermission = r))
+ }
+ })
+
+ let bookmarksPermission
+ $: if (disableBookmarks !== null && disableBookmarks === false) {
+ if (bookmarksPermission) {
+ browser.permissions.request({ permissions: ["bookmarks"] }, r => (bookmarksPermission = r))
+ } else {
+ browser.permissions.remove({ permissions: ["bookmarks"] })
+ bookmarksPermission = false
+ }
+ }
+</script>
+
+<div>
+ <Row>
+ <Label>{browser.i18n.getMessage("theme") || "Theme"}</Label>
+ <Select
+ values={[
+ { value: "detect", name: browser.i18n.getMessage("auto") || "Auto" },
+ { value: "light", name: browser.i18n.getMessage("light") || "Light" },
+ { value: "dark", name: browser.i18n.getMessage("dark") || "Dark" },
+ ]}
+ value={_options.theme}
+ onChange={e => {
+ _options.theme = e.target.options[e.target.options.selectedIndex].value
+ options.set(_options)
+ }}
+ />
+ </Row>
+
+ <Row>
+ <Label>{browser.i18n.getMessage("fetchPublicInstances") || "Fetch public instances"}</Label>
+ <Select
+ value={_options.fetchInstances}
+ values={[
+ { value: "github", name: "GitHub" },
+ { value: "codeberg", name: "Codeberg" },
+ { value: "disable", name: browser.i18n.getMessage("disable") || "Disable" },
+ ]}
+ onChange={e => {
+ _options.fetchInstances = e.target.options[e.target.options.selectedIndex].value
+ options.set(_options)
+ }}
+ />
+ </Row>
+
+ <Row>
+ <Label>{browser.i18n.getMessage("redirectOnlyInIncognito") || "Redirect Only in Incognito"}</Label>
+ <Checkbox
+ checked={_options.redirectOnlyInIncognito}
+ onChange={e => {
+ _options.redirectOnlyInIncognito = e.target.checked
+ options.set(_options)
+ }}
+ />
+ </Row>
+
+ {#if disableBookmarks === false}
+ <Row>
+ <Label>{browser.i18n.getMessage("bookmarksMenu") || "Bookmarks menu"}</Label>
+ <Checkbox bind:checked={bookmarksPermission} />
+ </Row>
+ {/if}
+
+ <Exceptions />
+
+ <SettingsButtons />
+</div>
diff --git a/src/pages/options_src/General/SettingsButtons.svelte b/src/pages/options_src/General/SettingsButtons.svelte
new file mode 100644
index 00000000..4be747fe
--- /dev/null
+++ b/src/pages/options_src/General/SettingsButtons.svelte
@@ -0,0 +1,112 @@
+<script>
+ const browser = window.browser || window.chrome
+
+ import { onDestroy } from "svelte"
+ import Button from "../../components/Button.svelte"
+ import ExportIcon from "../../icons/ExportIcon.svelte"
+ import ImportIcon from "../../icons/ImportIcon.svelte"
+ import ResetIcon from "../../icons/ResetIcon.svelte"
+ import { options } from "../stores"
+ import servicesHelper from "../../../assets/javascripts/services.js"
+ import utils from "../../../assets/javascripts/utils.js"
+
+ let _options
+ const unsubscribe = options.subscribe(val => (_options = val))
+ onDestroy(unsubscribe)
+
+ let importSettingsInput
+ let importSettingsFiles
+ $: if (importSettingsFiles) {
+ const reader = new FileReader()
+ reader.readAsText(importSettingsFiles[0])
+ reader.onload = async () => {
+ let data = JSON.parse(reader.result)
+ if (data.version != browser.runtime.getManifest().version) {
+ alert("Importing from a previous version. Be careful")
+ }
+ data = await servicesHelper.processUpdate(data)
+ options.set(data)
+ }
+ reader.onerror = error => {
+ console.log("error", error)
+ alert("Error!")
+ }
+ }
+
+ async function exportSettings() {
+ _options.version = browser.runtime.getManifest().version
+ const resultString = JSON.stringify(_options, null, " ")
+ const anchor = document.createElement("a")
+ anchor.href = "data:application/json;base64," + btoa(resultString)
+ anchor.download = `libredirect-settings-v${_options.version}.json`
+ anchor.click()
+ }
+
+ async function exportSettingsSync() {
+ _options.version = browser.runtime.getManifest().version
+ browser.storage.sync.set({ options: _options })
+ }
+
+ async function importSettingsSync() {
+ browser.storage.sync.get({ options }, async r => {
+ let data = r.options
+ if (data.version != browser.runtime.getManifest().version) {
+ alert("Importing from a previous version. Be careful")
+ }
+ data = await servicesHelper.processUpdate(data)
+ options.set(data)
+ })
+ }
+
+ async function resetSettings() {
+ browser.storage.local.clear(async () => {
+ const data = await servicesHelper.initDefaults()
+ options.set(data)
+ })
+ }
+</script>
+
+<div class="buttons">
+ <Button on:click={() => importSettingsInput.click()}>
+ <ImportIcon class="margin margin_{document.body.dir}" />
+ {browser.i18n.getMessage("importSettings") || "Import Settings"}
+ </Button>
+ <input
+ type="file"
+ accept=".json"
+ style="display: none"
+ bind:this={importSettingsInput}
+ bind:files={importSettingsFiles}
+ />
+
+ <Button on:click={exportSettings}>
+ <ExportIcon class="margin margin_{document.body.dir}" />
+ {browser.i18n.getMessage("exportSettings") || "Export Settings"}
+ </Button>
+
+ <Button on:click={exportSettingsSync}>
+ <ExportIcon class="margin margin_{document.body.dir}" />
+ {browser.i18n.getMessage("exportSettingsToSync") || "Export Settings to Sync"}
+ </Button>
+
+ <Button on:click={importSettingsSync}>
+ <ImportIcon class="margin margin_{document.body.dir}" />
+ {browser.i18n.getMessage("importSettingsFromSync") || "Import Settings from Sync"}
+ </Button>
+
+ <Button on:click={resetSettings}>
+ <ResetIcon class="margin margin_{document.body.dir}" />
+ {browser.i18n.getMessage("resetSettings") || "Reset Settings"}
+ </Button>
+</div>
+
+<style>
+ :global(.margin) {
+ margin-right: 10px;
+ margin-left: 0;
+ }
+ :global(.margin_rtl) {
+ margin-right: 0;
+ margin-left: 10px;
+ }
+</style>
diff --git a/src/pages/options_src/Services/FrontendIcon.svelte b/src/pages/options_src/Services/FrontendIcon.svelte
new file mode 100644
index 00000000..4b392676
--- /dev/null
+++ b/src/pages/options_src/Services/FrontendIcon.svelte
@@ -0,0 +1,41 @@
+<script>
+ import { onDestroy } from "svelte"
+ export let details
+ export let selectedService
+ import { config, options } from "../stores"
+ let _options
+ let _config
+
+ const unsubscribeOptions = options.subscribe(val => (_options = val))
+ const unsubscribeConfig = config.subscribe(val => (_config = val))
+ onDestroy(() => {
+ unsubscribeOptions()
+ unsubscribeConfig()
+ })
+
+ let theme
+ $: if (_options) {
+ if (_options.theme == "dark") {
+ theme = "dark"
+ } else if (_options.theme == "light") {
+ theme = "light"
+ } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
+ theme = "dark"
+ } else {
+ theme = "light"
+ }
+ }
+ $: imageType = _config.services[selectedService].frontends[details.value].imageType
+</script>
+
+{#if imageType}
+ {#if imageType == "svgMono"}
+ {#if theme == "dark"}
+ <img src={`/assets/images/${details.value}-icon-light.svg`} alt={details.label} />
+ {:else}
+ <img src={`/assets/images/${details.value}-icon.svg`} alt={details.label} />
+ {/if}
+ {:else}
+ <img src={`/assets/images/${details.value}-icon.${imageType}`} alt={details.label} />
+ {/if}
+{/if}
diff --git a/src/pages/options_src/Services/Instances.svelte b/src/pages/options_src/Services/Instances.svelte
new file mode 100644
index 00000000..4e5d1e7d
--- /dev/null
+++ b/src/pages/options_src/Services/Instances.svelte
@@ -0,0 +1,261 @@
+<script>
+ const browser = window.browser || window.chrome
+
+ import Button from "../../components/Button.svelte"
+ import AddIcon from "../../icons/AddIcon.svelte"
+ import { options, config } from "../stores"
+ import PingIcon from "../../icons/PingIcon.svelte"
+ import AutoPickIcon from "../../icons/AutoPickIcon.svelte"
+ import Row from "../../components/Row.svelte"
+ import Input from "../../components/Input.svelte"
+ import Label from "../../components/Label.svelte"
+ import CloseIcon from "../../icons/CloseIcon.svelte"
+ import { onDestroy, onMount } from "svelte"
+ import utils from "../../../assets/javascripts/utils"
+
+ export let selectedService
+ export let selectedFrontend
+
+ let _options
+ let _config
+
+ const unsubscribeOptions = options.subscribe(val => (_options = val))
+ const unsubscribeConfig = config.subscribe(val => (_config = val))
+ onDestroy(() => {
+ unsubscribeOptions()
+ unsubscribeConfig()
+ })
+
+ let blacklist
+ let redirects
+
+ $: serviceConf = _config.services[selectedService]
+
+ let allInstances = []
+
+ $: {
+ allInstances = []
+ if (_options[selectedFrontend]) allInstances.push(..._options[selectedFrontend])
+ if (redirects && redirects[selectedFrontend]) {
+ allInstances.push(...redirects[selectedFrontend]["clearnet"])
+ }
+ allInstances = [...new Set(allInstances)]
+ }
+
+ let pingCache
+ $: {
+ if (pingCache) browser.storage.local.set({ pingCache })
+ }
+
+ function isCustomInstance(instance) {
+ if (redirects[selectedFrontend]) {
+ for (const network in redirects[selectedFrontend]) {
+ if (redirects[selectedFrontend][network].includes(instance)) return false
+ }
+ }
+ return true
+ }
+
+ async function pingInstances() {
+ pingCache = {}
+ for (const instance of allInstances) {
+ pingCache[instance] = { color: "lightblue", value: "pinging..." }
+ const time = await utils.ping(instance)
+ pingCache[instance] = colorTime(time)
+ }
+ }
+
+ async function autoPickInstance() {
+ const instances = utils.randomInstances(redirects[selectedFrontend]["clearnet"], 5)
+ const myInstancesCache = []
+ for (const instance of instances) {
+ pingCache[instance] = { color: "lightblue", value: "pinging..." }
+ const time = await utils.ping(instance)
+ pingCache[instance] = colorTime(time)
+ myInstancesCache.push([instance, time])
+ }
+ myInstancesCache.sort((a, b) => a[1] - b[1])
+
+ _options[selectedFrontend].push(myInstancesCache[0][0])
+ options.set(_options)
+ }
+
+ function colorTime(time) {
+ let value
+ let color
+ if (time < 5000) {
+ value = `${time}ms`
+ if (time <= 1000) color = "green"
+ else if (time <= 2000) color = "orange"
+ } else if (time >= 5000) {
+ color = "red"
+ if (time == 5000) value = "5000ms+"
+ if (time > 5000) value = `Error: ${time - 5000}`
+ } else {
+ color = "red"
+ value = "Server not found"
+ }
+ return { color, value }
+ }
+
+ onMount(async () => {
+ blacklist = await utils.getBlacklist(_options)
+ redirects = await utils.getList(_options)
+ pingCache = await utils.getPingCache()
+ })
+
+ let addInstanceValue
+ function addInstance() {
+ const instance = utils.protocolHost(new URL(addInstanceValue))
+ if (!_options[selectedFrontend].includes(instance)) {
+ _options[selectedFrontend].push(instance)
+ addInstanceValue = ""
+ options.set(_options)
+ }
+ }
+</script>
+
+{#if serviceConf.frontends[selectedFrontend].instanceList && redirects && blacklist}
+ <hr />
+
+ <div>
+ <Button on:click={pingInstances}>
+ <PingIcon class="margin margin_{document.body.dir}" />
+ {browser.i18n.getMessage("pingInstances") || "Ping Instances"}
+ </Button>
+ <Button on:click={autoPickInstance}>
+ <AutoPickIcon class="margin margin_{document.body.dir}" />
+ {browser.i18n.getMessage("autoPickInstance") || "Auto Pick Instance"}
+ </Button>
+ </div>
+
+ <Row>
+ <Label>{browser.i18n.getMessage("addYourFavoriteInstances") || "Add your favorite instances"}</Label>
+ </Row>
+ <div dir="ltr">
+ <Row>
+ <Input
+ bind:value={addInstanceValue}
+ type="url"
+ placeholder="https://instance.com"
+ aria-label="Add instance input"
+ on:keydown={e => e.key === "Enter" && addInstance()}
+ />
+ <button on:click={addInstance} class="add" aria-label="Add the instance">
+ <AddIcon />
+ </button>
+ </Row>
+
+ {#each _options[selectedFrontend] as instance}
+ <Row>
+ <span>
+ <a href={instance} target="_blank" rel="noopener noreferrer">{instance}</a>
+ {#if isCustomInstance(instance)}
+ <span style="color:grey">custom</span>
+ {/if}
+ {#if pingCache && pingCache[instance]}
+ <span style="color:{pingCache[instance].color}">{pingCache[instance].value}</span>
+ {/if}
+ </span>
+ <button
+ class="add"
+ aria-label="Remove Instance"
+ on:click={() => {
+ const index = _options[selectedFrontend].indexOf(instance)
+ if (index > -1) {
+ _options[selectedFrontend].splice(index, 1)
+ options.set(_options)
+ }
+ }}
+ >
+ <CloseIcon />
+ </button>
+ </Row>
+ <hr />
+ {/each}
+
+ {#if redirects !== "disabled" && blacklist !== "disabled"}
+ {#if redirects[selectedFrontend] && redirects[selectedFrontend]["clearnet"]}
+ {#each Object.entries(_config.networks) as [networkName, network]}
+ {#if redirects[selectedFrontend] && redirects[selectedFrontend][networkName] && redirects[selectedFrontend][networkName].length > 0}
+ <Row></Row>
+ <Row><Label>{network.name}</Label></Row>
+ <hr />
+ {#each redirects[selectedFrontend][networkName] as instance}
+ <Row>
+ <span>
+ <a href={instance} target="_blank" rel="noopener noreferrer">{instance}</a>
+ {#if blacklist.cloudflare.includes(instance)}
+ <a
+ href="https://libredirect.github.io/docs.html#instances"
+ target="_blank"
+ rel="noopener noreferrer"
+ style="color:red;"
+ >
+ cloudflare
+ </a>
+ {/if}
+ {#if _options[selectedFrontend].includes(instance)}
+ <span style="color:grey">chosen</span>
+ {/if}
+ {#if pingCache && pingCache[instance]}
+ <span style="color:{pingCache[instance].color}">{pingCache[instance].value}</span>
+ {/if}
+ </span>
+ <button
+ class="add"
+ aria-label="Add instance"
+ on:click={() => {
+ if (_options[selectedFrontend]) {
+ if (!_options[selectedFrontend].includes(instance)) {
+ _options[selectedFrontend].push(instance)
+ options.set(_options)
+ }
+ }
+ }}
+ >
+ <AddIcon />
+ </button>
+ </Row>
+ <hr />
+ {/each}
+ {/if}
+ {/each}
+ {:else}
+ <Row><Label>No instances found.</Label></Row>
+ {/if}
+ {/if}
+ </div>
+{/if}
+
+<style>
+ .add {
+ background-color: transparent;
+ border: none;
+ color: var(--text);
+ padding: 0;
+ margin: 0;
+ text-decoration: none;
+ display: inline-block;
+ cursor: pointer;
+ }
+
+ a {
+ color: var(--text);
+ text-decoration: none;
+ word-wrap: anywhere;
+ }
+
+ a:hover {
+ text-decoration: underline;
+ }
+
+ :global(.margin) {
+ margin-right: 10px;
+ margin-left: 0;
+ }
+ :global(.margin_rtl) {
+ margin-right: 0;
+ margin-left: 10px;
+ }
+</style>
diff --git a/src/pages/options_src/Services/RedirectType.svelte b/src/pages/options_src/Services/RedirectType.svelte
new file mode 100644
index 00000000..69ea2b73
--- /dev/null
+++ b/src/pages/options_src/Services/RedirectType.svelte
@@ -0,0 +1,102 @@
+<script>
+ const browser = window.browser || window.chrome
+
+ import { onDestroy } from "svelte"
+ import SvelteSelect from "svelte-select"
+ import { options, config } from "../stores"
+ import Row from "../../components/Row.svelte"
+ import Label from "../../components/Label.svelte"
+ import FrontendIcon from "./FrontendIcon.svelte"
+ import Select from "../../components/Select.svelte"
+
+ let _options
+ let _config
+
+ const unsubscribeOptions = options.subscribe(val => (_options = val))
+ const unsubscribeConfig = config.subscribe(val => (_config = val))
+ onDestroy(() => {
+ unsubscribeOptions()
+ unsubscribeConfig()
+ })
+
+ export let selectedService
+
+ $: serviceConf = _config.services[selectedService]
+ $: serviceOptions = _options[selectedService]
+ $: frontendName = _options[selectedService].frontend
+
+ let values
+ $: if (serviceConf.frontends[frontendName].embeddable) {
+ values = [
+ { value: "both", name: browser.i18n.getMessage("both") || "Both" },
+ { value: "sub_frame", name: browser.i18n.getMessage("onlyEmbedded") || "Only Embedded" },
+ { value: "main_frame", name: browser.i18n.getMessage("onlyNotEmbedded") || "Only Not Embedded" },
+ ]
+ } else if (
+ serviceConf.frontends[frontendName].desktopApp &&
+ Object.values(serviceConf.frontends).some(frontend => frontend.embeddable)
+ ) {
+ values = [
+ { value: "both", name: browser.i18n.getMessage("both") || "Both" },
+ { value: "main_frame", name: browser.i18n.getMessage("onlyNotEmbedded") || "Only Not Embedded" },
+ ]
+ if (serviceOptions.redirectType == "sub_frame") {
+ serviceOptions.redirectType = "main_frame"
+ options.set(_options)
+ }
+ } else {
+ values = [{ value: "main_frame", name: browser.i18n.getMessage("onlyNotEmbedded") || "Only Not Embedded" }]
+ serviceOptions.redirectType = "main_frame"
+ options.set(_options)
+ }
+
+ let embeddableFrontends = []
+ $: if (serviceConf) {
+ embeddableFrontends = []
+ for (const [frontendId, frontendConf] of Object.entries(serviceConf.frontends)) {
+ if (frontendConf.embeddable && frontendConf.instanceList) {
+ embeddableFrontends.push({
+ value: frontendId,
+ label: frontendConf.name,
+ })
+ }
+ }
+ }
+</script>
+
+<Row>
+ <Label>{browser.i18n.getMessage("redirectType") || "Redirect Type"}</Label>
+ <Select
+ value={serviceOptions.redirectType}
+ onChange={e => {
+ serviceOptions.redirectType = e.target.options[e.target.options.selectedIndex].value
+ options.set(_options)
+ }}
+ {values}
+ />
+</Row>
+
+{#if serviceConf.frontends[frontendName].desktopApp && serviceOptions.redirectType != "main_frame"}
+ <Row>
+ <Label>{browser.i18n.getMessage("embedFrontend") || "Embed Frontend"}</Label>
+ <SvelteSelect
+ clearable={false}
+ class="svelte_select"
+ value={serviceOptions.embedFrontend}
+ on:change={e => {
+ serviceOptions.embedFrontend = e.detail.value
+ options.set(_options)
+ }}
+ items={embeddableFrontends}
+ >
+ <div class="slot" slot="item" let:item>
+ <FrontendIcon details={item} {selectedService} />
+ {item.label}
+ </div>
+ <div class="slot" slot="selection" let:selection>
+ <FrontendIcon details={selection} {selectedService} />
+ {selection.label}
+ </div>
+ </SvelteSelect>
+ </Row>
+{/if}
diff --git a/src/pages/options_src/Services/ServiceIcon.svelte b/src/pages/options_src/Services/ServiceIcon.svelte
new file mode 100644
index 00000000..89393cf6
--- /dev/null
+++ b/src/pages/options_src/Services/ServiceIcon.svelte
@@ -0,0 +1,40 @@
+<script>
+ import { onDestroy } from "svelte"
+ export let details
+ import { config, options } from "../stores"
+ let _options
+ let _config
+
+ const unsubscribeOptions = options.subscribe(val => (_options = val))
+ const unsubscribeConfig = config.subscribe(val => (_config = val))
+ onDestroy(() => {
+ unsubscribeOptions()
+ unsubscribeConfig()
+ })
+
+ let theme
+ $: if (_options) {
+ if (_options.theme == "dark") {
+ theme = "dark"
+ } else if (_options.theme == "light") {
+ theme = "light"
+ } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
+ theme = "dark"
+ } else {
+ theme = "light"
+ }
+ }
+ $: imageType = _config.services[details.value].imageType
+</script>
+
+{#if imageType}
+ {#if imageType == "svgMono"}
+ {#if theme == "dark"}
+ <img src={`/assets/images/${details.value}-icon-light.svg`} alt={details.label} />
+ {:else}
+ <img src={`/assets/images/${details.value}-icon.svg`} alt={details.label} />
+ {/if}
+ {:else}
+ <img src={`/assets/images/${details.value}-icon.${imageType}`} alt={details.label} />
+ {/if}
+{/if}
diff --git a/src/pages/options_src/Services/Services.svelte b/src/pages/options_src/Services/Services.svelte
new file mode 100644
index 00000000..db2977f9
--- /dev/null
+++ b/src/pages/options_src/Services/Services.svelte
@@ -0,0 +1,260 @@
+<script>
+ const browser = window.browser || window.chrome
+
+ import url from "../url"
+ import Row from "../../components/Row.svelte"
+ import Label from "../../components/Label.svelte"
+ import Select from "../../components/Select.svelte"
+ import { options, config } from "../stores"
+ import RedirectType from "./RedirectType.svelte"
+ import { onDestroy } from "svelte"
+ import Instances from "./Instances.svelte"
+ import SvelteSelect from "svelte-select"
+ import ServiceIcon from "./ServiceIcon.svelte"
+ import FrontendIcon from "./FrontendIcon.svelte"
+ import Checkbox from "../../components/Checkbox.svelte"
+
+ let _options
+ let _config
+
+ const unsubscribeOptions = options.subscribe(val => (_options = val))
+ const unsubscribeConfig = config.subscribe(val => (_config = val))
+ onDestroy(() => {
+ unsubscribeOptions()
+ unsubscribeConfig()
+ })
+ let selectedService = $url.hash.startsWith("#services:") ? $url.hash.split(":")[1] : "youtube"
+ let hideServiceSelection = false
+ let hideFrontendSelection = false
+ $: serviceConf = _config.services[selectedService]
+ $: serviceOptions = _options[selectedService]
+ $: frontendWebsite = serviceConf.frontends[serviceOptions.frontend].url
+ $: servicesEntries = Object.entries(_config.services)
+ $: frontendEntries = Object.entries(serviceConf.frontends)
+</script>
+
+<div>
+ <Row>
+ <Label>
+ <a href={serviceConf.url} style="text-decoration: underline;" target="_blank" rel="noopener noreferrer">
+ {browser.i18n.getMessage("service") || "Service"}
+ </a>
+ </Label>
+ <div dir="ltr" on:click={() => (hideServiceSelection = true)} on:keydown={null}>
+ <SvelteSelect
+ clearable={false}
+ class="svelte_select"
+ value={selectedService}
+ showChevron
+ on:change={e => {
+ selectedService = e.detail.value
+ window.location.hash = `services:${e.detail.value}`
+ hideServiceSelection = false
+ }}
+ on:pointerup={() => (hideServiceSelection = false)}
+ on:focus={() => (hideServiceSelection = true)}
+ on:blur={() => (hideServiceSelection = false)}
+ items={[
+ ...servicesEntries.map(([serviceKey, service]) => {
+ return { value: serviceKey, label: service.name }
+ }),
+ ]}
+ >
+ <div class={"slot " + (!_options[item.value].enabled && "disabled")} slot="item" let:item>
+ <ServiceIcon details={item} />
+ {item.label}
+ </div>
+ <div
+ class={"slot " + (!_options[selection.value].enabled && !hideServiceSelection && "disabled")}
+ slot="selection"
+ let:selection
+ >
+ {#if !hideServiceSelection}
+ <ServiceIcon details={selection} />
+ {selection.label}
+ {:else}
+ {browser.i18n.getMessage("searchService") || "Search Service"}
+ {/if}
+ </div>
+ <div style="font-size: 10px;" slot="chevron-icon">🮦</div>
+ </SvelteSelect>
+ </div>
+ </Row>
+
+ <hr />
+
+ <Row>
+ <Label>{browser.i18n.getMessage("enable") || "Enable"}</Label>
+ <Checkbox
+ checked={serviceOptions.enabled}
+ onChange={e => {
+ serviceOptions.enabled = e.target.checked
+ options.set(_options)
+ }}
+ />
+ </Row>
+
+ <div style={!serviceOptions.enabled && "pointer-events: none;opacity: 0.4;user-select: none;"}>
+ <Row>
+ <Label>{browser.i18n.getMessage("showInPopup") || "Show in popup"}</Label>
+ <Checkbox
+ checked={_options.popupServices.includes(selectedService)}
+ onChange={e => {
+ if (e.target.checked && !_options.popupServices.includes(selectedService)) {
+ _options.popupServices.push(selectedService)
+ } else if (_options.popupServices.includes(selectedService)) {
+ const index = _options.popupServices.indexOf(selectedService)
+ if (index !== -1) _options.popupServices.splice(index, 1)
+ }
+ options.set(_options)
+ }}
+ />
+ </Row>
+
+ <Row>
+ <Label>
+ <a href={frontendWebsite} style="text-decoration: underline;" target="_blank" rel="noopener noreferrer">
+ {browser.i18n.getMessage("frontend") || "Frontend"}
+ </a>
+ </Label>
+ <div dir="ltr" on:click={() => (hideFrontendSelection = true)} on:keydown={null}>
+ <SvelteSelect
+ clearable={false}
+ dir="ltr"
+ class="svelte_select"
+ value={serviceOptions.frontend}
+ showChevron
+ on:change={e => {
+ serviceOptions.frontend = e.detail.value
+ options.set(_options)
+ hideFrontendSelection = false
+ }}
+ on:pointerup={() => (hideServiceSelection = false)}
+ on:focus={() => (hideFrontendSelection = true)}
+ on:blur={() => (hideFrontendSelection = false)}
+ items={[
+ ...frontendEntries.map(([frontendId, frontend]) => ({
+ value: frontendId,
+ label: frontend.name,
+ })),
+ ]}
+ >
+ <div class="slot" slot="item" let:item>
+ <FrontendIcon details={item} {selectedService} />
+ {item.label}
+ </div>
+ <div class="slot" slot="selection" let:selection>
+ {#if !hideFrontendSelection}
+ <FrontendIcon details={selection} {selectedService} />
+ {selection.label}
+ {:else}
+ {browser.i18n.getMessage("search_frontend") || "Search Frontend"}
+ {/if}
+ </div>
+ <div style="font-size: 10px;" slot="chevron-icon">🮦</div>
+ </SvelteSelect>
+ </div>
+ </Row>
+
+ <RedirectType {selectedService} />
+
+ <Row>
+ <Label>{browser.i18n.getMessage("unsupportedIframesHandling") || "Unsupported embeds handling"}</Label>
+ <Select
+ value={serviceOptions.unsupportedUrls}
+ onChange={e => {
+ serviceOptions.unsupportedUrls = e.target.options[e.target.options.selectedIndex].value
+ options.set(_options)
+ }}
+ values={[
+ { value: "bypass", name: browser.i18n.getMessage("bypass") || "Bypass" },
+ { value: "block", name: browser.i18n.getMessage("block") || "Block" },
+ ]}
+ />
+ </Row>
+
+ <div style={_options.redirectOnlyInIncognito && "pointer-events: none;opacity: 0.4;user-select: none;"}>
+ <Row>
+ <Label>{browser.i18n.getMessage("redirectOnlyInIncognito") || "Redirect Only in Incognito"}</Label>
+ <Checkbox
+ checked={serviceOptions.redirectOnlyInIncognito}
+ onChange={e => {
+ serviceOptions.redirectOnlyInIncognito = e.target.checked
+ options.set(_options)
+ }}
+ />
+ </Row>
+ </div>
+
+ {#if selectedService == "search"}
+ <Row>
+ <Label>{browser.i18n.getMessage("redirectGoogle") || "Redirect Google"}</Label>
+ <Checkbox
+ checked={serviceOptions.redirectGoogle}
+ onChange={e => {
+ serviceOptions.redirectGoogle = e.target.checked
+ options.set(_options)
+ }}
+ />
+ </Row>
+ <Row>
+ <Label>
+ {@html browser.i18n.getMessage("searchHint") ||
+ `Set LibRedirect as Default Search Engine. For how to do in chromium browsers, click
+ <a
+ href="https://libredirect.github.io/docs.html#search_engine_chromium"
+ target="_blank"
+ rel="noopener noreferrer"
+ >here
+ </a>.`}
+ </Label>
+ </Row>
+ {/if}
+
+ <Instances
+ {selectedService}
+ selectedFrontend={!serviceConf.frontends[serviceOptions.frontend].desktopApp ||
+ serviceOptions.redirectType == "main_frame"
+ ? serviceOptions.frontend
+ : serviceOptions.embedFrontend}
+ />
+
+ <Row></Row>
+ </div>
+</div>
+
+<style>
+ :global(.svelte_select) {
+ font-weight: bold;
+ --item-padding: 0 10px;
+ --border: none;
+ --border-hover: none;
+ --border-focused: none;
+ --width: 210px;
+ --background: var(--bg-secondary);
+ --list-background: var(--bg-secondary);
+ --item-is-active-bg: grey;
+ --item-hover-bg: grey;
+ --item-is-active-color: var(--text);
+ --list-max-height: 400px;
+ --padding: 0 0 0 10px;
+ --item-color: var(--text);
+ }
+ :global(.svelte_select .slot) {
+ display: flex;
+ justify-content: start;
+ align-items: center;
+ }
+
+ :global(.svelte_select img, .svelte_select svg) {
+ margin-right: 10px;
+ margin-left: 0;
+ height: 26px;
+ width: 26px;
+ color: var(--text);
+ }
+
+ :global(.svelte_select .disabled) {
+ opacity: 0.4;
+ }
+</style>
diff --git a/src/pages/options_src/Sidebar.svelte b/src/pages/options_src/Sidebar.svelte
new file mode 100644
index 00000000..6b67581a
--- /dev/null
+++ b/src/pages/options_src/Sidebar.svelte
@@ -0,0 +1,69 @@
+<script>
+ const browser = window.browser || window.chrome
+
+ import url from "./url"
+ import GeneralIcon from "../icons/GeneralIcon.svelte"
+ import ServicesIcon from "../icons/ServicesIcon.svelte"
+ import AboutIcon from "../icons/AboutIcon.svelte"
+</script>
+
+<div>
+ <a href="#general" style={$url.hash == "#general" && "color: var(--active);"}>
+ <GeneralIcon class="margin margin_{document.body.dir}" />
+ {browser.i18n.getMessage("general") || "General"}
+ </a>
+ <a href="#services" style={$url.hash == "#services" && "color: var(--active);"}>
+ <ServicesIcon class="margin margin_{document.body.dir}" />
+ {browser.i18n.getMessage("services") || "Services"}
+ </a>
+ <a href="https://libredirect.github.io" target="_blank" rel="noopener noreferrer">
+ <AboutIcon class="margin margin_{document.body.dir}" />
+ {browser.i18n.getMessage("about") || "About"}
+ </a>
+</div>
+
+<style>
+ div {
+ display: flex;
+ flex-direction: column;
+ margin: 0 20px;
+ }
+
+ a {
+ display: flex;
+ align-items: center;
+ font-size: 18px;
+ text-decoration: none;
+ color: var(--text);
+ transition: 0.1s;
+ margin: 10px;
+ min-width: max-content;
+ }
+
+ a:hover {
+ color: var(--active);
+ }
+
+ @media (max-width: 1250px) {
+ div {
+ flex-direction: row;
+ justify-content: center;
+ margin: 0;
+ }
+ }
+
+ @media (max-width: 715px) {
+ a {
+ margin: 5px;
+ }
+ }
+
+ :global(.margin) {
+ margin-right: 5px;
+ margin-left: 0;
+ }
+ :global(.margin_rtl) {
+ margin-right: 0;
+ margin-left: 5px;
+ }
+</style>
diff --git a/src/pages/options_src/main.js b/src/pages/options_src/main.js
new file mode 100644
index 00000000..c4012f4a
--- /dev/null
+++ b/src/pages/options_src/main.js
@@ -0,0 +1,7 @@
+import App from "./App.svelte"
+
+const app = new App({
+ target: document.body,
+})
+
+export default app
diff --git a/src/pages/options_src/stores.js b/src/pages/options_src/stores.js
new file mode 100644
index 00000000..7ae0f8c7
--- /dev/null
+++ b/src/pages/options_src/stores.js
@@ -0,0 +1,4 @@
+import { writable } from "svelte/store"
+
+export const options = writable(null)
+export const config = writable(null)
diff --git a/src/pages/options_src/url.js b/src/pages/options_src/url.js
new file mode 100644
index 00000000..010e5b21
--- /dev/null
+++ b/src/pages/options_src/url.js
@@ -0,0 +1,38 @@
+// https://svelte.dev/repl/5abaac000b164aa1aacc6051d5c4f584?version=3.59.2
+
+import { derived, writable } from 'svelte/store'
+
+export function createUrlStore(ssrUrl) {
+ // Ideally a bundler constant so that it's tree-shakable
+ if (typeof window === 'undefined') {
+ const { subscribe } = writable(ssrUrl)
+ return { subscribe }
+ }
+
+ const href = writable(window.location.href)
+
+ const originalPushState = history.pushState
+ const originalReplaceState = history.replaceState
+
+ const updateHref = () => href.set(window.location.href)
+
+ history.pushState = () => {
+ originalPushState.apply(this, arguments)
+ updateHref()
+ }
+
+ history.replaceState = () => {
+ originalReplaceState.apply(this, arguments)
+ updateHref()
+ }
+
+ window.addEventListener('popstate', updateHref)
+ window.addEventListener('hashchange', updateHref)
+
+ return {
+ subscribe: derived(href, ($href) => new URL($href)).subscribe
+ }
+}
+
+// If you're using in a pure SPA, you can return a store directly and share it everywhere
+export default createUrlStore()