~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
127 lines · 3.8 KB Raw History
<script lang="ts" module>
	/** The id Appearance opens this dialog by. */
	export const THEME_BROWSER = 'theme-browser';
</script>

<script lang="ts">
	import type * as defs from '#lib/lexicons/dev/knotless/defs.ts';
	import type * as listThemes from '#lib/lexicons/dev/knotless/listThemes.ts';
	import { ask } from '#lib/data.ts';
	import { scrollFades } from '#lib/rows.ts';
	import Empty from '#lib/molecules/Empty.svelte';
	import Icon from '#lib/atoms/Icon.svelte';
	import ThemeCard from '#lib/molecules/ThemeCard.svelte';
	import Dialog from '#lib/molecules/Dialog.svelte';
	/**
	 * Every theme people made, to add to the viewer's own: the latest
	 * first, a page at a time, or those whose name starts as searched.
	 * Asked for once it opens. `has` says whether the viewer made one
	 * (`mine`, with nothing to add) or added it (`added`, marked so); `toggle`
	 * adds one, or takes it out,
	 * leaving the dialog open to add more. Choosing is Appearance's.
	 */
	let {
		has,
		toggle
	}: {
		has: (t: defs.ThemeView) => 'mine' | 'added' | null;
		toggle: (t: defs.ThemeView) => Promise<void>;
	} = $props();
	/** The theme being added or taken out, by URI. */
	let changing = $state<string | null>(null);
	async function change(t: defs.ThemeView) {
		changing = t.uri;
		try {
			await toggle(t);
		} finally {
			changing = null;
		}
	}
	const PAGE = 48;
	let themes = $state<defs.ThemeView[]>([]);
	let cursor = $state<string | undefined>();
	let q = $state('');
	let asked = $state(false);
	let busy = $state(false);
	let problem = $state('');
	let timer: ReturnType<typeof setTimeout> | undefined;
	async function load(more = false) {
		busy = true;
		problem = '';
		try {
			const asking = q.trim()
				? { q: q.trim(), limit: PAGE }
				: { limit: PAGE, cursor: more ? cursor : undefined };
			const found = await ask<typeof listThemes.main>('dev.knotless.listThemes', asking);
			themes = more ? [...themes, ...found.themes] : found.themes;
			cursor = found.cursor;
			asked = true;
		} catch (err) {
			problem = (err as Error).message;
		} finally {
			busy = false;
		}
	}
	function opened(e: ToggleEvent) {
		if (e.newState === 'open' && !asked) load();
	}
	function search() {
		clearTimeout(timer);
		timer = setTimeout(() => load(), 250);
	}
</script>

<Dialog id={THEME_BROWSER} class="theme-browser" title="Themes made on knotless" ontoggle={opened}>
	<p class="hint">Add the ones you like. They show under Your themes, to choose from.</p>
	<input
		type="search"
		aria-label="Find a theme by name"
		placeholder="Find a theme by name"
		bind:value={q}
		oninput={search}
	/>
	<div class="theme-browser-list" aria-busy={busy} {@attach scrollFades}>
		{#if themes.length}
			<div class="themes">
				{#each themes as t (t.uri)}
					<ThemeCard theme={t}>
						{#snippet action()}
							{#if has(t) !== 'mine'}
								{@const said = `${has(t) === 'added' ? 'Remove' : 'Add'} ${t.name}`}
								<button
									class="btn theme-add"
									type="button"
									aria-label={said}
									title={said}
									aria-pressed={has(t) === 'added'}
									disabled={changing === t.uri}
									aria-busy={changing === t.uri}
									onclick={() => change(t)}
									><Icon name={has(t) === 'added' ? 'check' : 'plus'} /></button
								>
							{/if}
						{/snippet}
					</ThemeCard>
				{/each}
			</div>
			{#if cursor && !q.trim()}<p class="theme-browser-more">
					<button
						class="btn"
						type="button"
						disabled={busy}
						aria-busy={busy}
						onclick={() => load(true)}>{busy ? 'Loading…' : 'Show more'}</button
					>
				</p>{/if}
		{:else if asked && q.trim()}
			<Empty title="No themes match"
				><button class="link" type="button" onclick={() => ((q = ''), load())}
					>Clear the search</button
				></Empty
			>
		{:else if asked}
			<Empty title="No themes made yet" />
		{/if}
		{#if problem}<p class="problem" role="alert">{problem}</p>{/if}
	</div>
</Dialog>