about summary refs log tree commit diff stats
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()