@joewinke/jatui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +46 -0
- package/src/lib/components/AudioWaveform.svelte +694 -0
- package/src/lib/components/AvailabilityModal.svelte +173 -0
- package/src/lib/components/Badge.svelte +38 -0
- package/src/lib/components/BookingForm.svelte +276 -0
- package/src/lib/components/Button.svelte +72 -0
- package/src/lib/components/CalendarPicker.svelte +284 -0
- package/src/lib/components/Card.svelte +67 -0
- package/src/lib/components/CharacterCounter.svelte +82 -0
- package/src/lib/components/ChipInput.svelte +596 -0
- package/src/lib/components/ColorSelector.svelte +163 -0
- package/src/lib/components/ConfirmModal.svelte +75 -0
- package/src/lib/components/CountdownTimer.svelte +94 -0
- package/src/lib/components/DateRangePicker.svelte +192 -0
- package/src/lib/components/Drawer.svelte +110 -0
- package/src/lib/components/FilterDropdown.svelte +202 -0
- package/src/lib/components/ImageUpload.svelte +97 -0
- package/src/lib/components/InlineEdit.svelte +283 -0
- package/src/lib/components/LazyImage.svelte +122 -0
- package/src/lib/components/LoadingSpinner.svelte +102 -0
- package/src/lib/components/Modal.svelte +208 -0
- package/src/lib/components/PhoneInput.svelte +92 -0
- package/src/lib/components/ResizableDivider.svelte +305 -0
- package/src/lib/components/ResizablePanel.svelte +302 -0
- package/src/lib/components/SearchDropdown.svelte +341 -0
- package/src/lib/components/SelectInput.svelte +215 -0
- package/src/lib/components/SignaturePad.svelte +171 -0
- package/src/lib/components/SortDropdown.svelte +148 -0
- package/src/lib/components/Sparkline.svelte +107 -0
- package/src/lib/components/SpeechForm.svelte +114 -0
- package/src/lib/components/StatusBadge.svelte +155 -0
- package/src/lib/components/TextArea.svelte +143 -0
- package/src/lib/components/TextInput.svelte +108 -0
- package/src/lib/components/ThemeSelector.svelte +195 -0
- package/src/lib/components/TimeSlotPicker.svelte +162 -0
- package/src/lib/components/VoicePlayer.svelte +420 -0
- package/src/lib/components/messaging/Avatar.svelte +81 -0
- package/src/lib/components/messaging/ChannelInfoModal.svelte +163 -0
- package/src/lib/components/messaging/ChannelList.svelte +107 -0
- package/src/lib/components/messaging/ChannelMemberAvatarStack.svelte +69 -0
- package/src/lib/components/messaging/ChannelMembersModal.svelte +182 -0
- package/src/lib/components/messaging/CreateChannelModal.svelte +190 -0
- package/src/lib/components/messaging/DirectMessageList.svelte +145 -0
- package/src/lib/components/messaging/EmojiSelector.svelte +260 -0
- package/src/lib/components/messaging/MentionAutocomplete.svelte +193 -0
- package/src/lib/components/messaging/MessageAttachment.svelte +270 -0
- package/src/lib/components/messaging/MessageAttachmentUpload.svelte +243 -0
- package/src/lib/components/messaging/MessageInput.svelte +451 -0
- package/src/lib/components/messaging/MessageItem.svelte +338 -0
- package/src/lib/components/messaging/MessageThread.svelte +306 -0
- package/src/lib/components/messaging/NotificationSettingsModal.svelte +234 -0
- package/src/lib/components/messaging/QuotedMessageDisplay.svelte +118 -0
- package/src/lib/components/messaging/StartDMModal.svelte +100 -0
- package/src/lib/components/messaging/ThreadPanel.svelte +153 -0
- package/src/lib/index.ts +185 -0
- package/src/lib/types/booking.ts +143 -0
- package/src/lib/types/messaging.ts +459 -0
- package/src/lib/utils/currency.ts +20 -0
- package/src/lib/utils/daisyuiColors.ts +243 -0
- package/src/lib/utils/dateFormatters.ts +153 -0
- package/src/lib/utils/mentionParser.ts +188 -0
- package/src/lib/utils/phoneFormat.ts +74 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy } from "svelte"
|
|
3
|
+
import type { Snippet } from "svelte"
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
getDaisyUIColor,
|
|
7
|
+
lightenColor,
|
|
8
|
+
darkenColor,
|
|
9
|
+
watchThemeChanges,
|
|
10
|
+
} from "../utils/daisyuiColors"
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
audioUrl,
|
|
14
|
+
height = 80,
|
|
15
|
+
waveColor = "neutral",
|
|
16
|
+
progressColor = "accent",
|
|
17
|
+
backgroundColor = "base-100",
|
|
18
|
+
waveColorHex = "#6b7280",
|
|
19
|
+
progressColorHex = "#f97316",
|
|
20
|
+
backgroundColorHex = "#f1f5f9",
|
|
21
|
+
interactive = true,
|
|
22
|
+
showTrimControls = false,
|
|
23
|
+
onTrim,
|
|
24
|
+
providedDuration,
|
|
25
|
+
speedControls,
|
|
26
|
+
} = $props<{
|
|
27
|
+
audioUrl: string
|
|
28
|
+
height?: number
|
|
29
|
+
waveColor?: string
|
|
30
|
+
progressColor?: string
|
|
31
|
+
backgroundColor?: string
|
|
32
|
+
waveColorHex?: string
|
|
33
|
+
progressColorHex?: string
|
|
34
|
+
backgroundColorHex?: string
|
|
35
|
+
interactive?: boolean
|
|
36
|
+
showTrimControls?: boolean
|
|
37
|
+
onTrim?: (startTime: number, endTime: number) => void
|
|
38
|
+
providedDuration?: number
|
|
39
|
+
speedControls?: Snippet
|
|
40
|
+
}>()
|
|
41
|
+
|
|
42
|
+
let canvas = $state<HTMLCanvasElement | null>(null)
|
|
43
|
+
let audio = $state<HTMLAudioElement | null>(null)
|
|
44
|
+
let canvasContext: CanvasRenderingContext2D | null = null
|
|
45
|
+
|
|
46
|
+
let isPlaying = $state(false)
|
|
47
|
+
let currentTime = $state(0)
|
|
48
|
+
let duration = $state(0)
|
|
49
|
+
let isLoading = $state(true)
|
|
50
|
+
let waveformData: number[] = []
|
|
51
|
+
|
|
52
|
+
let hoverX = $state(0)
|
|
53
|
+
let isHovering = $state(false)
|
|
54
|
+
let hoverTime = $state(0)
|
|
55
|
+
|
|
56
|
+
let trimStart = $state(0)
|
|
57
|
+
let trimEnd = $state(0)
|
|
58
|
+
let isDraggingStart = $state(false)
|
|
59
|
+
let isDraggingEnd = $state(false)
|
|
60
|
+
let isInTrimMode = $state(false)
|
|
61
|
+
|
|
62
|
+
function resolveColor(daisyUIName: string, hexFallback: string): string {
|
|
63
|
+
if (typeof window === "undefined") return hexFallback
|
|
64
|
+
try {
|
|
65
|
+
if (daisyUIName.startsWith("#")) return daisyUIName
|
|
66
|
+
return getDaisyUIColor(daisyUIName)
|
|
67
|
+
} catch {
|
|
68
|
+
return hexFallback
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let resolvedWaveColor = $state(resolveColor(waveColor, waveColorHex))
|
|
73
|
+
let resolvedProgressColor = $state(resolveColor(progressColor, progressColorHex))
|
|
74
|
+
let resolvedBackgroundColor = $state(resolveColor(backgroundColor, backgroundColorHex))
|
|
75
|
+
|
|
76
|
+
function updateResolvedColors() {
|
|
77
|
+
resolvedWaveColor = resolveColor(waveColor, waveColorHex)
|
|
78
|
+
resolvedProgressColor = resolveColor(progressColor, progressColorHex)
|
|
79
|
+
resolvedBackgroundColor = resolveColor(backgroundColor, backgroundColorHex)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
onMount(async () => {
|
|
83
|
+
const cleanupThemeWatcher = watchThemeChanges(() => {
|
|
84
|
+
updateResolvedColors()
|
|
85
|
+
if (canvasContext && canvas && waveformData.length > 0) {
|
|
86
|
+
drawWaveform()
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
if (canvas) {
|
|
91
|
+
canvasContext = canvas.getContext("2d")
|
|
92
|
+
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
if (canvas && canvasContext) {
|
|
95
|
+
const container = canvas.parentElement
|
|
96
|
+
if (container) {
|
|
97
|
+
const containerRect = container.getBoundingClientRect()
|
|
98
|
+
const displayWidth = containerRect.width
|
|
99
|
+
const displayHeight = height
|
|
100
|
+
|
|
101
|
+
canvas.width = displayWidth
|
|
102
|
+
canvas.height = displayHeight
|
|
103
|
+
canvas.style.width = displayWidth + "px"
|
|
104
|
+
canvas.style.height = displayHeight + "px"
|
|
105
|
+
|
|
106
|
+
updateResolvedColors()
|
|
107
|
+
loadAudioAndGenerateWaveform()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}, 100)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (audio) {
|
|
114
|
+
audio.load()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return cleanupThemeWatcher
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
async function loadAudioAndGenerateWaveform() {
|
|
121
|
+
try {
|
|
122
|
+
isLoading = true
|
|
123
|
+
generateSimpleWaveform()
|
|
124
|
+
isLoading = false
|
|
125
|
+
} catch {
|
|
126
|
+
isLoading = false
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function generateSimpleWaveform() {
|
|
131
|
+
if (!canvas) return
|
|
132
|
+
|
|
133
|
+
const canvasWidth = canvas.width
|
|
134
|
+
|
|
135
|
+
if (waveformData.length === 0 || waveformData.length !== canvasWidth) {
|
|
136
|
+
waveformData = []
|
|
137
|
+
|
|
138
|
+
const seed = audioUrl
|
|
139
|
+
? audioUrl.split("").reduce((acc: number, char: string) => acc + char.charCodeAt(0), 0)
|
|
140
|
+
: 12345
|
|
141
|
+
|
|
142
|
+
for (let i = 0; i < canvasWidth; i++) {
|
|
143
|
+
const progress = i / canvasWidth
|
|
144
|
+
const baseAmplitude = Math.sin(progress * Math.PI * 2 + seed * 0.001) * 0.3 + 0.5
|
|
145
|
+
const speechPattern = Math.sin(progress * Math.PI * 20 + seed * 0.002) * 0.2
|
|
146
|
+
const variation = Math.sin(progress * Math.PI * 8 + seed * 0.003) * 0.1
|
|
147
|
+
|
|
148
|
+
let amplitude = baseAmplitude + speechPattern + variation
|
|
149
|
+
amplitude = Math.max(0.1, Math.min(0.9, amplitude))
|
|
150
|
+
|
|
151
|
+
waveformData.push(amplitude)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
drawWaveform()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function createWaveformGradient() {
|
|
159
|
+
if (!canvasContext || !canvas) return null
|
|
160
|
+
|
|
161
|
+
const gradient = canvasContext.createLinearGradient(0, 0, 0, canvas.height)
|
|
162
|
+
const neutralColor = resolvedWaveColor
|
|
163
|
+
const neutralContentColor = resolveColor("neutral-content", "#ffffff")
|
|
164
|
+
const lighterNeutral = lightenColor(neutralColor, 0.2)
|
|
165
|
+
|
|
166
|
+
gradient.addColorStop(0, neutralColor)
|
|
167
|
+
gradient.addColorStop(0.7, neutralColor)
|
|
168
|
+
gradient.addColorStop(0.71, neutralContentColor)
|
|
169
|
+
gradient.addColorStop(0.72, neutralContentColor)
|
|
170
|
+
gradient.addColorStop(0.73, lighterNeutral)
|
|
171
|
+
gradient.addColorStop(1, lighterNeutral)
|
|
172
|
+
|
|
173
|
+
return gradient
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function createProgressGradient() {
|
|
177
|
+
if (!canvasContext || !canvas) return null
|
|
178
|
+
|
|
179
|
+
const gradient = canvasContext.createLinearGradient(0, 0, 0, canvas.height)
|
|
180
|
+
const accentColor = resolvedProgressColor
|
|
181
|
+
const accentContentColor = resolveColor("accent-content", "#ffffff")
|
|
182
|
+
const darkerAccent = darkenColor(accentColor, 0.1)
|
|
183
|
+
const lighterAccent = lightenColor(accentColor, 0.3)
|
|
184
|
+
|
|
185
|
+
gradient.addColorStop(0, accentColor)
|
|
186
|
+
gradient.addColorStop(0.7, darkerAccent)
|
|
187
|
+
gradient.addColorStop(0.71, accentContentColor)
|
|
188
|
+
gradient.addColorStop(0.72, accentContentColor)
|
|
189
|
+
gradient.addColorStop(0.73, lighterAccent)
|
|
190
|
+
gradient.addColorStop(1, lighterAccent)
|
|
191
|
+
|
|
192
|
+
return gradient
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function drawBaseWaveform() {
|
|
196
|
+
if (!canvasContext || !canvas || waveformData.length === 0) return
|
|
197
|
+
|
|
198
|
+
const { width, height } = canvas
|
|
199
|
+
canvasContext.clearRect(0, 0, width, height)
|
|
200
|
+
|
|
201
|
+
canvasContext.fillStyle = resolvedBackgroundColor
|
|
202
|
+
canvasContext.fillRect(0, 0, width, height)
|
|
203
|
+
|
|
204
|
+
const barWidth = 2
|
|
205
|
+
const barSpacing = 1
|
|
206
|
+
const totalBarWidth = barWidth + barSpacing
|
|
207
|
+
const numBars = Math.floor(width / totalBarWidth)
|
|
208
|
+
const centerY = height / 2
|
|
209
|
+
|
|
210
|
+
const validDuration = isFinite(duration) && duration > 0
|
|
211
|
+
const progressX = validDuration ? (currentTime / duration) * width : 0
|
|
212
|
+
const trimStartX = validDuration ? (trimStart / duration) * width : 0
|
|
213
|
+
const trimEndX = validDuration ? (trimEnd / duration) * width : width
|
|
214
|
+
|
|
215
|
+
const waveGradient = createWaveformGradient()
|
|
216
|
+
const progressGradient = createProgressGradient()
|
|
217
|
+
|
|
218
|
+
for (let i = 0; i < numBars; i++) {
|
|
219
|
+
const dataIndex = Math.floor((i / numBars) * waveformData.length)
|
|
220
|
+
const amplitude = waveformData[dataIndex] || 0.1
|
|
221
|
+
const barHeight = Math.max(4, amplitude * height * 0.8)
|
|
222
|
+
const x = i * totalBarWidth
|
|
223
|
+
const y = centerY - barHeight / 2
|
|
224
|
+
|
|
225
|
+
let fillStyle = waveGradient || waveColor
|
|
226
|
+
if (showTrimControls && isInTrimMode) {
|
|
227
|
+
if (x < trimStartX || x > trimEndX) {
|
|
228
|
+
canvasContext.globalAlpha = 0.3
|
|
229
|
+
} else if (x < progressX) {
|
|
230
|
+
fillStyle = progressGradient || progressColor
|
|
231
|
+
canvasContext.globalAlpha = 1
|
|
232
|
+
} else {
|
|
233
|
+
canvasContext.globalAlpha = 1
|
|
234
|
+
}
|
|
235
|
+
} else if (x < progressX) {
|
|
236
|
+
fillStyle = progressGradient || progressColor
|
|
237
|
+
canvasContext.globalAlpha = 1
|
|
238
|
+
} else {
|
|
239
|
+
canvasContext.globalAlpha = 1
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
canvasContext.fillStyle = fillStyle
|
|
243
|
+
canvasContext.fillRect(x, y, barWidth, barHeight)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
canvasContext.globalAlpha = 1
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function drawOverlays() {
|
|
250
|
+
if (!canvasContext || !canvas) return
|
|
251
|
+
|
|
252
|
+
const { width, height } = canvas
|
|
253
|
+
|
|
254
|
+
const validDuration = isFinite(duration) && duration > 0
|
|
255
|
+
const progressX = validDuration ? (currentTime / duration) * width : 0
|
|
256
|
+
const trimStartX = validDuration ? (trimStart / duration) * width : 0
|
|
257
|
+
const trimEndX = validDuration ? (trimEnd / duration) * width : width
|
|
258
|
+
|
|
259
|
+
if (isHovering && hoverX > 0) {
|
|
260
|
+
canvasContext.fillStyle = "rgba(255, 255, 255, 0.15)"
|
|
261
|
+
const rect = canvas.getBoundingClientRect()
|
|
262
|
+
const canvasHoverX = (hoverX / rect.width) * width
|
|
263
|
+
canvasContext.fillRect(0, 0, canvasHoverX, height)
|
|
264
|
+
|
|
265
|
+
canvasContext.strokeStyle = "rgba(255, 255, 255, 0.6)"
|
|
266
|
+
canvasContext.lineWidth = 1
|
|
267
|
+
canvasContext.beginPath()
|
|
268
|
+
canvasContext.moveTo(canvasHoverX, 0)
|
|
269
|
+
canvasContext.lineTo(canvasHoverX, height)
|
|
270
|
+
canvasContext.stroke()
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (progressX > 0) {
|
|
274
|
+
canvasContext.strokeStyle = resolvedProgressColor
|
|
275
|
+
canvasContext.lineWidth = 2
|
|
276
|
+
canvasContext.beginPath()
|
|
277
|
+
canvasContext.moveTo(progressX, 0)
|
|
278
|
+
canvasContext.lineTo(progressX, height)
|
|
279
|
+
canvasContext.stroke()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (showTrimControls && isInTrimMode) {
|
|
283
|
+
canvasContext.strokeStyle = "#ef4444"
|
|
284
|
+
canvasContext.lineWidth = 3
|
|
285
|
+
canvasContext.beginPath()
|
|
286
|
+
canvasContext.moveTo(trimStartX, 0)
|
|
287
|
+
canvasContext.lineTo(trimStartX, height)
|
|
288
|
+
canvasContext.stroke()
|
|
289
|
+
|
|
290
|
+
canvasContext.strokeStyle = "#ef4444"
|
|
291
|
+
canvasContext.lineWidth = 3
|
|
292
|
+
canvasContext.beginPath()
|
|
293
|
+
canvasContext.moveTo(trimEndX, 0)
|
|
294
|
+
canvasContext.lineTo(trimEndX, height)
|
|
295
|
+
canvasContext.stroke()
|
|
296
|
+
|
|
297
|
+
canvasContext.fillStyle = "rgba(59, 130, 246, 0.1)"
|
|
298
|
+
canvasContext.fillRect(trimStartX, 0, trimEndX - trimStartX, height)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function drawWaveform() {
|
|
303
|
+
drawBaseWaveform()
|
|
304
|
+
drawOverlays()
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function handleCanvasMouseDown(event: MouseEvent) {
|
|
308
|
+
if (!interactive || !canvas || !isFinite(duration) || duration <= 0) return
|
|
309
|
+
|
|
310
|
+
const rect = canvas.getBoundingClientRect()
|
|
311
|
+
const x = event.clientX - rect.left
|
|
312
|
+
const clickPosition = x / rect.width
|
|
313
|
+
const clickTime = clickPosition * duration
|
|
314
|
+
|
|
315
|
+
if (showTrimControls && isInTrimMode) {
|
|
316
|
+
const trimStartX = (trimStart / duration) * rect.width
|
|
317
|
+
const trimEndX = (trimEnd / duration) * rect.width
|
|
318
|
+
const threshold = 10
|
|
319
|
+
|
|
320
|
+
if (Math.abs(x - trimStartX) < threshold) {
|
|
321
|
+
isDraggingStart = true
|
|
322
|
+
return
|
|
323
|
+
} else if (Math.abs(x - trimEndX) < threshold) {
|
|
324
|
+
isDraggingEnd = true
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (audio) {
|
|
330
|
+
audio.currentTime = clickTime
|
|
331
|
+
currentTime = clickTime
|
|
332
|
+
drawWaveform()
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function handleCanvasMouseMove(event: MouseEvent) {
|
|
337
|
+
if (!canvas || !isFinite(duration) || duration <= 0) return
|
|
338
|
+
|
|
339
|
+
const rect = canvas.getBoundingClientRect()
|
|
340
|
+
const x = event.clientX - rect.left
|
|
341
|
+
const position = x / rect.width
|
|
342
|
+
const time = Math.max(0, Math.min(duration, position * duration))
|
|
343
|
+
|
|
344
|
+
if (interactive && !isDraggingStart && !isDraggingEnd) {
|
|
345
|
+
const wasHovering = isHovering
|
|
346
|
+
const oldHoverX = hoverX
|
|
347
|
+
|
|
348
|
+
isHovering = true
|
|
349
|
+
hoverX = x
|
|
350
|
+
hoverTime = time
|
|
351
|
+
|
|
352
|
+
if (!wasHovering || Math.abs(oldHoverX - hoverX) > 2) {
|
|
353
|
+
drawBaseWaveform()
|
|
354
|
+
drawOverlays()
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (isDraggingStart) {
|
|
359
|
+
trimStart = Math.min(time, trimEnd - 0.1)
|
|
360
|
+
drawWaveform()
|
|
361
|
+
} else if (isDraggingEnd) {
|
|
362
|
+
trimEnd = Math.max(time, trimStart + 0.1)
|
|
363
|
+
drawWaveform()
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (showTrimControls && isInTrimMode) {
|
|
367
|
+
const trimStartX = (trimStart / duration) * rect.width
|
|
368
|
+
const trimEndX = (trimEnd / duration) * rect.width
|
|
369
|
+
const threshold = 10
|
|
370
|
+
|
|
371
|
+
if (Math.abs(x - trimStartX) < threshold || Math.abs(x - trimEndX) < threshold) {
|
|
372
|
+
canvas.style.cursor = "ew-resize"
|
|
373
|
+
} else {
|
|
374
|
+
canvas.style.cursor = "pointer"
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
canvas.style.cursor = "pointer"
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function handleCanvasMouseUp() {
|
|
382
|
+
isDraggingStart = false
|
|
383
|
+
isDraggingEnd = false
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function handleCanvasMouseLeave() {
|
|
387
|
+
if (isHovering) {
|
|
388
|
+
isHovering = false
|
|
389
|
+
hoverX = 0
|
|
390
|
+
hoverTime = 0
|
|
391
|
+
drawBaseWaveform()
|
|
392
|
+
drawOverlays()
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function handlePlay() { isPlaying = true }
|
|
397
|
+
function handlePause() { isPlaying = false }
|
|
398
|
+
|
|
399
|
+
function handleTimeUpdate() {
|
|
400
|
+
if (audio) {
|
|
401
|
+
currentTime = audio.currentTime
|
|
402
|
+
drawWaveform()
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function handleLoadedMetadata() {
|
|
407
|
+
if (audio && isFinite(audio.duration) && audio.duration > 0) {
|
|
408
|
+
duration = audio.duration
|
|
409
|
+
trimStart = 0
|
|
410
|
+
trimEnd = duration
|
|
411
|
+
isLoading = false
|
|
412
|
+
drawWaveform()
|
|
413
|
+
} else if (providedDuration && providedDuration > 0) {
|
|
414
|
+
duration = providedDuration
|
|
415
|
+
trimStart = 0
|
|
416
|
+
trimEnd = duration
|
|
417
|
+
isLoading = false
|
|
418
|
+
drawWaveform()
|
|
419
|
+
} else {
|
|
420
|
+
duration = 40
|
|
421
|
+
trimStart = 0
|
|
422
|
+
trimEnd = duration
|
|
423
|
+
isLoading = false
|
|
424
|
+
drawWaveform()
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function handleEnded() {
|
|
429
|
+
isPlaying = false
|
|
430
|
+
currentTime = 0
|
|
431
|
+
drawWaveform()
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function handleCanPlayThrough() {
|
|
435
|
+
if (audio && isFinite(audio.duration) && audio.duration > 0) {
|
|
436
|
+
duration = audio.duration
|
|
437
|
+
trimStart = 0
|
|
438
|
+
trimEnd = duration
|
|
439
|
+
isLoading = false
|
|
440
|
+
drawWaveform()
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function handleAudioError() {
|
|
445
|
+
isLoading = false
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function togglePlayback() {
|
|
449
|
+
if (!audio) return
|
|
450
|
+
if (isPlaying) {
|
|
451
|
+
audio.pause()
|
|
452
|
+
} else {
|
|
453
|
+
audio.play()
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function formatTime(seconds: number): string {
|
|
458
|
+
if (!isFinite(seconds) || isNaN(seconds) || seconds < 0) return "0:00"
|
|
459
|
+
const mins = Math.floor(seconds / 60)
|
|
460
|
+
const secs = Math.floor(seconds % 60)
|
|
461
|
+
return `${mins}:${secs.toString().padStart(2, "0")}`
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function startTrimMode() {
|
|
465
|
+
if (!isFinite(duration) || duration <= 0) return
|
|
466
|
+
isInTrimMode = true
|
|
467
|
+
trimStart = 0
|
|
468
|
+
trimEnd = duration
|
|
469
|
+
drawWaveform()
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function cancelTrim() {
|
|
473
|
+
isInTrimMode = false
|
|
474
|
+
trimStart = 0
|
|
475
|
+
trimEnd = duration
|
|
476
|
+
drawWaveform()
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function applyTrim() {
|
|
480
|
+
if (onTrim) onTrim(trimStart, trimEnd)
|
|
481
|
+
isInTrimMode = false
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function previewTrimSelection() {
|
|
485
|
+
if (audio) {
|
|
486
|
+
audio.currentTime = trimStart
|
|
487
|
+
currentTime = trimStart
|
|
488
|
+
audio.play()
|
|
489
|
+
|
|
490
|
+
const checkTime = () => {
|
|
491
|
+
if (audio.currentTime >= trimEnd) {
|
|
492
|
+
audio.pause()
|
|
493
|
+
} else if (isPlaying) {
|
|
494
|
+
requestAnimationFrame(checkTime)
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
requestAnimationFrame(checkTime)
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
$effect(() => {
|
|
502
|
+
if (canvas && canvasContext) {
|
|
503
|
+
const rect = canvas.getBoundingClientRect()
|
|
504
|
+
canvas.width = rect.width
|
|
505
|
+
canvas.height = height
|
|
506
|
+
canvas.style.width = rect.width + "px"
|
|
507
|
+
canvas.style.height = height + "px"
|
|
508
|
+
generateSimpleWaveform()
|
|
509
|
+
}
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
function resizeCanvas() {
|
|
513
|
+
if (!canvas || !canvasContext) return
|
|
514
|
+
const container = canvas.parentElement
|
|
515
|
+
if (!container) return
|
|
516
|
+
|
|
517
|
+
const containerRect = container.getBoundingClientRect()
|
|
518
|
+
let displayWidth = containerRect.width
|
|
519
|
+
const displayHeight = height
|
|
520
|
+
|
|
521
|
+
if (displayWidth <= 0) {
|
|
522
|
+
let parent = container.parentElement
|
|
523
|
+
while (parent && displayWidth <= 0) {
|
|
524
|
+
displayWidth = parent.getBoundingClientRect().width
|
|
525
|
+
parent = parent.parentElement
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (displayWidth > 0 && Math.abs(canvas.width - displayWidth) > 2) {
|
|
530
|
+
canvas.width = displayWidth
|
|
531
|
+
canvas.height = displayHeight
|
|
532
|
+
canvas.style.width = displayWidth + "px"
|
|
533
|
+
canvas.style.height = displayHeight + "px"
|
|
534
|
+
waveformData = []
|
|
535
|
+
generateSimpleWaveform()
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
$effect(() => {
|
|
540
|
+
if (!canvas) return
|
|
541
|
+
|
|
542
|
+
const handleResize = () => {
|
|
543
|
+
setTimeout(() => resizeCanvas(), 50)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
window.addEventListener("resize", handleResize)
|
|
547
|
+
|
|
548
|
+
const observer = new MutationObserver(() => handleResize())
|
|
549
|
+
|
|
550
|
+
if (canvas.parentElement) {
|
|
551
|
+
observer.observe(canvas.parentElement, {
|
|
552
|
+
attributes: true,
|
|
553
|
+
childList: true,
|
|
554
|
+
subtree: true,
|
|
555
|
+
})
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
setTimeout(() => resizeCanvas(), 200)
|
|
559
|
+
|
|
560
|
+
return () => {
|
|
561
|
+
window.removeEventListener("resize", handleResize)
|
|
562
|
+
observer.disconnect()
|
|
563
|
+
}
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
$effect(() => {
|
|
567
|
+
if (audio && audioUrl) {
|
|
568
|
+
currentTime = 0
|
|
569
|
+
duration = 0
|
|
570
|
+
isLoading = true
|
|
571
|
+
audio.load()
|
|
572
|
+
}
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
onDestroy(() => {
|
|
576
|
+
if (audio) audio.pause()
|
|
577
|
+
})
|
|
578
|
+
</script>
|
|
579
|
+
|
|
580
|
+
<div class="audio-waveform w-full">
|
|
581
|
+
<audio
|
|
582
|
+
bind:this={audio}
|
|
583
|
+
src={audioUrl}
|
|
584
|
+
onplay={handlePlay}
|
|
585
|
+
onpause={handlePause}
|
|
586
|
+
ontimeupdate={handleTimeUpdate}
|
|
587
|
+
onloadedmetadata={handleLoadedMetadata}
|
|
588
|
+
oncanplaythrough={handleCanPlayThrough}
|
|
589
|
+
onended={handleEnded}
|
|
590
|
+
onerror={handleAudioError}
|
|
591
|
+
preload="metadata"
|
|
592
|
+
crossorigin="anonymous"
|
|
593
|
+
></audio>
|
|
594
|
+
|
|
595
|
+
<div class="relative w-full" style="height: {height}px;">
|
|
596
|
+
{#if isLoading}
|
|
597
|
+
<div class="absolute inset-0 flex items-center justify-center bg-base-200 rounded">
|
|
598
|
+
<span class="loading loading-dots loading-sm"></span>
|
|
599
|
+
<span class="ml-2 text-sm">Loading waveform...</span>
|
|
600
|
+
</div>
|
|
601
|
+
{/if}
|
|
602
|
+
|
|
603
|
+
<canvas
|
|
604
|
+
bind:this={canvas}
|
|
605
|
+
class="rounded border border-base-300"
|
|
606
|
+
style="display: block;"
|
|
607
|
+
onmousedown={handleCanvasMouseDown}
|
|
608
|
+
onmousemove={handleCanvasMouseMove}
|
|
609
|
+
onmouseup={handleCanvasMouseUp}
|
|
610
|
+
onmouseleave={handleCanvasMouseLeave}
|
|
611
|
+
></canvas>
|
|
612
|
+
|
|
613
|
+
{#if isHovering && interactive && hoverX > 0}
|
|
614
|
+
<div
|
|
615
|
+
class="absolute pointer-events-none bg-base-900 text-base-100 text-xs px-2 py-1 rounded shadow-lg z-10"
|
|
616
|
+
style="left: {hoverX - 20}px; top: -30px;"
|
|
617
|
+
>
|
|
618
|
+
{formatTime(hoverTime)}
|
|
619
|
+
</div>
|
|
620
|
+
{/if}
|
|
621
|
+
</div>
|
|
622
|
+
|
|
623
|
+
<div class="flex items-center justify-between mt-3 gap-4">
|
|
624
|
+
<div class="flex items-center gap-3">
|
|
625
|
+
<button
|
|
626
|
+
class="btn btn-circle btn-primary btn-sm"
|
|
627
|
+
onclick={togglePlayback}
|
|
628
|
+
disabled={isLoading}
|
|
629
|
+
aria-label={isPlaying ? "Pause" : "Play"}
|
|
630
|
+
>
|
|
631
|
+
{#if isLoading}
|
|
632
|
+
<span class="loading loading-spinner loading-xs"></span>
|
|
633
|
+
{:else if isPlaying}
|
|
634
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
|
|
635
|
+
{:else}
|
|
636
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="6 3 20 12 6 21 6 3"/></svg>
|
|
637
|
+
{/if}
|
|
638
|
+
</button>
|
|
639
|
+
|
|
640
|
+
<div class="text-sm font-mono">
|
|
641
|
+
{formatTime(currentTime)} / {formatTime(duration)}
|
|
642
|
+
</div>
|
|
643
|
+
</div>
|
|
644
|
+
|
|
645
|
+
<div class="flex items-center">
|
|
646
|
+
{#if speedControls}
|
|
647
|
+
{@render speedControls()}
|
|
648
|
+
{/if}
|
|
649
|
+
</div>
|
|
650
|
+
</div>
|
|
651
|
+
|
|
652
|
+
{#if showTrimControls}
|
|
653
|
+
<div class="mt-4">
|
|
654
|
+
{#if !isInTrimMode}
|
|
655
|
+
<button
|
|
656
|
+
class="btn btn-outline btn-sm"
|
|
657
|
+
onclick={startTrimMode}
|
|
658
|
+
disabled={isLoading || !isFinite(duration) || duration <= 0}
|
|
659
|
+
>
|
|
660
|
+
Trim Audio
|
|
661
|
+
</button>
|
|
662
|
+
{:else}
|
|
663
|
+
<div class="space-y-3">
|
|
664
|
+
<div class="text-sm">
|
|
665
|
+
<div class="flex justify-between items-center">
|
|
666
|
+
<span>Selection: {formatTime(trimStart)} - {formatTime(trimEnd)}</span>
|
|
667
|
+
<span class="badge badge-neutral">{formatTime(trimEnd - trimStart)} duration</span>
|
|
668
|
+
</div>
|
|
669
|
+
</div>
|
|
670
|
+
|
|
671
|
+
<div class="flex gap-2">
|
|
672
|
+
<button class="btn btn-outline btn-sm" onclick={previewTrimSelection} disabled={isLoading}>
|
|
673
|
+
Preview
|
|
674
|
+
</button>
|
|
675
|
+
<button class="btn btn-primary btn-sm" onclick={applyTrim} disabled={isLoading || trimEnd <= trimStart}>
|
|
676
|
+
Apply Trim
|
|
677
|
+
</button>
|
|
678
|
+
<button class="btn btn-ghost btn-sm" onclick={cancelTrim}>
|
|
679
|
+
Cancel
|
|
680
|
+
</button>
|
|
681
|
+
</div>
|
|
682
|
+
|
|
683
|
+
<div class="text-xs text-base-content/60">
|
|
684
|
+
Drag the red lines to select the audio section you want to keep
|
|
685
|
+
</div>
|
|
686
|
+
</div>
|
|
687
|
+
{/if}
|
|
688
|
+
</div>
|
|
689
|
+
{/if}
|
|
690
|
+
</div>
|
|
691
|
+
|
|
692
|
+
<style>
|
|
693
|
+
.audio-waveform canvas { display: block; }
|
|
694
|
+
</style>
|