mirror of
https://github.com/bytedream/stream-bypass.git
synced 2026-08-04 21:10:40 +02:00
update settings layout
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user