add per domain settings (#30)

This commit is contained in:
2026-07-07 12:26:54 +02:00
parent 9857ba6416
commit eef560f224
20 changed files with 630 additions and 59 deletions
+5
View File
@@ -154,6 +154,11 @@ When using firefox, use the following:
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](https://github.com/woodruffw/ff2mpv) allows you to play streams directly in [mpv](https://mpv.io/) instead of the browser.
+14 -2
View File
@@ -5,13 +5,14 @@
import Toast from '@/entrypoints/popup/components/Toast.svelte';
import Main from '@/entrypoints/popup/pages/main/Main.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';
/* state init */
browser.runtime.getPlatformInfo().then((info) => ($isMobile = info.os === 'android'));
/* types */
type Page = 'main' | 'settings';
type Page = 'main' | 'settings' | 'site-config';
/* states */
let activePage = $state<Page>('main');
@@ -21,7 +22,10 @@
<Toast />
{#if activePage === 'main'}
<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>
{:else if activePage === 'settings'}
<div
@@ -31,5 +35,13 @@
>
<Settings onSettingsCloseRequest={() => (activePage = 'main')} />
</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}
</div>
@@ -6,12 +6,13 @@
interface Props {
host: Host;
enabled?: boolean;
badge?: string;
onclick?: () => void;
actions: Snippet;
}
/* states */
let { host, enabled, onclick, actions }: Props = $props();
let { host, enabled, badge, onclick, actions }: Props = $props();
let rowClass = $derived(
`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-gray-500={enabled === false}
class:bg-transparent={enabled === undefined}
aria-hidden="true"
></span>
<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(', ')}>
{host.domains.join(', ')}
</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"
bind:value
{placeholder}
aria-label="Filter hosts"
class="flex-1 bg-transparent text-sm text-gray-100 placeholder:text-gray-500 outline-none min-w-0"
/>
{#if value}
<button
type="button"
class="text-gray-400 hover:text-gray-100 cursor-pointer shrink-0"
aria-label="Clear search"
onclick={() => (value = '')}
>
<Icon src={XMark} size="0.95rem" />
@@ -22,7 +22,6 @@
<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"
aria-expanded={open}
onclick={() => (open = !open)}
>
<div class="flex items-center gap-2 min-w-0">
@@ -5,7 +5,7 @@
{#if $toast}
<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 }}
>
{$toast.message}
+18 -3
View File
@@ -4,14 +4,17 @@
checked: boolean;
onChecked?: (checked: boolean) => void | boolean | Promise<void> | Promise<boolean>;
size?: 'sm' | 'md';
disabled?: boolean;
title?: string;
}
/* 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));
/* callbacks */
async function onInputChange() {
if (disabled) return;
internalChecked = !internalChecked;
let approved = false;
@@ -36,9 +39,21 @@
}
</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">
<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
class="block rounded-full box bg-red-700 peer-checked:bg-linux-mint-green"
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>
+57 -17
View File
@@ -3,11 +3,11 @@
import { onDestroy } from '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 CurrentSite from '@/entrypoints/popup/pages/main/CurrentSite.svelte';
import Header from '@/entrypoints/popup/pages/main/Header.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 { hosts, type Host } from '@/lib/host';
import { HostSettings } from '@/lib/settings';
@@ -15,11 +15,13 @@
/* types */
interface Props {
onSettingsOpenRequest: () => void;
onSiteConfigOpenRequest: () => void;
}
/* states */
let { onSettingsOpenRequest }: Props = $props();
let { onSettingsOpenRequest, onSiteConfigOpenRequest }: Props = $props();
let currentMatch = $state<{ host: Host; url: string; domain: string } | null>(null);
let currentDomain = $state<string | null>(null);
let allHostsDisabled = $state(false);
HostSettings.getAllHostsDisabled().then((val) => (allHostsDisabled = val));
@@ -27,35 +29,73 @@
/* lifecycle */
const cancel = listenMessages((type, data) => {
if (type !== MessageType.NotifyActiveMatch) return;
currentMatch = {
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">
<div class="w-full shrink-0">
<Header bind:allHostsDisabled onSettingsClick={onSettingsOpenRequest} />
<Divider />
</div>
<div class="px-2 h-full">
{#if allHostsDisabled}
<AllDisabled />
{:else if !currentMatch}
<NoMatch />
{:else}
<div class="flex flex-col justify-between h-full pb-2">
<Match host={currentMatch.host} domain={currentMatch.domain} />
<div class="divider border-dashed"></div>
<div class="mt-2">
<div class="grid grid-cols-1 grid-rows-[min-content_auto_min-content] gap-2 h-full">
<div class="px-2 pt-1.5 shrink-0">
{#if currentDomain}
<CurrentSite domain={currentDomain} onConfigureClick={onSiteConfigOpenRequest} />
{/if}
</div>
<div>
{#if allHostsDisabled}
<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} />
</div>
</div>
{/if}
{:else}
<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>
<style>
@@ -4,17 +4,13 @@
/* types */
interface Props {
host: Host;
domain: string;
}
/* states */
const { host, domain }: Props = $props();
const { host }: Props = $props();
</script>
<div class="flex flex-col items-center justify-center w-full h-full">
<p class="text-lg">Match found:</p>
<div class="*:select-text">
<span class="underline text-green-400 text-2xl font-bold">{host.name}</span>
<span class="text-xs text-slate-300">({domain})</span>
</div>
<div class="flex flex-col items-center justify-center">
<p class="text-md text-gray-400">Match found:</p>
<span class="underline text-green-400 text-xl font-bold select-text">{host.name}</span>
</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
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"
aria-label="Back"
onclick={() => onBackClick()}
>
<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">
import { CommandLine, ServerStack } from '@steeze-ui/heroicons';
import { CommandLine, MapPin, ServerStack } from '@steeze-ui/heroicons';
import Divider from '@/entrypoints/popup/components/Divider.svelte';
import Section from '@/entrypoints/popup/components/Section.svelte';
import Ff2mpv from '@/entrypoints/popup/pages/settings/Ff2mpv.svelte';
import Header from '@/entrypoints/popup/pages/settings/Header.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';
/* types */
@@ -32,6 +33,9 @@
<HostsTable bind:searchQuery />
</Section>
</div>
<Section title="Per-site disables" description="Disable hosts for specific websites" icon={MapPin}>
<PerSiteDisables />
</Section>
{#if !$isMobile}
<Section title="ff2mpv" description="Play streams directly in mpv via native messaging" icon={CommandLine}>
<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
View File
@@ -17,7 +17,7 @@ import Vidmoly from './vidmoly';
import Vidoza from './vidoza';
import Voe from './voe';
import Vupload from './vupload';
import { HostSettings } from '@/lib/settings';
import { HostSettings, PerDomainSettings } from '@/lib/settings';
export enum HostMatchType {
NATIVE = 'native',
@@ -79,13 +79,25 @@ export function filterHosts(list: Host[], query: string): Host[] {
}
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) {
if (host.domains.includes(domain)) {
if (!disabledIds.includes(host.id)) return host;
else return null;
// host is generally disabled via popup
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;
}
}
+45
View File
@@ -64,3 +64,48 @@ export class UrlReferer {
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;
});
}