johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
<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>