diff options
author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2024-09-11 18:44:14 +0200 |
---|---|---|
committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2024-09-11 18:44:14 +0200 |
commit | 2af0f5e64e47c59e575802249983feb8968959b1 (patch) | |
tree | a2d80749d6b297ef05b1893949081b678f9e8677 /src/pages/options_src | |
parent | chore(Merge): remote-tracking branch 'origin/master' (diff) | |
parent | Translated using Weblate (Arabic) (diff) | |
download | libredirect-2af0f5e64e47c59e575802249983feb8968959b1.zip |
chore(merge): Merge remote-tracking branch 'origin/master'
Following Conflicts have been resolved: README.md src/_locales/bs/messages.json src/_locales/de/messages.json src/_locales/en/messages.json src/_locales/fr/messages.json src/_locales/ko/messages.json src/_locales/nb_NO/messages.json src/_locales/pt/messages.json src/_locales/pt_BR/messages.json src/_locales/sr/messages.json src/_locales/vi/messages.json src/assets/images/libredirect.svg src/assets/javascripts/services.js src/config.json src/manifest.json src/updates/updates.xml
Diffstat (limited to 'src/pages/options_src')
-rw-r--r-- | src/pages/options_src/App.svelte | 101 | ||||
-rw-r--r-- | src/pages/options_src/General/Exceptions.svelte | 110 | ||||
-rw-r--r-- | src/pages/options_src/General/General.svelte | 98 | ||||
-rw-r--r-- | src/pages/options_src/General/SettingsButtons.svelte | 112 | ||||
-rw-r--r-- | src/pages/options_src/Services/FrontendIcon.svelte | 41 | ||||
-rw-r--r-- | src/pages/options_src/Services/Instances.svelte | 261 | ||||
-rw-r--r-- | src/pages/options_src/Services/RedirectType.svelte | 102 | ||||
-rw-r--r-- | src/pages/options_src/Services/ServiceIcon.svelte | 40 | ||||
-rw-r--r-- | src/pages/options_src/Services/Services.svelte | 260 | ||||
-rw-r--r-- | src/pages/options_src/Sidebar.svelte | 69 | ||||
-rw-r--r-- | src/pages/options_src/main.js | 7 | ||||
-rw-r--r-- | src/pages/options_src/stores.js | 4 | ||||
-rw-r--r-- | src/pages/options_src/url.js | 38 |
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() |