Music streaming on ATProto!
1<script lang="ts"> 2 import { 3 List, 4 Pause, 5 Play, 6 Repeat, 7 Shuffle, 8 SkipBack, 9 SkipForward, 10 Volume2, 11 type Icon as LucideIcon, 12 } from "@lucide/svelte"; 13 import { Slider } from "bits-ui"; 14 import cn from "clsx"; 15 16 let playing = $state(true); 17 let shuffle = $state(false); 18 let repeat = $state(false); 19 20 const MainIcon = $derived(playing ? Pause : Play); 21 22 const songLength = 256; 23 let playback = $state(0); 24</script> 25 26{#snippet plainButton(Icon: typeof LucideIcon, label: string)} 27 <button class="flex cursor-pointer" aria-label={label}> 28 <Icon /> 29 </button> 30{/snippet} 31 32{#snippet clickable(content: string)} 33 <span class="cursor-pointer hover:underline">{content}</span> 34{/snippet} 35 36<!-- TODO: labelled by the artist & title --> 37<aside 38 class="fixed right-2 bottom-2 left-2 flex items-center gap-4 rounded-lg border border-slate-300 bg-white p-2 px-4 text-slate-500" 39> 40 <div class="flex items-center gap-2 text-slate-900"> 41 {@render plainButton(SkipBack, "Previous song")} 42 <button 43 class="flex cursor-pointer items-center justify-center rounded-full bg-orange-500 p-2 text-white" 44 aria-label="Play" 45 onclick={() => (playing = !playing)} 46 > 47 <MainIcon /> 48 </button> 49 {@render plainButton(SkipForward, "Next song")} 50 </div> 51 52 <div class="flex items-center gap-2"> 53 <img 54 src="https://lh3.googleusercontent.com/0z6Kg2GFi8hFgZYxWm3c3UNul0gyaCQjuqmY-p1oeFC1n5EMOf1dxrownTzhzk-_cdtO_FLLktQcMecwGQ=w544-h544-l90-rj" 55 class="h-12 w-12 rounded object-cover object-center" 56 alt="" 57 /> 58 <div class="flex flex-col"> 59 <span class="text-sm font-semibold text-slate-900 opacity-70"> 60 {@render clickable("Protostar")}, {@render clickable("Laminar")} & {@render clickable( 61 "imallryt", 62 )} 63 </span> 64 <span class="font-bolder text-sm font-semibold text-slate-900"> 65 {@render clickable("Blood in the Water")} 66 <!-- <span class="opacity-50">| {@render clickable("Epic Album")}</span> --> 67 </span> 68 </div> 69 </div> 70 71 <div class="flex flex-1 px-30"> 72 <Slider.Root 73 type="single" 74 bind:value={playback} 75 max={songLength} 76 class="relative flex flex-1 touch-none items-center select-none" 77 > 78 {#snippet children()} 79 <span 80 class="relative h-1 w-full cursor-pointer overflow-hidden rounded-full bg-slate-200" 81 > 82 <Slider.Range class="absolute h-full rounded-full bg-orange-500" /> 83 </span> 84 <Slider.Thumb 85 index={0} 86 class="block size-4 cursor-pointer rounded-full border border-slate-900 bg-slate-50 focus-visible:ring focus-visible:ring-orange-500 focus-visible:ring-offset-2 " 87 /> 88 {/snippet} 89 </Slider.Root> 90 </div> 91 92 <button 93 class={cn("flex", "cursor-pointer", { "text-orange-500": shuffle })} 94 onclick={() => (shuffle = !shuffle)} 95 > 96 <Shuffle /> 97 </button> 98 <button 99 class={cn("flex", "cursor-pointer", { "text-orange-500": repeat })} 100 onclick={() => (repeat = !repeat)} 101 > 102 <Repeat /> 103 </button> 104 105 <List class="cursor-pointer" /> 106 <Volume2 class="cursor-pointer" /> 107</aside>