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'); let activePage = $state<Page>('main');
</script> </script>
<div class="flex w-75 overflow-hidden" class:w-screen={$isMobile}> <div class="flex w-78 overflow-hidden" class:w-screen={$isMobile}>
<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">
@@ -26,7 +26,7 @@
{:else if activePage === 'settings'} {:else if activePage === 'settings'}
<div <div
transition:fly={{ x: 300, duration: 150 }} 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} class:h-screen={$isMobile}
> >
<Settings onSettingsCloseRequest={() => (activePage = 'main')} /> <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> </script>
<div class="flex items-center gap-2"> <div class="flex items-center justify-between">
<div class="relative mr-3"> <div class="flex flex-col gap-0.5">
<span>Communication enabled</span> <p class="text-sm font-semibold text-gray-100">Communication enabled</p>
<a <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" 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> </div>
{#key enabled} {#key enabled}
<Toggle <Toggle
@@ -1,4 +1,7 @@
<script lang="ts"> <script lang="ts">
import { ArrowLeft } from '@steeze-ui/heroicons';
import { Icon } from '@steeze-ui/svelte-icon';
/* types */ /* types */
interface Props { interface Props {
onBackClick: () => void; onBackClick: () => void;
@@ -8,9 +11,19 @@
let { onBackClick }: Props = $props(); let { onBackClick }: Props = $props();
</script> </script>
<div class="flex justify-between items-center p-2"> <div class="flex justify-between items-center px-3 py-2.5">
<div class="flex items-baseline gap-2"> <div class="flex items-center gap-2">
<button class="cursor-pointer" onclick={() => onBackClick()}>←</button> <button
<h1>Settings</h1> 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>
</div> </div>
@@ -1,41 +1,47 @@
<script lang="ts"> <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 Toggle from '@/entrypoints/popup/components/Toggle.svelte';
import { hosts } from '@/lib/host'; import { filterHosts, hosts } from '@/lib/host';
import { HostSettings } from '@/lib/settings'; import { HostSettings } from '@/lib/settings';
/* types */
interface Props {
searchQuery: string;
}
/* states */ /* states */
let { searchQuery = $bindable('') }: Props = $props();
let disabledHostIds = $state<Array<string>>([]); let disabledHostIds = $state<Array<string>>([]);
HostSettings.getDisabledHosts().then((val) => (disabledHostIds = val)); HostSettings.getDisabledHosts().then((val) => (disabledHostIds = val));
let filtered = $derived(filterHosts(hosts, searchQuery));
</script> </script>
<div class="grid grid-cols-[35%_43%_22%] gap-y-0.75"> <div class="flex flex-col gap-2 max-h-full">
<p class="font-bold">Host</p> <SearchInput bind:value={searchQuery} placeholder="Filter hosts…" />
<p class="font-bold">Domains</p> {#if filtered.length === 0}
<p class="font-bold">Enabled</p> <p class="text-xs text-gray-400 text-center py-2">No hosts match "{searchQuery}"</p>
{#each hosts as host (host.id)} {:else}
{@const domainList = host.domains.join(', ')} <div class="flex flex-col divide-y divide-gray-700/40 -mx-1 max-h-full overflow-y-scroll">
<p>{host.name}</p> {#each filtered as host (host.id)}
<div> <HostRow {host} enabled={!disabledHostIds.includes(host.id)}>
<label for={host.id}> {#snippet actions()}
<input id={host.id} type="checkbox" class="peer hidden" checked /> {#key disabledHostIds}
<p <Toggle
title={domainList} bind:checked={
class="cursor-pointer overflow-hidden peer-checked:text-ellipsis peer-checked:text-nowrap" () => !disabledHostIds.includes(host.id),
> (v) =>
{domainList} v
</p> ? HostSettings.removeDisabledHost(host.id)
</label> : HostSettings.addDisabledHost(host.id)
}
size="sm"
/>
{/key}
{/snippet}
</HostRow>
{/each}
</div> </div>
<div class="mt-[.2rem]"> {/if}
{#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}
</div> </div>
@@ -1,5 +1,7 @@
<script lang="ts"> <script lang="ts">
import { CommandLine, 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 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';
@@ -12,22 +14,28 @@
/* states */ /* states */
let { onSettingsCloseRequest }: Props = $props(); let { onSettingsCloseRequest }: Props = $props();
let searchQuery = $state('');
</script> </script>
<div class="w-full"> <div class="w-full">
<Header onBackClick={onSettingsCloseRequest} /> <Header onBackClick={onSettingsCloseRequest} />
<Divider /> <Divider />
</div> </div>
<div class="flex flex-col gap-y-1 pt-1 h-full mx-2 my-1 overflow-y-scroll"> <div class="flex flex-col gap-2 p-2 h-full overflow-y-scroll">
<details class="details" open> <div class="max-h-full">
<summary>Hosts</summary> <Section
<HostsTable /> title="Hosts"
</details> description="Enable or disable support for specific streaming providers"
icon={ServerStack}
defaultOpen
>
<HostsTable bind:searchQuery />
</Section>
</div>
{#if !$isMobile} {#if !$isMobile}
<details class="details"> <Section title="ff2mpv" description="Play streams directly in mpv via native messaging" icon={CommandLine}>
<summary>ff2mpv</summary>
<Ff2mpv /> <Ff2mpv />
</details> </Section>
{/if} {/if}
</div> </div>
@@ -35,26 +43,4 @@
* { * {
user-select: none; 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> </style>
+9
View File
@@ -69,6 +69,15 @@ export function getHostFromId(id: HostId): Host | null {
return hosts.find((host) => host.id === id) ?? 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> { export async function getHost(domain: string): Promise<Host | null> {
if (await HostSettings.getAllHostsDisabled()) return null; if (await HostSettings.getAllHostsDisabled()) return null;