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.
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user