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');
|
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 />
|
|
||||||
<p
|
|
||||||
title={domainList}
|
|
||||||
class="cursor-pointer overflow-hidden peer-checked:text-ellipsis peer-checked:text-nowrap"
|
|
||||||
>
|
|
||||||
{domainList}
|
|
||||||
</p>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="mt-[.2rem]">
|
|
||||||
{#key disabledHostIds}
|
{#key disabledHostIds}
|
||||||
<Toggle
|
<Toggle
|
||||||
bind:checked={
|
bind:checked={
|
||||||
() => !disabledHostIds.includes(host.id),
|
() => !disabledHostIds.includes(host.id),
|
||||||
(v) => (v ? HostSettings.removeDisabledHost(host.id) : HostSettings.addDisabledHost(host.id))
|
(v) =>
|
||||||
|
v
|
||||||
|
? HostSettings.removeDisabledHost(host.id)
|
||||||
|
: HostSettings.addDisabledHost(host.id)
|
||||||
}
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
></Toggle>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
{/snippet}
|
||||||
|
</HostRow>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
</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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user