@joewinke/jatui 0.1.22 → 0.1.24

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