[READ-ONLY] Mirror of https://github.com/flo-bit/room. tiny 3d rooms saved locally or in your bluesky account, svelte/threlte
flo-bit.dev/room/
4.3 kB
167 lines
1<script lang="ts">
2 import NumberFlow, { NumberFlowGroup } from '@number-flow/svelte';
3 import { onMount } from 'svelte';
4 import WaveSurfer from 'wavesurfer.js';
5
6 import { ThemeWatcher } from '$lib/helper/ThemeWatcher.svelte';
7
8 let wavesurfer: WaveSurfer | null = $state(null);
9
10 let { url, darkMode = 'class' }: {
11 url: string;
12 darkMode?: 'class' | 'mediaQuery';
13 } = $props();
14
15 let audioContainer: HTMLElement | null = $state(null);
16
17 let audioDuration = $state(0);
18 let currentTime = $state(0);
19 let remainingTime = $derived(Math.round(audioDuration - currentTime));
20 let remainingSeconds = $derived(remainingTime % 60);
21 let remainingMinutes = $derived(Math.floor(remainingTime / 60));
22
23 let isPlaying = $state(false);
24 let isLoaded = $state(false);
25
26 let themeWatcher: ThemeWatcher | null = $state(null);
27
28 function isDarkMode() {
29 return themeWatcher?.isDarkMode() ?? false;
30 }
31
32 function getCSSVar(variable: string) {
33 return themeWatcher?.getCSSVar(variable) ?? '';
34 }
35
36 onMount(() => {
37 if (!audioContainer) return;
38
39 themeWatcher = new ThemeWatcher(updateWaveformColors, darkMode);
40
41 wavesurfer = WaveSurfer.create({
42 container: audioContainer,
43 cursorWidth: 2,
44 height: 16,
45 barWidth: 2,
46 barGap: 1,
47 barRadius: 2,
48 dragToSeek: {
49 debounceTime: 100
50 },
51 fillParent: true,
52 normalize: true,
53 hideScrollbar: true
54 });
55
56 updateWaveformColors();
57
58 wavesurfer?.load(url);
59
60 wavesurfer?.on('ready', (duration: number) => {
61 audioDuration = duration;
62 isLoaded = true;
63 });
64
65 wavesurfer?.on('timeupdate', (time: number) => {
66 currentTime = time;
67 });
68
69 wavesurfer?.on('play', () => {
70 isPlaying = true;
71 });
72
73 wavesurfer?.on('pause', () => {
74 isPlaying = false;
75 });
76
77 return () => {
78 wavesurfer?.pause();
79 wavesurfer?.destroy();
80
81 themeWatcher?.destroy();
82 };
83 });
84
85 function updateWaveformColors() {
86 wavesurfer?.setOptions({
87 waveColor: isDarkMode() ? getCSSVar('--color-accent-950') : getCSSVar('--color-accent-200'),
88 progressColor: isDarkMode() ? getCSSVar('--color-accent-500') : getCSSVar('--color-accent-500')
89 });
90 }
91
92 function playAudio() {
93 wavesurfer?.play();
94 }
95
96 function pauseAudio() {
97 wavesurfer?.pause();
98 }
99</script>
100
101<div class="relative flex h-24 w-full items-center justify-center gap-4 overflow-hidden">
102 <button
103 onclick={() => {
104 isPlaying ? pauseAudio() : playAudio();
105 }}
106 class="bg-accent-600 hover:bg-accent-500 inline-flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-30 md:size-14"
107 >
108 {#if isPlaying}
109 <svg
110 xmlns="http://www.w3.org/2000/svg"
111 viewBox="0 0 24 24"
112 fill="currentColor"
113 class="size-6 md:size-9 text-base-50 dark:text-base-950"
114 >
115 <path
116 fill-rule="evenodd"
117 d="M6.75 5.25a.75.75 0 0 1 .75-.75H9a.75.75 0 0 1 .75.75v13.5a.75.75 0 0 1-.75.75H7.5a.75.75 0 0 1-.75-.75V5.25Zm7.5 0A.75.75 0 0 1 15 4.5h1.5a.75.75 0 0 1 .75.75v13.5a.75.75 0 0 1-.75.75H15a.75.75 0 0 1-.75-.75V5.25Z"
118 clip-rule="evenodd"
119 />
120 </svg>
121 {:else}
122 <svg
123 xmlns="http://www.w3.org/2000/svg"
124 viewBox="0 0 24 24"
125 fill="currentColor"
126 class="ml-1 size-6 md:ml-1.5 md:size-9 text-base-50 dark:text-base-950"
127 >
128 <path
129 fill-rule="evenodd"
130 d="M4.5 5.653c0-1.427 1.529-2.33 2.779-1.643l11.54 6.347c1.295.712 1.295 2.573 0 3.286L7.28 19.99c-1.25.687-2.779-.217-2.779-1.643V5.653Z"
131 clip-rule="evenodd"
132 />
133 </svg>
134 {/if}
135 <span class="sr-only">{isPlaying ? 'Pause' : 'Play'}</span>
136 </button>
137 <div bind:this={audioContainer} class="h-4 w-full grow">
138 {#if !isLoaded}
139 <div class="m-1.5 h-1 w-full dark:bg-accent-950 bg-accent-100 rounded-full"></div>
140 {/if}
141 </div>
142 {#if isLoaded}
143 <NumberFlowGroup>
144 <div
145 class="text-accent-500 flex items-end justify-end font-semibold"
146 style="font-variant-numeric: tabular-nums;"
147 >
148 -<NumberFlow
149 value={remainingMinutes}
150 trend={-1}
151 format={{ minimumIntegerDigits: 2 }}
152 digits={{ 1: { max: 5 } }}
153 />
154 <NumberFlow
155 value={remainingSeconds}
156 trend={-1}
157 format={{ minimumIntegerDigits: 2 }}
158 digits={{ 1: { max: 5 } }}
159 prefix=":"
160 />
161 </div>
162 </NumberFlowGroup>
163 {:else}
164
165 <div class="text-accent-500 flex items-end justify-end text-sm font-medium">loading</div>
166 {/if}
167</div>