aboutsummaryrefslogtreecommitdiffstats
path: root/src/pages/options_src
diff options
context:
space:
mode:
authorManeraKai <manerakai@protonmail.com>2024-07-25 15:17:57 +0300
committerManeraKai <manerakai@protonmail.com>2024-07-25 15:17:57 +0300
commitc6de68c4c4bda3edcf6cf012bd98adea3baf866b (patch)
tree419e5cb8cdfe04fd734c5590e78888fa16e58e51 /src/pages/options_src
parentMade Select Frontend searchable and with icons (diff)
downloadlibredirect-c6de68c4c4bda3edcf6cf012bd98adea3baf866b.zip
Migrating popup to svelte
Diffstat (limited to 'src/pages/options_src')
-rw-r--r--src/pages/options_src/App.svelte118
-rw-r--r--src/pages/options_src/General/Exceptions.svelte108
-rw-r--r--src/pages/options_src/General/General.svelte70
-rw-r--r--src/pages/options_src/General/SettingsButtons.svelte118
-rw-r--r--src/pages/options_src/Services/FrontendIcon.svelte43
-rw-r--r--src/pages/options_src/Services/Instances.svelte233
-rw-r--r--src/pages/options_src/Services/RedirectType.svelte99
-rw-r--r--src/pages/options_src/Services/ServiceIcon.svelte40
-rw-r--r--src/pages/options_src/Services/Services.svelte188
-rw-r--r--src/pages/options_src/Sidebar.svelte43
-rw-r--r--src/pages/options_src/main.js7
-rw-r--r--src/pages/options_src/stores.js5
12 files changed, 1072 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..6bbacb9b
--- /dev/null
+++ b/src/pages/options_src/App.svelte
@@ -0,0 +1,118 @@
+<script>
+ let browser = window.browser || window.chrome
+
+ import General from "./General/General.svelte"
+ 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, page } 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 _page
+ page.subscribe(val => (_page = val))
+
+ const dark = {
+ text: "#fff",
+ bgMain: "#121212",
+ bgSecondary: "#202020",
+ active: "#fbc117",
+ danger: "#f04141",
+ lightGrey: "#c3c3c3",
+ }
+ const light = {
+ text: "black",
+ bgMain: "white",
+ bgSecondary: "#e4e4e4",
+ active: "#fb9817",
+ danger: "#f04141",
+ lightGrey: "#c3c3c3",
+ }
+ let cssVariables
+ $: if (_options) {
+ if (_options.theme == "dark") {
+ cssVariables = dark
+ } else if (_options.theme == "light") {
+ cssVariables = light
+ } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
+ cssVariables = dark
+ } else {
+ cssVariables = light
+ }
+ }
+</script>
+
+{#if _options && _config}
+ <div
+ class="main"
+ dir="auto"
+ style="
+ --text: {cssVariables.text};
+ --bg-main: {cssVariables.bgMain};
+ --bg-secondary: {cssVariables.bgSecondary};
+ --active: {cssVariables.active};
+ --danger: {cssVariables.danger};
+ --light-grey: {cssVariables.lightGrey};"
+ >
+ <Sidebar />
+ {#if _page == "general"}
+ <General />
+ {:else if _page == "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";
+ box-sizing: border-box;
+ font-size: 16px;
+ background-color: var(--bg-main);
+ color: var(--text);
+ overflow: scroll;
+ }
+</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..4ef5591b
--- /dev/null
+++ b/src/pages/options_src/General/Exceptions.svelte
@@ -0,0 +1,108 @@
+<script>
+ 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>
+
+<div class="block block-option">
+ <Row>
+ <Label>Excluded from redirecting</Label>
+ </Row>
+ <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..d5b2dd59
--- /dev/null
+++ b/src/pages/options_src/General/General.svelte
@@ -0,0 +1,70 @@
+<script>
+ let browser = window.browser || window.chrome
+
+ import Exceptions from "./Exceptions.svelte"
+ import SettingsButtons from "./SettingsButtons.svelte"
+ import RowSelect from "../../components/RowSelect.svelte"
+ import Checkbox from "../../components/RowCheckbox.svelte"
+ import { options } from "../stores"
+ import { onDestroy } from "svelte"
+
+ let _options
+ const unsubscribe = options.subscribe(val => (_options = val))
+ onDestroy(unsubscribe)
+
+ let bookmarksPermission
+ browser.permissions.contains({ permissions: ["bookmarks"] }, r => (bookmarksPermission = r))
+ $: if (bookmarksPermission) {
+ browser.permissions.request({ permissions: ["bookmarks"] }, r => (bookmarksPermission = r))
+ } else {
+ browser.permissions.remove({ permissions: ["bookmarks"] })
+ bookmarksPermission = false
+ }
+</script>
+
+<div>
+ <RowSelect
+ label="Theme"
+ values={[
+ { value: "detect", name: "Auto" },
+ { value: "light", name: "Light" },
+ { value: "dark", name: "Dark" },
+ ]}
+ value={_options.theme}
+ onChange={e => {
+ _options["theme"] = e.target.options[e.target.options.selectedIndex].value
+ options.set(_options)
+ }}
+ ariaLabel="select theme"
+ />
+
+ <RowSelect
+ label="Fetch public instances"
+ value={_options.fetchInstances}
+ onChange={e => {
+ _options["fetchInstances"] = e.target.options[e.target.options.selectedIndex].value
+ options.set(_options)
+ }}
+ ariaLabel="Select fetch public instances"
+ values={[
+ { value: "github", name: "GitHub" },
+ { value: "codeberg", name: "Codeberg" },
+ { value: "disable", name: "Disable" },
+ ]}
+ />
+
+ <Checkbox
+ label="Redirect Only in Incognito"
+ checked={_options.redirectOnlyInIncognito}
+ onChange={e => {
+ _options["redirectOnlyInIncognito"] = e.target.checked
+ options.set(_options)
+ }}
+ />
+
+ <Checkbox label="Bookmarks menu" bind:checked={bookmarksPermission} />
+
+ <Exceptions opts={_options} />
+
+ <SettingsButtons opts={_options} />
+</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..c37a3702
--- /dev/null
+++ b/src/pages/options_src/General/SettingsButtons.svelte
@@ -0,0 +1,118 @@
+<script>
+ let 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 disableButtons = false
+
+ let importSettingsInput
+ let importSettingsFiles
+ $: if (importSettingsFiles) {
+ disableButtons = true
+ const reader = new FileReader()
+ reader.readAsText(importSettingsFiles[0])
+ reader.onload = async () => {
+ const data = JSON.parse(reader.result)
+ if ("theme" in data && data.version == browser.runtime.getManifest().version) {
+ browser.storage.local.clear(async () => {
+ console.log("clearing")
+ options.set(data)
+ disableButtons = false
+ })
+ } else {
+ console.log("incompatible settings")
+ alert("Incompatible settings")
+ }
+ }
+ reader.onerror = error => {
+ console.log("error", error)
+ alert("Error!")
+ }
+ }
+
+ async function exportSettings() {
+ disableButtons = true
+ _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()
+ disableButtons = false
+ }
+
+ async function exportSettingsSync() {
+ disableButtons = true
+ _options.version = browser.runtime.getManifest().version
+ await servicesHelper.initDefaults()
+ browser.storage.sync.set({ options: _options })
+ disableButtons = false
+ }
+
+ async function importSettingsSync() {
+ disableButtons = true
+ browser.storage.sync.get({ options }, r => {
+ const optionsSync = r.options
+ if (optionsSync.version == browser.runtime.getManifest().version) {
+ options.set(optionsSync)
+ } else {
+ alert("Error")
+ }
+ disableButtons = false
+ })
+ }
+
+ async function resetSettings() {
+ disableButtons = true
+ browser.storage.local.clear(async () => {
+ await servicesHelper.initDefaults()
+ options.set(await utils.getOptions())
+ disableButtons = false
+ })
+ }
+</script>
+
+<div class="buttons">
+ <Button on:click={() => importSettingsInput.click()} disabled={disableButtons}>
+ <ImportIcon />
+ <x data-localise="__MSG_importSettings__">Import Settings</x>
+ </Button>
+ <input
+ type="file"
+ accept=".json"
+ style="display: none"
+ bind:this={importSettingsInput}
+ bind:files={importSettingsFiles}
+ />
+
+ <Button on:click={exportSettings} disabled={disableButtons}>
+ <ExportIcon />
+ <x data-localise="__MSG_exportSettings__">Export Settings</x>
+ </Button>
+
+ <Button on:click={exportSettingsSync} disabled={disableButtons}>
+ <ExportIcon />
+ <x>Export Settings to Sync</x>
+ </Button>
+
+ <Button on:click={importSettingsSync} disabled={disableButtons}>
+ <ImportIcon />
+ <x>Import Settings from Sync</x>
+ </Button>
+
+ <Button on:click={resetSettings} disabled={disableButtons}>
+ <ResetIcon />
+ <x>Reset Settings</x>
+ </Button>
+</div>
diff --git a/src/pages/options_src/Services/FrontendIcon.svelte b/src/pages/options_src/Services/FrontendIcon.svelte
new file mode 100644
index 00000000..24942fd6
--- /dev/null
+++ b/src/pages/options_src/Services/FrontendIcon.svelte
@@ -0,0 +1,43 @@
+<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"
+ }
+ }
+
+ export let selectedService
+
+ $: 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..892e395b
--- /dev/null
+++ b/src/pages/options_src/Services/Instances.svelte
@@ -0,0 +1,233 @@
+<script>
+ let 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 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
+
+ $: serviceOptions = _options[selectedService]
+ $: serviceConf = _config.services[selectedService]
+
+ let allInstances = []
+
+ $: {
+ allInstances = []
+ if (_options[selectedFrontend]) allInstances.push(..._options[selectedFrontend])
+ if (redirects && redirects[selectedFrontend]) {
+ for (const network in redirects[selectedFrontend]) {
+ allInstances.push(...redirects[selectedFrontend][network])
+ }
+ }
+ }
+
+ 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) {
+ console.log("pinging...", instance)
+ pingCache[instance] = { color: "lightblue", value: "pinging..." }
+ const time = await utils.ping(instance)
+ pingCache[instance] = processTime(time)
+ }
+ }
+ function processTime(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 dir="ltr">
+ <div class="ping">
+ <Button on:click={pingInstances}>
+ <PingIcon />
+ Ping Instances
+ </Button>
+ </div>
+
+ <Row>
+ <Label>Add your favorite instances</Label>
+ </Row>
+
+ <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;
+ }
+
+ a:hover {
+ text-decoration: underline;
+ }
+</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..7f7a2843
--- /dev/null
+++ b/src/pages/options_src/Services/RedirectType.svelte
@@ -0,0 +1,99 @@
+<script>
+ import { onDestroy } from "svelte"
+
+ import RowSelect from "../../components/RowSelect.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"
+
+ 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: "Both" },
+ { value: "sub_frame", name: "Only Embedded" },
+ { value: "main_frame", name: "Only Not Embedded" },
+ ]
+ } else if (
+ serviceConf.frontends[frontendName].desktopApp &&
+ Object.values(serviceConf.frontends).some(frontend => frontend.embeddable)
+ ) {
+ values = [
+ { value: "both", name: "both" },
+ { value: "main_frame", name: "Only Not Embedded" },
+ ]
+ if (serviceOptions.redirectType == "sub_frame") {
+ serviceOptions.redirectType = "main_frame"
+ options.set(_options)
+ }
+ } else {
+ values = [{ value: "main_frame", name: "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>
+
+<RowSelect
+ label="Redirect Type"
+ value={serviceOptions.redirectType}
+ onChange={e => {
+ serviceOptions.redirectType = e.target.options[e.target.options.selectedIndex].value
+ options.set(_options)
+ }}
+ {values}
+/>
+
+{#if serviceConf.frontends[frontendName].desktopApp && serviceOptions.redirectType != "main_frame"}
+ <Row>
+ <Label>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..cb1efacb
--- /dev/null
+++ b/src/pages/options_src/Services/Services.svelte
@@ -0,0 +1,188 @@
+<script>
+ import Checkbox from "../../components/RowCheckbox.svelte"
+ import RowSelect from "../../components/RowSelect.svelte"
+ 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"
+
+ let _options
+ let _config
+
+ const unsubscribeOptions = options.subscribe(val => (_options = val))
+ const unsubscribeConfig = config.subscribe(val => (_config = val))
+ onDestroy(() => {
+ unsubscribeOptions()
+ unsubscribeConfig()
+ })
+
+ let selectedService = "youtube"
+ $: serviceConf = _config.services[selectedService]
+ $: serviceOptions = _options[selectedService]
+ $: frontendWebsite = serviceConf.frontends[serviceOptions.frontend].url
+</script>
+
+<div>
+ <Row>
+ <Label>
+ Service:
+ <a href={serviceConf.url} target="_blank" rel="noopener noreferrer">{serviceConf.url}</a>
+ </Label>
+ <SvelteSelect
+ clearable={false}
+ class="svelte_select"
+ value={selectedService}
+ on:change={e => (selectedService = e.detail.value)}
+ items={[
+ ...Object.entries(_config.services).map(([serviceKey, service]) => {
+ return { value: serviceKey, label: service.name }
+ }),
+ ]}
+ >
+ <div class="slot" slot="item" let:item>
+ <ServiceIcon details={item} />
+ {item.label}
+ </div>
+ <div class="slot" slot="selection" let:selection>
+ <ServiceIcon details={selection} />
+ {selection.label}
+ </div>
+ </SvelteSelect>
+ </Row>
+
+ <hr />
+
+ <Checkbox
+ label="Enable"
+ checked={serviceOptions.enabled}
+ onChange={e => {
+ serviceOptions.enabled = e.target.checked
+ options.set(_options)
+ }}
+ />
+
+ <div style={!serviceOptions.enabled && "pointer-events: none;opacity: 0.4;user-select: none;"}>
+ <Checkbox
+ label="Show in popup"
+ 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>
+ <Label>
+ Frontend:
+ <a href={frontendWebsite} target="_blank" rel="noopener noreferrer">
+ {frontendWebsite}
+ </a>
+ </Label>
+
+ <SvelteSelect
+ clearable={false}
+ class="svelte_select"
+ value={serviceOptions.frontend}
+ on:change={e => {
+ serviceOptions.frontend = e.detail.value
+ options.set(_options)
+ }}
+ items={[
+ ...Object.entries(serviceConf.frontends).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>
+ <FrontendIcon details={selection} {selectedService} />
+ {selection.label}
+ </div>
+ </SvelteSelect>
+ </Row>
+
+ <RedirectType {selectedService} />
+
+ <RowSelect
+ label="Unsupported embeds handling"
+ value={serviceOptions.unsupportedUrls}
+ onChange={e => {
+ serviceOptions.unsupportedUrls = e.target.options[e.target.options.selectedIndex].value
+ options.set(_options)
+ }}
+ values={[
+ { value: "bypass", name: "Bypass" },
+ { value: "block", name: "Block" },
+ ]}
+ />
+
+ {#if selectedService == "search"}
+ <Row>
+ <Label>
+ 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;
+ --width: 210px;
+ --background: var(--bg-secondary);
+ --list-background: var(--bg-secondary);
+ --item-active-background: red;
+ --item-is-active-bg: grey;
+ --item-hover-bg: grey;
+ --item-is-active-color: var(--text);
+ --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;
+ height: 26px;
+ width: 26px;
+ color: var(--text);
+ }
+</style>
diff --git a/src/pages/options_src/Sidebar.svelte b/src/pages/options_src/Sidebar.svelte
new file mode 100644
index 00000000..ff44995f
--- /dev/null
+++ b/src/pages/options_src/Sidebar.svelte
@@ -0,0 +1,43 @@
+<script>
+ import { page } from "./stores"
+ import GeneralIcon from "../icons/GeneralIcon.svelte"
+ import ServicesIcon from "../icons/ServicesIcon.svelte"
+ import AboutIcon from "../icons/AboutIcon.svelte"
+</script>
+
+<div>
+ <a href="#general" on:click={() => page.set("general")} style={$page == "general" && "color: var(--active);"}>
+ <GeneralIcon style="margin-right: 5px" />
+ <span data-localise="__MSG_general__">General</span>
+ </a>
+ <a href="#services" on:click={() => page.set("services")} style={$page == "services" && "color: var(--active);"}>
+ <ServicesIcon style="margin-right: 5px" />
+ <span data-localise="__MSG_services__">Services</span>
+ </a>
+ <a href="https://libredirect.github.io" target="_blank" rel="noopener noreferrer">
+ <AboutIcon style="margin-right: 5px" />
+ <span data-localise="__MSG_about__">About</span>
+ </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;
+ }
+
+ a:hover {
+ color: var(--active);
+ }
+</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..782f6064
--- /dev/null
+++ b/src/pages/options_src/stores.js
@@ -0,0 +1,5 @@
+import { writable } from "svelte/store"
+
+export const options = writable(null)
+export const config = writable(null)
+export const page = writable("general")