a simple web player for subsonic
tinysub.devins.page
subsonic
navidrome
javascript
1<script lang="ts">
2 import type { Snippet } from "svelte";
3
4 let {
5 items,
6 itemHeight,
7 children,
8 scrollToIndex = $bindable(),
9 } = $props<{
10 items: any[];
11 itemHeight: number;
12 children: Snippet<[any, number]>;
13 scrollToIndex?: (index: number) => void;
14 }>();
15
16 let container: HTMLDivElement;
17 let scrollTop = $state(0);
18 let containerHeight = $state(0);
19
20 scrollToIndex = (index: number) => {
21 if (!container) return;
22 const targetTop = index * itemHeight;
23 const targetBottom = targetTop + itemHeight;
24 if (targetTop < container.scrollTop) {
25 container.scrollTop = targetTop;
26 } else if (targetBottom > container.scrollTop + containerHeight) {
27 container.scrollTop = targetBottom - containerHeight;
28 }
29 };
30
31 let visibleStart = $derived(
32 Math.max(0, Math.floor(scrollTop / itemHeight) - 8),
33 );
34 let visibleEnd = $derived(
35 Math.min(
36 items.length,
37 Math.ceil((scrollTop + containerHeight) / itemHeight) + 8,
38 ),
39 );
40</script>
41
42<div
43 bind:this={container}
44 bind:clientHeight={containerHeight}
45 class="virtual-container"
46 onscroll={() => (scrollTop = container.scrollTop)}
47 tabindex="-1"
48>
49 <div class="virtual-spacer" style:block-size="{items.length * itemHeight}px">
50 {#each items.slice(visibleStart, visibleEnd) as item, i (i + visibleStart)}
51 {@const index = i + visibleStart}
52 <div
53 class="virtual-item"
54 style:block-size="{itemHeight}px"
55 style:transform="translateY({index * itemHeight}px)"
56 >
57 {@render children(item, index)}
58 </div>
59 {/each}
60 </div>
61</div>
62
63<style>
64 .virtual-container {
65 block-size: 100%;
66 overflow-y: auto;
67 padding-inline: 1rem;
68 }
69 .virtual-spacer {
70 position: relative;
71 }
72 .virtual-item {
73 position: absolute;
74 inset-inline-start: 0;
75 inset-inline-end: 0;
76 }
77</style>