appview-less bluesky client
1<script lang="ts">
2 import FeedItem from './FeedItem.svelte';
3 import VirtualList from '@tutorlatin/svelte-tiny-virtual-list';
4 import type { FeedGenerator } from '$lib/at/feeds';
5 import { fetchFeedGenerator, parseFeedUri } from '$lib/at/feeds';
6 import type { SavedFeed } from '$lib/settings';
7 import { viewClient } from '$lib/state.svelte';
8
9 interface Props {
10 feeds: SavedFeed[];
11 onAddFeed: (feed: FeedGenerator) => void;
12 onRemoveFeed: (uri: string) => void;
13 onTogglePin: (uri: string) => void;
14 }
15
16 let { feeds, onAddFeed, onRemoveFeed, onTogglePin }: Props = $props();
17
18 let newFeedInput = $state('');
19
20 const handleAddFeed = async () => {
21 const uri = newFeedInput.trim();
22 if (!uri) return;
23 if (!parseFeedUri(uri)) return;
24 if (feeds.some((f) => f.feed.uri === uri)) return;
25 const feed = await fetchFeedGenerator(viewClient, uri);
26 if (!feed) return;
27 onAddFeed(feed);
28 newFeedInput = '';
29 };
30
31 const sortedFeeds = $derived([...feeds].sort((a, b) => (b.pinned ? 1 : 0) - (a.pinned ? 1 : 0)));
32 const isValidUri = $derived(parseFeedUri(newFeedInput.trim()) !== null);
33</script>
34
35<div class="space-y-4 p-4">
36 <div>
37 <h3 class="settings-header">saved feeds</h3>
38 <div class="settings-box space-y-2">
39 <div class="flex gap-2">
40 <input
41 type="text"
42 bind:value={newFeedInput}
43 placeholder="https://bsky.app/profile/bsky.app/feed/whats-hot"
44 class="single-line-input flex-1"
45 />
46 <button disabled={!isValidUri} onclick={handleAddFeed} class="action-button">add</button>
47 </div>
48 {#if sortedFeeds.length > 0}
49 <div class="h-fit">
50 <VirtualList
51 height={Math.min(sortedFeeds.length, 7) * 44}
52 itemCount={sortedFeeds.length}
53 itemSize={44}
54 >
55 {#snippet item({ index, style }: { index: number; style: string })}
56 <FeedItem
57 {style}
58 data={sortedFeeds[index]}
59 onRemove={() => onRemoveFeed(sortedFeeds[index].feed.uri)}
60 onTogglePin={() => onTogglePin(sortedFeeds[index].feed.uri)}
61 />
62 {/snippet}
63 </VirtualList>
64 </div>
65 {:else}
66 <p class="py-2 text-center text-sm opacity-50">no saved feeds</p>
67 {/if}
68 </div>
69 </div>
70</div>