update settings layout

This commit is contained in:
2026-07-02 11:11:56 +02:00
parent c08e071c88
commit 9857ba6416
9 changed files with 237 additions and 71 deletions
+2 -2
View File
@@ -17,7 +17,7 @@
let activePage = $state<Page>('main');
</script>
<div class="flex w-75 overflow-hidden" class:w-screen={$isMobile}>
<div class="flex w-78 overflow-hidden" class:w-screen={$isMobile}>
<Toast />
{#if activePage === 'main'}
<div transition:fly={{ x: -300, duration: 150 }} class="min-w-full w-full h-75 flex-1 flex flex-col">
@@ -26,7 +26,7 @@
{:else if activePage === 'settings'}
<div
transition:fly={{ x: 300, duration: 150 }}
class="min-w-full w-full h-75 flex-1 flex flex-col"
class="min-w-full w-full h-120 flex-1 flex flex-col"
class:h-screen={$isMobile}
>
<Settings onSettingsCloseRequest={() => (activePage = 'main')} />
@@ -0,0 +1,54 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import type { Host } from '@/lib/host';
/* types */
interface Props {
host: Host;
enabled?: boolean;
onclick?: () => void;
actions: Snippet;
}
/* states */
let { host, enabled, 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' : ''}`
);
/* functions */
function handleKeydown(event: KeyboardEvent) {
if (!onclick) return;
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
onclick();
}
}
</script>
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<div
class={rowClass}
role={onclick ? 'button' : undefined}
tabindex={onclick ? 0 : undefined}
{onclick}
onkeydown={handleKeydown}
>
<span
class="w-2 h-2 rounded-full shrink-0"
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>
<p class="text-xs text-gray-400 truncate" title={host.domains.join(', ')}>
{host.domains.join(', ')}
</p>
</div>
<div class="shrink-0">
{@render actions()}
</div>
</div>
@@ -0,0 +1,36 @@
<script lang="ts">
import { MagnifyingGlass, XMark } from '@steeze-ui/heroicons';
import { Icon } from '@steeze-ui/svelte-icon';
/* types */
interface Props {
value: string;
placeholder?: string;
}
/* states */
let { value = $bindable(''), placeholder = 'Search…' }: Props = $props();
</script>
<div
class="flex items-center gap-2 px-2.5 py-1.5 bg-gray-900/60 border border-gray-700 rounded-md focus-within:border-linux-mint-green transition-colors"
>
<Icon src={MagnifyingGlass} size="0.95rem" class="text-gray-400 shrink-0" />
<input
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" />
</button>
{/if}
</div>
@@ -0,0 +1,59 @@
<script lang="ts">
import { ChevronDown } from '@steeze-ui/heroicons';
import { Icon, type IconSource } from '@steeze-ui/svelte-icon';
import { untrack, type Snippet } from 'svelte';
import { slide } from 'svelte/transition';
/* types */
interface Props {
title: string;
description?: string;
icon?: IconSource;
defaultOpen?: boolean;
children: Snippet;
}
/* states */
let { title, description, icon, defaultOpen = false, children }: Props = $props();
let open = $state(untrack(() => defaultOpen));
</script>
<div class="bg-gray-800/40 border border-gray-700/60 rounded-lg max-h-full flex flex-col">
<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">
{#if icon}
<Icon src={icon} size="1.1rem" class="text-gray-400 shrink-0" />
{/if}
<div class="min-w-0 text-left">
<h3 class="text-sm font-semibold text-gray-100 leading-tight">{title}</h3>
{#if description}
<p class="text-xs text-gray-400 mt-0.5 leading-tight">{description}</p>
{/if}
</div>
</div>
<span class="text-gray-400 shrink-0 inline-flex chevron" class:rotated={open}>
<Icon src={ChevronDown} size="1rem" />
</span>
</button>
{#if open}
<div class="px-3 py-3 flex overflow-hidden" transition:slide={{ duration: 150 }}>
<div class="w-full">
{@render children()}
</div>
</div>
{/if}
</div>
<style>
.chevron {
transition: transform 150ms ease;
}
.rotated {
transform: rotate(180deg);
}
</style>
@@ -16,14 +16,17 @@
}
</script>
<div class="flex items-center gap-2">
<div class="relative mr-3">
<span>Communication enabled</span>
<div class="flex items-center justify-between">
<div class="flex flex-col gap-0.5">
<p class="text-sm font-semibold text-gray-100">Communication enabled</p>
<a
class="absolute -top-1 -right-4 text-sm"
class="inline-flex items-center gap-1 text-xs text-gray-400 hover:text-gray-200 transition-colors"
href="https://github.com/bytedream/stream-bypass/tree/main?tab=readme-ov-file#ff2mpv-use-mpv-to-directly-play-streams"
target="_blank"><Icon src={InformationCircle} size="1rem" /></a
target="_blank"
>
<Icon src={InformationCircle} size="0.85rem" />
<span>Learn more</span>
</a>
</div>
{#key enabled}
<Toggle
@@ -1,4 +1,7 @@
<script lang="ts">
import { ArrowLeft } from '@steeze-ui/heroicons';
import { Icon } from '@steeze-ui/svelte-icon';
/* types */
interface Props {
onBackClick: () => void;
@@ -8,9 +11,19 @@
let { onBackClick }: Props = $props();
</script>
<div class="flex justify-between items-center p-2">
<div class="flex items-baseline gap-2">
<button class="cursor-pointer" onclick={() => onBackClick()}>←</button>
<h1>Settings</h1>
<div class="flex justify-between items-center px-3 py-2.5">
<div class="flex items-center gap-2">
<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" />
</button>
<div>
<h1 class="text-lg font-semibold text-gray-100 leading-tight">Settings</h1>
<p class="text-xs text-gray-400 leading-tight">Manage hosts and integrations</p>
</div>
</div>
</div>
@@ -1,41 +1,47 @@
<script lang="ts">
import HostRow from '@/entrypoints/popup/components/HostRow.svelte';
import SearchInput from '@/entrypoints/popup/components/SearchInput.svelte';
import Toggle from '@/entrypoints/popup/components/Toggle.svelte';
import { hosts } from '@/lib/host';
import { filterHosts, hosts } from '@/lib/host';
import { HostSettings } from '@/lib/settings';
/* types */
interface Props {
searchQuery: string;
}
/* states */
let { searchQuery = $bindable('') }: Props = $props();
let disabledHostIds = $state<Array<string>>([]);
HostSettings.getDisabledHosts().then((val) => (disabledHostIds = val));
let filtered = $derived(filterHosts(hosts, searchQuery));
</script>
<div class="grid grid-cols-[35%_43%_22%] gap-y-0.75">
<p class="font-bold">Host</p>
<p class="font-bold">Domains</p>
<p class="font-bold">Enabled</p>
{#each hosts as host (host.id)}
{@const domainList = host.domains.join(', ')}
<p>{host.name}</p>
<div>
<label for={host.id}>
<input id={host.id} type="checkbox" class="peer hidden" checked />
<p
title={domainList}
class="cursor-pointer overflow-hidden peer-checked:text-ellipsis peer-checked:text-nowrap"
>
{domainList}
</p>
</label>
<div class="flex flex-col gap-2 max-h-full">
<SearchInput bind:value={searchQuery} placeholder="Filter hosts…" />
{#if filtered.length === 0}
<p class="text-xs text-gray-400 text-center py-2">No hosts match "{searchQuery}"</p>
{:else}
<div class="flex flex-col divide-y divide-gray-700/40 -mx-1 max-h-full overflow-y-scroll">
{#each filtered as host (host.id)}
<HostRow {host} enabled={!disabledHostIds.includes(host.id)}>
{#snippet actions()}
{#key disabledHostIds}
<Toggle
bind:checked={
() => !disabledHostIds.includes(host.id),
(v) =>
v
? HostSettings.removeDisabledHost(host.id)
: HostSettings.addDisabledHost(host.id)
}
size="sm"
/>
{/key}
{/snippet}
</HostRow>
{/each}
</div>
<div class="mt-[.2rem]">
{#key disabledHostIds}
<Toggle
bind:checked={
() => !disabledHostIds.includes(host.id),
(v) => (v ? HostSettings.removeDisabledHost(host.id) : HostSettings.addDisabledHost(host.id))
}
size="sm"
></Toggle>
{/key}
</div>
{/each}
{/if}
</div>
@@ -1,5 +1,7 @@
<script lang="ts">
import { CommandLine, 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';
@@ -12,22 +14,28 @@
/* states */
let { onSettingsCloseRequest }: Props = $props();
let searchQuery = $state('');
</script>
<div class="w-full">
<Header onBackClick={onSettingsCloseRequest} />
<Divider />
</div>
<div class="flex flex-col gap-y-1 pt-1 h-full mx-2 my-1 overflow-y-scroll">
<details class="details" open>
<summary>Hosts</summary>
<HostsTable />
</details>
<div class="flex flex-col gap-2 p-2 h-full overflow-y-scroll">
<div class="max-h-full">
<Section
title="Hosts"
description="Enable or disable support for specific streaming providers"
icon={ServerStack}
defaultOpen
>
<HostsTable bind:searchQuery />
</Section>
</div>
{#if !$isMobile}
<details class="details">
<summary>ff2mpv</summary>
<Section title="ff2mpv" description="Play streams directly in mpv via native messaging" icon={CommandLine}>
<Ff2mpv />
</details>
</Section>
{/if}
</div>
@@ -35,26 +43,4 @@
* {
user-select: none;
}
.details {
/* using normal css instead of tailwind in the following blocks.
for some reason tailwind fails to resolve many references */
&::before,
&::after {
content: '';
display: block;
width: 100%;
border-top: 1px solid var(--color-gray-600);
margin: 0.25rem 0;
}
& > summary {
cursor: pointer;
}
&[open] > summary {
margin-bottom: 0.5rem;
}
}
</style>
+9
View File
@@ -69,6 +69,15 @@ export function getHostFromId(id: HostId): Host | null {
return hosts.find((host) => host.id === id) ?? null;
}
export function filterHosts(list: Host[], query: string): Host[] {
const q = query.trim().toLowerCase();
if (!q) return list;
return list.filter((host) => {
if (host.name.toLowerCase().includes(q)) return true;
return host.domains.some((domain) => domain.toLowerCase().includes(q));
});
}
export async function getHost(domain: string): Promise<Host | null> {
if (await HostSettings.getAllHostsDisabled()) return null;