mirror of
https://github.com/bytedream/stream-bypass.git
synced 2026-08-04 21:10:40 +02:00
add per domain settings (#30)
This commit is contained in:
@@ -154,6 +154,11 @@ When using firefox, use the following:
|
|||||||
|
|
||||||
You can enable or disabled for which hosts the extension should redirect.
|
You can enable or disabled for which hosts the extension should redirect.
|
||||||
|
|
||||||
|
### Per-site disables
|
||||||
|
|
||||||
|
The popup shows the current website at the top. Press **Configure this site** to disable specific hosts (or all of them) for the current site only — useful when a site uses multiple hosts and you only want to bypass a few, or when a host misbehaves on a particular site.
|
||||||
|
All per-site disables are listed here and can be edited or removed.
|
||||||
|
|
||||||
### ff2mpv
|
### ff2mpv
|
||||||
|
|
||||||
[ff2mpv](https://github.com/woodruffw/ff2mpv) allows you to play streams directly in [mpv](https://mpv.io/) instead of the browser.
|
[ff2mpv](https://github.com/woodruffw/ff2mpv) allows you to play streams directly in [mpv](https://mpv.io/) instead of the browser.
|
||||||
|
|||||||
@@ -5,13 +5,14 @@
|
|||||||
import Toast from '@/entrypoints/popup/components/Toast.svelte';
|
import Toast from '@/entrypoints/popup/components/Toast.svelte';
|
||||||
import Main from '@/entrypoints/popup/pages/main/Main.svelte';
|
import Main from '@/entrypoints/popup/pages/main/Main.svelte';
|
||||||
import Settings from '@/entrypoints/popup/pages/settings/Settings.svelte';
|
import Settings from '@/entrypoints/popup/pages/settings/Settings.svelte';
|
||||||
|
import SiteConfig from '@/entrypoints/popup/pages/site-config/SiteConfig.svelte';
|
||||||
import { isMobile } from '@/entrypoints/popup/state.js';
|
import { isMobile } from '@/entrypoints/popup/state.js';
|
||||||
|
|
||||||
/* state init */
|
/* state init */
|
||||||
browser.runtime.getPlatformInfo().then((info) => ($isMobile = info.os === 'android'));
|
browser.runtime.getPlatformInfo().then((info) => ($isMobile = info.os === 'android'));
|
||||||
|
|
||||||
/* types */
|
/* types */
|
||||||
type Page = 'main' | 'settings';
|
type Page = 'main' | 'settings' | 'site-config';
|
||||||
|
|
||||||
/* states */
|
/* states */
|
||||||
let activePage = $state<Page>('main');
|
let activePage = $state<Page>('main');
|
||||||
@@ -21,7 +22,10 @@
|
|||||||
<Toast />
|
<Toast />
|
||||||
{#if activePage === 'main'}
|
{#if activePage === 'main'}
|
||||||
<div transition:fly={{ x: -300, duration: 150 }} class="min-w-full w-full h-75 flex-1 flex flex-col">
|
<div transition:fly={{ x: -300, duration: 150 }} class="min-w-full w-full h-75 flex-1 flex flex-col">
|
||||||
<Main onSettingsOpenRequest={() => (activePage = 'settings')} />
|
<Main
|
||||||
|
onSettingsOpenRequest={() => (activePage = 'settings')}
|
||||||
|
onSiteConfigOpenRequest={() => (activePage = 'site-config')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else if activePage === 'settings'}
|
{:else if activePage === 'settings'}
|
||||||
<div
|
<div
|
||||||
@@ -31,5 +35,13 @@
|
|||||||
>
|
>
|
||||||
<Settings onSettingsCloseRequest={() => (activePage = 'main')} />
|
<Settings onSettingsCloseRequest={() => (activePage = 'main')} />
|
||||||
</div>
|
</div>
|
||||||
|
{:else if activePage === 'site-config'}
|
||||||
|
<div
|
||||||
|
transition:fly={{ x: 300, duration: 150 }}
|
||||||
|
class="min-w-full w-full h-120 flex-1 flex flex-col"
|
||||||
|
class:h-screen={$isMobile}
|
||||||
|
>
|
||||||
|
<SiteConfig onBackClick={() => (activePage = 'main')} />
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,12 +6,13 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
host: Host;
|
host: Host;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
badge?: string;
|
||||||
onclick?: () => void;
|
onclick?: () => void;
|
||||||
actions: Snippet;
|
actions: Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* states */
|
/* states */
|
||||||
let { host, enabled, onclick, actions }: Props = $props();
|
let { host, enabled, badge, onclick, actions }: Props = $props();
|
||||||
|
|
||||||
let rowClass = $derived(
|
let rowClass = $derived(
|
||||||
`flex items-center gap-3 px-3 py-2 rounded-md transition-colors ${onclick ? 'hover:bg-gray-800/40 cursor-pointer' : ''}`
|
`flex items-center gap-3 px-3 py-2 rounded-md transition-colors ${onclick ? 'hover:bg-gray-800/40 cursor-pointer' : ''}`
|
||||||
@@ -40,10 +41,18 @@
|
|||||||
class:bg-linux-mint-green={enabled === true}
|
class:bg-linux-mint-green={enabled === true}
|
||||||
class:bg-gray-500={enabled === false}
|
class:bg-gray-500={enabled === false}
|
||||||
class:bg-transparent={enabled === undefined}
|
class:bg-transparent={enabled === undefined}
|
||||||
aria-hidden="true"
|
|
||||||
></span>
|
></span>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<p class="text-sm font-semibold text-gray-100 truncate">{host.name}</p>
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
|
<p class="text-sm font-semibold text-gray-100 truncate">{host.name}</p>
|
||||||
|
{#if badge}
|
||||||
|
<span
|
||||||
|
class="shrink-0 text-[0.65rem] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded bg-linux-mint-green/20 text-linux-mint-green"
|
||||||
|
>
|
||||||
|
{badge}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<p class="text-xs text-gray-400 truncate" title={host.domains.join(', ')}>
|
<p class="text-xs text-gray-400 truncate" title={host.domains.join(', ')}>
|
||||||
{host.domains.join(', ')}
|
{host.domains.join(', ')}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ExclamationTriangle, InformationCircle } from '@steeze-ui/heroicons';
|
||||||
|
import { Icon } from '@steeze-ui/svelte-icon';
|
||||||
|
|
||||||
|
/* types */
|
||||||
|
interface Props {
|
||||||
|
level: 'warning' | 'info';
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let { level, title, description }: Props = $props();
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
warning: ExclamationTriangle,
|
||||||
|
info: InformationCircle
|
||||||
|
};
|
||||||
|
const containerStyles = {
|
||||||
|
warning: 'bg-yellow-900/30 border-yellow-700/50 text-yellow-200',
|
||||||
|
info: 'bg-blue-900/30 border-blue-700/50 text-blue-200'
|
||||||
|
};
|
||||||
|
const iconStyles = {
|
||||||
|
warning: 'text-yellow-400',
|
||||||
|
info: 'text-blue-400'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mx-2 mt-1 flex items-start gap-2 px-2.5 py-2 border rounded-md text-xs {containerStyles[level]}">
|
||||||
|
<Icon src={icons[level]} size="0.95rem" class={iconStyles[level]} />
|
||||||
|
<div class="leading-snug">
|
||||||
|
{#if title}
|
||||||
|
<p class="font-semibold">{title}</p>
|
||||||
|
{/if}
|
||||||
|
{#if description}
|
||||||
|
<p class="opacity-80">{description}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -20,14 +20,12 @@
|
|||||||
type="text"
|
type="text"
|
||||||
bind:value
|
bind:value
|
||||||
{placeholder}
|
{placeholder}
|
||||||
aria-label="Filter hosts"
|
|
||||||
class="flex-1 bg-transparent text-sm text-gray-100 placeholder:text-gray-500 outline-none min-w-0"
|
class="flex-1 bg-transparent text-sm text-gray-100 placeholder:text-gray-500 outline-none min-w-0"
|
||||||
/>
|
/>
|
||||||
{#if value}
|
{#if value}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="text-gray-400 hover:text-gray-100 cursor-pointer shrink-0"
|
class="text-gray-400 hover:text-gray-100 cursor-pointer shrink-0"
|
||||||
aria-label="Clear search"
|
|
||||||
onclick={() => (value = '')}
|
onclick={() => (value = '')}
|
||||||
>
|
>
|
||||||
<Icon src={XMark} size="0.95rem" />
|
<Icon src={XMark} size="0.95rem" />
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-full flex items-center justify-between gap-2 px-3 py-2.5 cursor-pointer hover:bg-gray-800/60 transition-colors"
|
class="w-full flex items-center justify-between gap-2 px-3 py-2.5 cursor-pointer hover:bg-gray-800/60 transition-colors"
|
||||||
aria-expanded={open}
|
|
||||||
onclick={() => (open = !open)}
|
onclick={() => (open = !open)}
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2 min-w-0">
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{#if $toast}
|
{#if $toast}
|
||||||
<div
|
<div
|
||||||
class="absolute top-2 left-1/2 -translate-x-1/2 z-20 px-3 py-1.5 bg-gray-700 text-gray-100 text-sm rounded shadow-lg pointer-events-none"
|
class="absolute top-2 left-1/2 -translate-x-1/2 z-20 w-max max-w-11/12 px-3 py-1.5 bg-gray-700 text-gray-100 text-sm rounded shadow-lg pointer-events-none"
|
||||||
transition:fly={{ y: -20, duration: 200 }}
|
transition:fly={{ y: -20, duration: 200 }}
|
||||||
>
|
>
|
||||||
{$toast.message}
|
{$toast.message}
|
||||||
|
|||||||
@@ -4,14 +4,17 @@
|
|||||||
checked: boolean;
|
checked: boolean;
|
||||||
onChecked?: (checked: boolean) => void | boolean | Promise<void> | Promise<boolean>;
|
onChecked?: (checked: boolean) => void | boolean | Promise<void> | Promise<boolean>;
|
||||||
size?: 'sm' | 'md';
|
size?: 'sm' | 'md';
|
||||||
|
disabled?: boolean;
|
||||||
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* states */
|
/* states */
|
||||||
let { checked = $bindable(), onChecked, size = 'md' }: Props = $props();
|
let { checked = $bindable(), onChecked, size = 'md', disabled = false, title }: Props = $props();
|
||||||
let internalChecked = $state($state.snapshot(checked));
|
let internalChecked = $state($state.snapshot(checked));
|
||||||
|
|
||||||
/* callbacks */
|
/* callbacks */
|
||||||
async function onInputChange() {
|
async function onInputChange() {
|
||||||
|
if (disabled) return;
|
||||||
internalChecked = !internalChecked;
|
internalChecked = !internalChecked;
|
||||||
let approved = false;
|
let approved = false;
|
||||||
|
|
||||||
@@ -36,9 +39,21 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<label class="flex items-center cursor-pointer">
|
<label
|
||||||
|
class="flex items-center"
|
||||||
|
class:cursor-pointer={!disabled}
|
||||||
|
class:cursor-not-allowed={disabled}
|
||||||
|
class:opacity-50={disabled}
|
||||||
|
{title}
|
||||||
|
>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input type="checkbox" class="peer sr-only" bind:checked={internalChecked} onchange={onInputChange} />
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="peer sr-only"
|
||||||
|
bind:checked={internalChecked}
|
||||||
|
onchange={onInputChange}
|
||||||
|
{disabled}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
class="block rounded-full box bg-red-700 peer-checked:bg-linux-mint-green"
|
class="block rounded-full box bg-red-700 peer-checked:bg-linux-mint-green"
|
||||||
class:w-8={size === 'sm'}
|
class:w-8={size === 'sm'}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AdjustmentsHorizontal, ChevronRight, GlobeAlt } from '@steeze-ui/heroicons';
|
||||||
|
import { Icon } from '@steeze-ui/svelte-icon';
|
||||||
|
|
||||||
|
/* types */
|
||||||
|
interface Props {
|
||||||
|
domain: string;
|
||||||
|
onConfigureClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let { domain, onConfigureClick }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between gap-2 px-3 py-1.5 text-sm text-gray-300">
|
||||||
|
<span class="flex items-center gap-2 min-w-0">
|
||||||
|
<Icon src={GlobeAlt} size="0.95rem" class="text-gray-400 shrink-0" />
|
||||||
|
<span class="truncate" title={domain}>{domain}</span>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex items-center gap-1 shrink-0 text-xs text-gray-400 hover:text-gray-200 px-1.5 py-0.5 rounded hover:bg-gray-800/60 cursor-pointer transition-colors"
|
||||||
|
onclick={onConfigureClick}
|
||||||
|
>
|
||||||
|
<Icon src={AdjustmentsHorizontal} size="0.85rem" />
|
||||||
|
<span>Configure</span>
|
||||||
|
<Icon src={ChevronRight} size="0.75rem" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@@ -3,11 +3,11 @@
|
|||||||
|
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import Divider from '@/entrypoints/popup/components/Divider.svelte';
|
import Divider from '@/entrypoints/popup/components/Divider.svelte';
|
||||||
import AllDisabled from '@/entrypoints/popup/pages/main/AllDisabled.svelte';
|
import Notice from '@/entrypoints/popup/components/Notice.svelte';
|
||||||
import CopyMatch from '@/entrypoints/popup/pages/main/CopyMatch.svelte';
|
import CopyMatch from '@/entrypoints/popup/pages/main/CopyMatch.svelte';
|
||||||
|
import CurrentSite from '@/entrypoints/popup/pages/main/CurrentSite.svelte';
|
||||||
import Header from '@/entrypoints/popup/pages/main/Header.svelte';
|
import Header from '@/entrypoints/popup/pages/main/Header.svelte';
|
||||||
import Match from '@/entrypoints/popup/pages/main/Match.svelte';
|
import Match from '@/entrypoints/popup/pages/main/Match.svelte';
|
||||||
import NoMatch from '@/entrypoints/popup/pages/main/NoMatch.svelte';
|
|
||||||
import { listenMessages, MessageType, sendMessageToActiveTab } from '@/lib/communication';
|
import { listenMessages, MessageType, sendMessageToActiveTab } from '@/lib/communication';
|
||||||
import { hosts, type Host } from '@/lib/host';
|
import { hosts, type Host } from '@/lib/host';
|
||||||
import { HostSettings } from '@/lib/settings';
|
import { HostSettings } from '@/lib/settings';
|
||||||
@@ -15,11 +15,13 @@
|
|||||||
/* types */
|
/* types */
|
||||||
interface Props {
|
interface Props {
|
||||||
onSettingsOpenRequest: () => void;
|
onSettingsOpenRequest: () => void;
|
||||||
|
onSiteConfigOpenRequest: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* states */
|
/* states */
|
||||||
let { onSettingsOpenRequest }: Props = $props();
|
let { onSettingsOpenRequest, onSiteConfigOpenRequest }: Props = $props();
|
||||||
let currentMatch = $state<{ host: Host; url: string; domain: string } | null>(null);
|
let currentMatch = $state<{ host: Host; url: string; domain: string } | null>(null);
|
||||||
|
let currentDomain = $state<string | null>(null);
|
||||||
|
|
||||||
let allHostsDisabled = $state(false);
|
let allHostsDisabled = $state(false);
|
||||||
HostSettings.getAllHostsDisabled().then((val) => (allHostsDisabled = val));
|
HostSettings.getAllHostsDisabled().then((val) => (allHostsDisabled = val));
|
||||||
@@ -27,35 +29,73 @@
|
|||||||
/* lifecycle */
|
/* lifecycle */
|
||||||
const cancel = listenMessages((type, data) => {
|
const cancel = listenMessages((type, data) => {
|
||||||
if (type !== MessageType.NotifyActiveMatch) return;
|
if (type !== MessageType.NotifyActiveMatch) return;
|
||||||
currentMatch = {
|
const match = {
|
||||||
host: hosts.find((host) => host.id === data.id)!,
|
host: hosts.find((host) => host.id === data.id)!,
|
||||||
url: data.url,
|
url: data.url,
|
||||||
domain: data.domain
|
domain: data.domain
|
||||||
};
|
};
|
||||||
|
currentMatch = match;
|
||||||
|
currentDomain = match.domain;
|
||||||
});
|
});
|
||||||
sendMessageToActiveTab(MessageType.RequestActiveMatch, undefined);
|
sendMessageToActiveTab(MessageType.RequestActiveMatch, undefined);
|
||||||
|
|
||||||
|
browser.tabs
|
||||||
|
.query({ active: true, currentWindow: true })
|
||||||
|
.then((tabs) => {
|
||||||
|
if (currentDomain) return;
|
||||||
|
const url = tabs[0]?.url;
|
||||||
|
if (!url) return;
|
||||||
|
try {
|
||||||
|
currentDomain = new URL(url).hostname;
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
onDestroy(cancel);
|
onDestroy(cancel);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full shrink-0">
|
||||||
<Header bind:allHostsDisabled onSettingsClick={onSettingsOpenRequest} />
|
<Header bind:allHostsDisabled onSettingsClick={onSettingsOpenRequest} />
|
||||||
<Divider />
|
<Divider />
|
||||||
</div>
|
</div>
|
||||||
<div class="px-2 h-full">
|
<div class="grid grid-cols-1 grid-rows-[min-content_auto_min-content] gap-2 h-full">
|
||||||
{#if allHostsDisabled}
|
<div class="px-2 pt-1.5 shrink-0">
|
||||||
<AllDisabled />
|
{#if currentDomain}
|
||||||
{:else if !currentMatch}
|
<CurrentSite domain={currentDomain} onConfigureClick={onSiteConfigOpenRequest} />
|
||||||
<NoMatch />
|
{/if}
|
||||||
{:else}
|
</div>
|
||||||
<div class="flex flex-col justify-between h-full pb-2">
|
|
||||||
<Match host={currentMatch.host} domain={currentMatch.domain} />
|
<div>
|
||||||
<div class="divider border-dashed"></div>
|
{#if allHostsDisabled}
|
||||||
<div class="mt-2">
|
<Notice
|
||||||
|
level="warning"
|
||||||
|
title="Extension disabled"
|
||||||
|
description="Configurations are still saved and take effect when extension is re-enabled."
|
||||||
|
/>
|
||||||
|
{:else if currentMatch}
|
||||||
|
<div class="shrink-0 flex justify-center py-2">
|
||||||
|
<Match host={currentMatch.host} />
|
||||||
|
</div>
|
||||||
|
<div class="shrink-0 mt-auto">
|
||||||
<CopyMatch url={currentMatch.url} domain={currentMatch.domain} />
|
<CopyMatch url={currentMatch.url} domain={currentMatch.domain} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{:else}
|
||||||
{/if}
|
<div class="flex-1 min-h-0 flex flex-col">
|
||||||
|
<Notice level="info" title="No match found" description="No redirectable host found on this site" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-2 pb-0.5">
|
||||||
|
<p class="text-xs text-gray-400 text-center">
|
||||||
|
Suggestions or bugs can be submitted <a
|
||||||
|
class="underline"
|
||||||
|
href="https://github.com/bytedream/stream-bypass/issues">here</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -4,17 +4,13 @@
|
|||||||
/* types */
|
/* types */
|
||||||
interface Props {
|
interface Props {
|
||||||
host: Host;
|
host: Host;
|
||||||
domain: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* states */
|
/* states */
|
||||||
const { host, domain }: Props = $props();
|
const { host }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col items-center justify-center w-full h-full">
|
<div class="flex flex-col items-center justify-center">
|
||||||
<p class="text-lg">Match found:</p>
|
<p class="text-md text-gray-400">Match found:</p>
|
||||||
<div class="*:select-text">
|
<span class="underline text-green-400 text-xl font-bold select-text">{host.name}</span>
|
||||||
<span class="underline text-green-400 text-2xl font-bold">{host.name}</span>
|
|
||||||
<span class="text-xs text-slate-300">({domain})</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
<script lang="ts"></script>
|
|
||||||
|
|
||||||
<div class="relative h-full">
|
|
||||||
<div class="h-full flex items-center justify-center">
|
|
||||||
<p class="text-[1.05rem]">No supported video found on this site</p>
|
|
||||||
</div>
|
|
||||||
<div class="absolute bottom-0.5">
|
|
||||||
<p class="text-xs text-gray-400">
|
|
||||||
Suggestions or bugs can be submitted <a
|
|
||||||
class="underline"
|
|
||||||
href="https://github.com/bytedream/stream-bypass/issues">here</a
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex items-center justify-center w-7 h-7 rounded-md text-gray-300 hover:text-gray-100 hover:bg-gray-800/60 cursor-pointer transition-colors"
|
class="flex items-center justify-center w-7 h-7 rounded-md text-gray-300 hover:text-gray-100 hover:bg-gray-800/60 cursor-pointer transition-colors"
|
||||||
aria-label="Back"
|
|
||||||
onclick={() => onBackClick()}
|
onclick={() => onBackClick()}
|
||||||
>
|
>
|
||||||
<Icon src={ArrowLeft} size="1.1rem" />
|
<Icon src={ArrowLeft} size="1.1rem" />
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { showToast } from '../../state';
|
||||||
|
import { Trash } from '@steeze-ui/heroicons';
|
||||||
|
import { Icon } from '@steeze-ui/svelte-icon';
|
||||||
|
import HostRow from '@/entrypoints/popup/components/HostRow.svelte';
|
||||||
|
import { getHostFromId, hosts } from '@/lib/host';
|
||||||
|
import { PerDomainSettings, type PerDomainConfig } from '@/lib/settings';
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let perDomainMap = $state<Record<string, PerDomainConfig>>({});
|
||||||
|
let expanded = $state<string | null>(null);
|
||||||
|
|
||||||
|
let newDomainInput = $state<string>('');
|
||||||
|
let selectedHostId = $state<string>('');
|
||||||
|
|
||||||
|
loadMap();
|
||||||
|
|
||||||
|
/* functions */
|
||||||
|
async function loadMap() {
|
||||||
|
perDomainMap = await PerDomainSettings.getMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDomain(input: string): string | null {
|
||||||
|
let d = input.trim().toLowerCase();
|
||||||
|
if (!d) return null;
|
||||||
|
d = d.replace(/^https?:\/\//, '');
|
||||||
|
d = d.split('/')[0];
|
||||||
|
d = d.split(':')[0];
|
||||||
|
if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/.test(d)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* callbacks */
|
||||||
|
async function onClear(domain: string) {
|
||||||
|
await PerDomainSettings.clear(domain);
|
||||||
|
await loadMap();
|
||||||
|
if (expanded === domain) expanded = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onAllRemove(domain: string) {
|
||||||
|
await PerDomainSettings.setAllDisabled(domain, false);
|
||||||
|
await loadMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onHostRemove(domain: string, hostId: string) {
|
||||||
|
await PerDomainSettings.setHostDisabled(domain, hostId, false);
|
||||||
|
await loadMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onHostAdd(domain: string, hostId: string) {
|
||||||
|
if (!hostId) return;
|
||||||
|
await PerDomainSettings.setHostDisabled(domain, hostId, true);
|
||||||
|
await loadMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDomainAdd(input: string) {
|
||||||
|
const domain = parseDomain(input);
|
||||||
|
if (!domain) {
|
||||||
|
showToast('Invalid domain');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await PerDomainSettings.add(domain);
|
||||||
|
await loadMap();
|
||||||
|
expanded = domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleExpand(domain: string) {
|
||||||
|
expanded = expanded === domain ? null : domain;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1 h-full overflow-y-scroll">
|
||||||
|
<div class="flex items-center gap-2 pb-1.5">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={newDomainInput}
|
||||||
|
placeholder="Add custom domain…"
|
||||||
|
class="flex-1 min-w-0 text-xs bg-gray-900/60 border border-gray-700 rounded px-2 py-1 text-gray-100 placeholder:text-gray-500 focus:border-linux-mint-green focus:outline-none transition-colors"
|
||||||
|
onkeydown={(e) => {
|
||||||
|
if (e.key === 'Enter') onDomainAdd(newDomainInput).then(() => (newDomainInput = ''));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="shrink-0 px-2 py-1 text-xs rounded bg-gray-700/60 hover:bg-linux-mint-green hover:text-white text-gray-200 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition-colors"
|
||||||
|
disabled={!newDomainInput.trim()}
|
||||||
|
onclick={() => onDomainAdd(newDomainInput).then(() => (newDomainInput = ''))}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{#if Object.keys(perDomainMap).length === 0}
|
||||||
|
<p class="text-xs text-gray-400 text-center py-2">
|
||||||
|
No per-site disables configured. Use the popup on a streaming site to configure it.
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
{#each Object.entries(perDomainMap) as [domain, settings] (domain)}
|
||||||
|
{@const isOpen = expanded === domain}
|
||||||
|
<div class="border border-gray-700/60 rounded-md overflow-hidden">
|
||||||
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<div
|
||||||
|
class="w-full flex items-center justify-between gap-2 px-2.5 py-1.5 bg-gray-900/40 hover:bg-gray-800/60 cursor-pointer transition-colors"
|
||||||
|
role="button"
|
||||||
|
tabindex={0}
|
||||||
|
onclick={() => toggleExpand(domain)}
|
||||||
|
>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="text-sm font-semibold text-gray-100 truncate">{domain}</p>
|
||||||
|
<p class="text-[0.7rem] text-gray-400">
|
||||||
|
{#if settings.allDisabled}
|
||||||
|
All hosts disabled
|
||||||
|
{:else}
|
||||||
|
{settings.disabledHostIds.length}
|
||||||
|
{settings.disabledHostIds.length === 1 ? 'host' : 'hosts'} disabled
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="shrink-0 p-1 rounded text-gray-400 hover:text-red-400 hover:bg-gray-700/40 cursor-pointer transition-colors"
|
||||||
|
onclick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onClear(domain);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon src={Trash} size="0.9rem" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{#if isOpen}
|
||||||
|
{@const availableHosts = hosts.filter((h) => !settings.disabledHostIds.includes(h.id))}
|
||||||
|
<div class="px-2 py-2 border-t border-gray-700/60 flex flex-col gap-2">
|
||||||
|
{#if settings.allDisabled}
|
||||||
|
<div class="flex items-center justify-between gap-2 px-2 py-1.5 bg-gray-900/40 rounded">
|
||||||
|
<p class="text-xs text-gray-300">All hosts disabled</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="shrink-0 p-1 rounded text-gray-400 hover:text-red-400 hover:bg-gray-700/40 cursor-pointer transition-colors"
|
||||||
|
onclick={() => onAllRemove(domain)}
|
||||||
|
>
|
||||||
|
<Icon src={Trash} size="0.9rem" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if settings.disabledHostIds.length > 0}
|
||||||
|
<div class="flex flex-col -mx-1 max-h-60 overflow-y-auto">
|
||||||
|
{#each settings.disabledHostIds as hostId (hostId)}
|
||||||
|
{@const host = getHostFromId(hostId)}
|
||||||
|
{#if host}
|
||||||
|
<HostRow {host} enabled={false}>
|
||||||
|
{#snippet actions()}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="shrink-0 p-1 rounded text-gray-400 hover:text-red-400 hover:bg-gray-700/40 cursor-pointer transition-colors"
|
||||||
|
onclick={() => onHostRemove(domain, hostId)}
|
||||||
|
>
|
||||||
|
<Icon src={Trash} size="0.9rem" />
|
||||||
|
</button>
|
||||||
|
{/snippet}
|
||||||
|
</HostRow>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if !settings.allDisabled && settings.disabledHostIds.length === 0}
|
||||||
|
<p class="text-xs text-gray-400 text-center py-1">No overrides set</p>
|
||||||
|
{/if}
|
||||||
|
<div class="flex items-center gap-2 pt-1">
|
||||||
|
<select
|
||||||
|
bind:value={selectedHostId}
|
||||||
|
class="flex-1 min-w-0 text-xs bg-gray-900/60 border border-gray-700 rounded px-1.5 py-1 text-gray-100 cursor-pointer focus:border-linux-mint-green focus:outline-none transition-colors"
|
||||||
|
>
|
||||||
|
<option value="" disabled>Add host…</option>
|
||||||
|
{#each availableHosts as host (host.id)}
|
||||||
|
<option value={host.id}>{host.name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="shrink-0 px-2 py-1 text-xs rounded bg-gray-700/60 hover:bg-linux-mint-green hover:text-white text-gray-200 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition-colors"
|
||||||
|
disabled={!selectedHostId}
|
||||||
|
onclick={() => onHostAdd(domain, selectedHostId).then(() => (selectedHostId = ''))}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { CommandLine, ServerStack } from '@steeze-ui/heroicons';
|
import { CommandLine, MapPin, ServerStack } from '@steeze-ui/heroicons';
|
||||||
import Divider from '@/entrypoints/popup/components/Divider.svelte';
|
import Divider from '@/entrypoints/popup/components/Divider.svelte';
|
||||||
import Section from '@/entrypoints/popup/components/Section.svelte';
|
import Section from '@/entrypoints/popup/components/Section.svelte';
|
||||||
import Ff2mpv from '@/entrypoints/popup/pages/settings/Ff2mpv.svelte';
|
import Ff2mpv from '@/entrypoints/popup/pages/settings/Ff2mpv.svelte';
|
||||||
import Header from '@/entrypoints/popup/pages/settings/Header.svelte';
|
import Header from '@/entrypoints/popup/pages/settings/Header.svelte';
|
||||||
import HostsTable from '@/entrypoints/popup/pages/settings/HostsTable.svelte';
|
import HostsTable from '@/entrypoints/popup/pages/settings/HostsTable.svelte';
|
||||||
|
import PerSiteDisables from '@/entrypoints/popup/pages/settings/PerSiteDisables.svelte';
|
||||||
import { isMobile } from '@/entrypoints/popup/state';
|
import { isMobile } from '@/entrypoints/popup/state';
|
||||||
|
|
||||||
/* types */
|
/* types */
|
||||||
@@ -32,6 +33,9 @@
|
|||||||
<HostsTable bind:searchQuery />
|
<HostsTable bind:searchQuery />
|
||||||
</Section>
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
|
<Section title="Per-site disables" description="Disable hosts for specific websites" icon={MapPin}>
|
||||||
|
<PerSiteDisables />
|
||||||
|
</Section>
|
||||||
{#if !$isMobile}
|
{#if !$isMobile}
|
||||||
<Section title="ff2mpv" description="Play streams directly in mpv via native messaging" icon={CommandLine}>
|
<Section title="ff2mpv" description="Play streams directly in mpv via native messaging" icon={CommandLine}>
|
||||||
<Ff2mpv />
|
<Ff2mpv />
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ArrowLeft } from '@steeze-ui/heroicons';
|
||||||
|
import { Icon } from '@steeze-ui/svelte-icon';
|
||||||
|
|
||||||
|
/* types */
|
||||||
|
interface Props {
|
||||||
|
domain: string;
|
||||||
|
onBackClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let { domain, onBackClick }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 px-3 py-2.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex items-center justify-center w-7 h-7 rounded-md text-gray-300 hover:text-gray-100 hover:bg-gray-800/60 cursor-pointer transition-colors"
|
||||||
|
onclick={() => onBackClick()}
|
||||||
|
>
|
||||||
|
<Icon src={ArrowLeft} size="1.1rem" />
|
||||||
|
</button>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<h1 class="text-lg font-semibold text-gray-100 leading-tight">Site settings</h1>
|
||||||
|
<p class="text-xs text-gray-400 leading-tight truncate" title={domain}>{domain}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import HostRow from '@/entrypoints/popup/components/HostRow.svelte';
|
||||||
|
import Toggle from '@/entrypoints/popup/components/Toggle.svelte';
|
||||||
|
import { hosts, type Host } from '@/lib/host';
|
||||||
|
import { PerDomainSettings } from '@/lib/settings';
|
||||||
|
|
||||||
|
/* types */
|
||||||
|
interface Props {
|
||||||
|
domain: string;
|
||||||
|
matchedHostId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let { domain, matchedHostId }: Props = $props();
|
||||||
|
|
||||||
|
let perDomain = $state<{ allDisabled: boolean; disabledHostIds: string[] }>({
|
||||||
|
allDisabled: false,
|
||||||
|
disabledHostIds: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// `domain` is updated after this component is active, and thus we must react to this update
|
||||||
|
$effect(() => {
|
||||||
|
PerDomainSettings.get(domain).then((val) => (perDomain = val));
|
||||||
|
});
|
||||||
|
|
||||||
|
let orderedHosts = $derived(getOrderedHosts(matchedHostId));
|
||||||
|
|
||||||
|
/* functions */
|
||||||
|
function getOrderedHosts(matchedId: string | null): Host[] {
|
||||||
|
const matched = matchedId ? hosts.find((h) => h.id === matchedId) : null;
|
||||||
|
const rest = hosts
|
||||||
|
.filter((h) => !matched || h.id !== matched.id)
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
return matched ? [matched, ...rest] : rest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* callbacks */
|
||||||
|
async function onHostToggle(hostId: string, enabled: boolean) {
|
||||||
|
await PerDomainSettings.setHostDisabled(domain, hostId, !enabled);
|
||||||
|
perDomain = await PerDomainSettings.get(domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onAllToggle(enabled: boolean) {
|
||||||
|
await PerDomainSettings.setAllDisabled(domain, !enabled);
|
||||||
|
perDomain = await PerDomainSettings.get(domain);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 px-2 pb-2 h-full overflow-hidden">
|
||||||
|
<div class="flex-1 min-h-0 overflow-y-auto -mx-1 divide-y divide-gray-700/40">
|
||||||
|
{#each orderedHosts as host (host.id)}
|
||||||
|
{@const isInDisabledList = perDomain.disabledHostIds.includes(host.id)}
|
||||||
|
<HostRow
|
||||||
|
{host}
|
||||||
|
enabled={!isInDisabledList && !perDomain.allDisabled}
|
||||||
|
badge={host.id === matchedHostId ? 'Match' : undefined}
|
||||||
|
>
|
||||||
|
{#snippet actions()}
|
||||||
|
{#key perDomain.allDisabled + perDomain.disabledHostIds.join(',')}
|
||||||
|
<Toggle
|
||||||
|
disabled={perDomain.allDisabled}
|
||||||
|
title={perDomain.allDisabled ? 'All hosts are disabled for this site' : undefined}
|
||||||
|
bind:checked={
|
||||||
|
() => !isInDisabledList,
|
||||||
|
(v) => {
|
||||||
|
onHostToggle(host.id, v);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
{/snippet}
|
||||||
|
</HostRow>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between gap-2 px-3 py-2 mt-1 bg-gray-800/40 border border-gray-700/60 rounded-md shrink-0"
|
||||||
|
>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="text-sm font-semibold text-gray-100">Disable all on this site</p>
|
||||||
|
<p class="text-xs text-gray-400 truncate">Turn off all hosts for {domain}</p>
|
||||||
|
</div>
|
||||||
|
{#key perDomain.allDisabled}
|
||||||
|
<Toggle
|
||||||
|
bind:checked={
|
||||||
|
() => !perDomain.allDisabled,
|
||||||
|
(v) => {
|
||||||
|
onAllToggle(v);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
import Divider from '@/entrypoints/popup/components/Divider.svelte';
|
||||||
|
import Header from '@/entrypoints/popup/pages/site-config/Header.svelte';
|
||||||
|
import PerSiteHostsList from '@/entrypoints/popup/pages/site-config/PerSiteHostsList.svelte';
|
||||||
|
import { listenMessages, MessageType, sendMessageToActiveTab } from '@/lib/communication';
|
||||||
|
import { hosts, type Host } from '@/lib/host';
|
||||||
|
|
||||||
|
/* types */
|
||||||
|
interface Props {
|
||||||
|
onBackClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let { onBackClick }: Props = $props();
|
||||||
|
|
||||||
|
let currentMatch = $state<{ host: Host; url: string; domain: string } | null>(null);
|
||||||
|
let currentDomain = $state<string | null>(null);
|
||||||
|
|
||||||
|
/* lifecycle */
|
||||||
|
const cancel = listenMessages((type, data) => {
|
||||||
|
if (type !== MessageType.NotifyActiveMatch) return;
|
||||||
|
const match = {
|
||||||
|
host: hosts.find((host) => host.id === data.id)!,
|
||||||
|
url: data.url,
|
||||||
|
domain: data.domain
|
||||||
|
};
|
||||||
|
currentMatch = match;
|
||||||
|
currentDomain = match.domain;
|
||||||
|
});
|
||||||
|
sendMessageToActiveTab(MessageType.RequestActiveMatch, undefined);
|
||||||
|
|
||||||
|
browser.tabs
|
||||||
|
.query({ active: true, currentWindow: true })
|
||||||
|
.then((tabs) => {
|
||||||
|
if (currentDomain) return;
|
||||||
|
const url = tabs[0]?.url;
|
||||||
|
if (!url) return;
|
||||||
|
try {
|
||||||
|
currentDomain = new URL(url).hostname;
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
|
onDestroy(cancel);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<Header domain={currentDomain ?? ''} {onBackClick} />
|
||||||
|
<Divider />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col h-full overflow-hidden">
|
||||||
|
{#if currentDomain}
|
||||||
|
<PerSiteHostsList domain={currentDomain} matchedHostId={currentMatch?.host.id ?? null} />
|
||||||
|
{:else}
|
||||||
|
<div class="flex items-center justify-center h-full text-sm text-gray-400 p-4 text-center">
|
||||||
|
No active website detected.
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
+17
-5
@@ -17,7 +17,7 @@ import Vidmoly from './vidmoly';
|
|||||||
import Vidoza from './vidoza';
|
import Vidoza from './vidoza';
|
||||||
import Voe from './voe';
|
import Voe from './voe';
|
||||||
import Vupload from './vupload';
|
import Vupload from './vupload';
|
||||||
import { HostSettings } from '@/lib/settings';
|
import { HostSettings, PerDomainSettings } from '@/lib/settings';
|
||||||
|
|
||||||
export enum HostMatchType {
|
export enum HostMatchType {
|
||||||
NATIVE = 'native',
|
NATIVE = 'native',
|
||||||
@@ -79,13 +79,25 @@ export function filterHosts(list: Host[], query: string): Host[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getHost(domain: string): Promise<Host | null> {
|
export async function getHost(domain: string): Promise<Host | null> {
|
||||||
if (await HostSettings.getAllHostsDisabled()) return null;
|
const [allHostsDisabled, disabledHosts, perDomainSetting] = await Promise.all([
|
||||||
|
HostSettings.getAllHostsDisabled(),
|
||||||
|
HostSettings.getDisabledHosts(),
|
||||||
|
PerDomainSettings.get(domain)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// extension is disabled via popup
|
||||||
|
if (allHostsDisabled) return null;
|
||||||
|
// domain is disabled via popup
|
||||||
|
else if (perDomainSetting.allDisabled) return null;
|
||||||
|
|
||||||
const disabledIds = await HostSettings.getDisabledHosts();
|
|
||||||
for (const host of hosts) {
|
for (const host of hosts) {
|
||||||
if (host.domains.includes(domain)) {
|
if (host.domains.includes(domain)) {
|
||||||
if (!disabledIds.includes(host.id)) return host;
|
// host is generally disabled via popup
|
||||||
else return null;
|
if (disabledHosts.includes(host.id)) return null;
|
||||||
|
// host is disabled for this domain via popup
|
||||||
|
else if (perDomainSetting.disabledHostIds.includes(host.id)) return null;
|
||||||
|
|
||||||
|
return host;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,3 +64,48 @@ export class UrlReferer {
|
|||||||
|
|
||||||
static get = (hostname: string) => this.temporaryUrlReferer.get().then((val) => val[hostname] ?? null);
|
static get = (hostname: string) => this.temporaryUrlReferer.get().then((val) => val[hostname] ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PerDomainConfig {
|
||||||
|
allDisabled: boolean;
|
||||||
|
disabledHostIds: HostId[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PerDomainSettings {
|
||||||
|
private static storage = new Setting<Record<string, PerDomainConfig>>('local:perDomainSettings', {});
|
||||||
|
|
||||||
|
private static ensureDomain = (val: Record<string, PerDomainConfig>, domain: string) => {
|
||||||
|
if (!val[domain]) val[domain] = { allDisabled: false, disabledHostIds: [] };
|
||||||
|
return val[domain];
|
||||||
|
};
|
||||||
|
|
||||||
|
static get = (domain: string): Promise<PerDomainConfig> =>
|
||||||
|
this.storage.get().then((val) => val[domain] ?? { allDisabled: false, disabledHostIds: [] });
|
||||||
|
|
||||||
|
static getMap = () => this.storage.get();
|
||||||
|
|
||||||
|
static setAllDisabled = (domain: string, disabled: boolean) =>
|
||||||
|
this.storage.update((val) => {
|
||||||
|
this.ensureDomain(val, domain).allDisabled = disabled;
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
|
||||||
|
static setHostDisabled = (domain: string, hostId: HostId, disabled: boolean) =>
|
||||||
|
this.storage.update((val) => {
|
||||||
|
const entry = this.ensureDomain(val, domain);
|
||||||
|
if (!disabled) entry.disabledHostIds = entry.disabledHostIds.filter((id) => id !== hostId);
|
||||||
|
else if (!entry.disabledHostIds.includes(hostId)) entry.disabledHostIds.push(hostId);
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
|
||||||
|
static add = (domain: string) =>
|
||||||
|
this.storage.update((val) => {
|
||||||
|
this.ensureDomain(val, domain);
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
|
||||||
|
static clear = (domain: string) =>
|
||||||
|
this.storage.update((val) => {
|
||||||
|
delete val[domain];
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user