@arraypress/waveform-bar 1.1.0 → 1.2.1
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/README.md +146 -334
- package/dist/waveform-bar-icons.css +14 -5
- package/dist/waveform-bar.esm.js +6 -3
- package/dist/waveform-bar.js +6 -3
- package/dist/waveform-bar.min.js +1 -1
- package/package.json +1 -1
- package/src/css/waveform-bar-icons.css +14 -5
- package/src/js/core.js +83 -23
package/README.md
CHANGED
|
@@ -47,6 +47,9 @@ Think Spotify's bottom player — but lightweight, zero-config, and works on any
|
|
|
47
47
|
<!-- WaveformBar -->
|
|
48
48
|
<link rel="stylesheet" href="https://unpkg.com/@arraypress/waveform-bar@latest/dist/waveform-bar.css">
|
|
49
49
|
<script src="https://unpkg.com/@arraypress/waveform-bar@latest/dist/waveform-bar.js"></script>
|
|
50
|
+
|
|
51
|
+
<!-- Optional: Page icons (play/pause overlays, hearts, carts, etc.) -->
|
|
52
|
+
<link rel="stylesheet" href="https://unpkg.com/@arraypress/waveform-bar@latest/dist/waveform-bar-icons.css">
|
|
50
53
|
```
|
|
51
54
|
|
|
52
55
|
### NPM
|
|
@@ -130,21 +133,62 @@ Add to any element to queue a track without immediately playing it.
|
|
|
130
133
|
</button>
|
|
131
134
|
```
|
|
132
135
|
|
|
133
|
-
### Pre-Generated
|
|
136
|
+
### Pre-Generated Waveforms
|
|
137
|
+
|
|
138
|
+
Skip client-side audio analysis by providing pre-generated waveform data. The `data-wb-waveform` attribute accepts three
|
|
139
|
+
formats:
|
|
134
140
|
|
|
135
|
-
|
|
136
|
-
|
|
141
|
+
| Format | Example | Description |
|
|
142
|
+
|---------------|--------------------------|--------------------------------------------------|
|
|
143
|
+
| JSON file URL | `waveforms/song.json` | Fetches peaks from `{ peaks: [...] }` or `[...]` |
|
|
144
|
+
| Inline array | `[0.2, 0.37, 0.41, ...]` | JSON array string |
|
|
145
|
+
| CSV string | `0.2,0.37,0.41,...` | Comma-separated values |
|
|
137
146
|
|
|
138
147
|
```html
|
|
148
|
+
<!-- JSON file — fetched automatically by the player -->
|
|
149
|
+
<div data-wb-play
|
|
150
|
+
data-url="audio/song.mp3"
|
|
151
|
+
data-title="My Song"
|
|
152
|
+
data-wb-waveform="waveforms/song.json">
|
|
153
|
+
</div>
|
|
139
154
|
|
|
155
|
+
<!-- Inline peaks -->
|
|
140
156
|
<div data-wb-play
|
|
141
|
-
data-url="song.mp3"
|
|
157
|
+
data-url="audio/song.mp3"
|
|
142
158
|
data-title="My Song"
|
|
143
|
-
data-wb-waveform=
|
|
159
|
+
data-wb-waveform="[0.12,0.45,0.89,0.34,0.67]">
|
|
144
160
|
</div>
|
|
145
161
|
```
|
|
146
162
|
|
|
147
|
-
|
|
163
|
+
Generate JSON files with [WaveformGen](https://github.com/arraypress/waveform-gen):
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
npx @arraypress/waveform-gen ./audio/*.mp3 --output ./waveforms/ --bpm
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
JSON waveform files can also include markers, which are loaded automatically if none are set via data attributes:
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"peaks": [
|
|
174
|
+
0.12,
|
|
175
|
+
0.45,
|
|
176
|
+
0.89,
|
|
177
|
+
0.34,
|
|
178
|
+
...
|
|
179
|
+
],
|
|
180
|
+
"markers": [
|
|
181
|
+
{
|
|
182
|
+
"time": 0,
|
|
183
|
+
"label": "Intro"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"time": 30,
|
|
187
|
+
"label": "Chorus"
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
```
|
|
148
192
|
|
|
149
193
|
### DJ Mode Markers
|
|
150
194
|
|
|
@@ -166,32 +210,31 @@ and metadata at each marker boundary.
|
|
|
166
210
|
</div>
|
|
167
211
|
```
|
|
168
212
|
|
|
169
|
-
Markers can include: `time` (seconds, required), `label` (required), `title`, `artist`, `artwork`, `bpm`, `key
|
|
170
|
-
`color`.
|
|
213
|
+
Markers can include: `time` (seconds, required), `label` (required), `title`, `artist`, `artwork`, `bpm`, `key`,`color`.
|
|
171
214
|
|
|
172
215
|
The active marker on the waveform gently pulses to indicate the current section.
|
|
173
216
|
|
|
174
217
|
### Attribute Reference
|
|
175
218
|
|
|
176
|
-
| Attribute | Description
|
|
177
|
-
|
|
178
|
-
| `data-wb-play` | Makes element a play trigger (click to play)
|
|
179
|
-
| `data-wb-queue` | Makes element a queue trigger (click to add to queue)
|
|
180
|
-
| `data-url` | Audio file URL (required)
|
|
181
|
-
| `data-id` | Unique track identifier (defaults to URL)
|
|
182
|
-
| `data-title` | Track title
|
|
183
|
-
| `data-artist` | Artist or subtitle
|
|
184
|
-
| `data-artwork` | Album artwork URL
|
|
185
|
-
| `data-album` | Album name (for Media Session API)
|
|
186
|
-
| `data-link` | URL to navigate when clicking track info in the bar
|
|
187
|
-
| `data-duration` | Display duration string
|
|
188
|
-
| `data-bpm` | BPM value — displayed as a tag in the bar
|
|
189
|
-
| `data-key` | Musical key — displayed as a tag in the bar
|
|
190
|
-
| `data-meta` | JSON object of custom metadata (e.g. `'{"genre":"Trap"}'`)
|
|
191
|
-
| `data-wb-waveform` | Pre-generated waveform peaks JSON array
|
|
192
|
-
| `data-wb-markers` | JSON array of marker objects for DJ mode
|
|
193
|
-
| `data-wb-favorited` | `"true"` to pre-set favorite state (server-side seeding)
|
|
194
|
-
| `data-wb-in-cart` | `"true"` to pre-set cart state (server-side seeding)
|
|
219
|
+
| Attribute | Description |
|
|
220
|
+
|---------------------|---------------------------------------------------------------------------|
|
|
221
|
+
| `data-wb-play` | Makes element a play trigger (click to play) |
|
|
222
|
+
| `data-wb-queue` | Makes element a queue trigger (click to add to queue) |
|
|
223
|
+
| `data-url` | Audio file URL (required) |
|
|
224
|
+
| `data-id` | Unique track identifier (defaults to URL) |
|
|
225
|
+
| `data-title` | Track title |
|
|
226
|
+
| `data-artist` | Artist or subtitle |
|
|
227
|
+
| `data-artwork` | Album artwork URL |
|
|
228
|
+
| `data-album` | Album name (for Media Session API) |
|
|
229
|
+
| `data-link` | URL to navigate when clicking track info in the bar |
|
|
230
|
+
| `data-duration` | Display duration string |
|
|
231
|
+
| `data-bpm` | BPM value — displayed as a tag in the bar |
|
|
232
|
+
| `data-key` | Musical key — displayed as a tag in the bar |
|
|
233
|
+
| `data-meta` | JSON object of custom metadata (e.g. `'{"genre":"Trap"}'`) |
|
|
234
|
+
| `data-wb-waveform` | Pre-generated waveform peaks: JSON array, CSV string, or `.json` file URL |
|
|
235
|
+
| `data-wb-markers` | JSON array of marker objects for DJ mode |
|
|
236
|
+
| `data-wb-favorited` | `"true"` to pre-set favorite state (server-side seeding) |
|
|
237
|
+
| `data-wb-in-cart` | `"true"` to pre-set cart state (server-side seeding) |
|
|
195
238
|
|
|
196
239
|
All attributes also accept `data-wb-` prefixed versions (`data-wb-url`, `data-wb-title`, etc.) to avoid conflicts with
|
|
197
240
|
other libraries.
|
|
@@ -238,11 +281,11 @@ WaveformBar.init({
|
|
|
238
281
|
// Server-side actions (REST callbacks)
|
|
239
282
|
actions: {
|
|
240
283
|
favorite: {
|
|
241
|
-
endpoint: '/api/favorites',
|
|
284
|
+
endpoint: '/api/favorites',
|
|
242
285
|
method: 'POST'
|
|
243
286
|
},
|
|
244
287
|
cart: {
|
|
245
|
-
endpoint: '/api/cart',
|
|
288
|
+
endpoint: '/api/cart',
|
|
246
289
|
method: 'POST'
|
|
247
290
|
}
|
|
248
291
|
},
|
|
@@ -270,7 +313,6 @@ WaveformBar.init({
|
|
|
270
313
|
### Playback
|
|
271
314
|
|
|
272
315
|
```javascript
|
|
273
|
-
// Play a track object
|
|
274
316
|
WaveformBar.play({
|
|
275
317
|
url: 'audio/song.mp3',
|
|
276
318
|
title: 'My Song',
|
|
@@ -281,15 +323,12 @@ WaveformBar.play({
|
|
|
281
323
|
link: '/products/my-song'
|
|
282
324
|
});
|
|
283
325
|
|
|
284
|
-
// Play by URL shorthand
|
|
285
326
|
WaveformBar.play('audio/song.mp3');
|
|
286
|
-
|
|
287
|
-
// Playback controls
|
|
288
327
|
WaveformBar.togglePlay();
|
|
289
328
|
WaveformBar.pause();
|
|
290
|
-
WaveformBar.next();
|
|
291
|
-
WaveformBar.previous();
|
|
292
|
-
WaveformBar.skipTo(3);
|
|
329
|
+
WaveformBar.next();
|
|
330
|
+
WaveformBar.previous();
|
|
331
|
+
WaveformBar.skipTo(3);
|
|
293
332
|
```
|
|
294
333
|
|
|
295
334
|
### Repeat
|
|
@@ -299,102 +338,57 @@ WaveformBar.cycleRepeat(); // Cycles: off → all → one → off
|
|
|
299
338
|
WaveformBar.setRepeat('all'); // Set directly: 'off', 'all', 'one'
|
|
300
339
|
```
|
|
301
340
|
|
|
302
|
-
- **off** — Stops at end of queue
|
|
303
|
-
- **all** — Loops back to first track after last; prev/next wrap around
|
|
304
|
-
- **one** — Replays the current track indefinitely
|
|
305
|
-
|
|
306
341
|
### Markers / DJ Mode
|
|
307
342
|
|
|
308
343
|
```javascript
|
|
309
|
-
// Seek to marker by index (0-based) on current track
|
|
310
344
|
WaveformBar.seekToMarker(3);
|
|
311
|
-
|
|
312
|
-
// Seek to marker by label name on current track
|
|
313
345
|
WaveformBar.seekToMarkerByLabel('Horizon');
|
|
314
346
|
```
|
|
315
347
|
|
|
316
|
-
Both methods auto-play if the player is paused. They only work on the currently loaded track's markers — if the track
|
|
317
|
-
with markers isn't loaded, they silently return.
|
|
318
|
-
|
|
319
348
|
### Volume
|
|
320
349
|
|
|
321
350
|
```javascript
|
|
322
|
-
WaveformBar.setVolume(0.5);
|
|
323
|
-
WaveformBar.getVolume();
|
|
351
|
+
WaveformBar.setVolume(0.5);
|
|
352
|
+
WaveformBar.getVolume();
|
|
324
353
|
WaveformBar.toggleMute();
|
|
325
|
-
WaveformBar.isMutedState(); // true/false
|
|
326
354
|
```
|
|
327
355
|
|
|
328
|
-
Volume and mute state persist in localStorage across sessions.
|
|
329
|
-
|
|
330
356
|
### Queue
|
|
331
357
|
|
|
332
358
|
```javascript
|
|
333
|
-
// Add to end of queue
|
|
334
359
|
WaveformBar.addToQueue({url: 'track.mp3', title: 'Next Up', artist: 'Someone'});
|
|
335
|
-
|
|
336
|
-
// Remove by index
|
|
337
360
|
WaveformBar.removeFromQueue(2);
|
|
338
|
-
|
|
339
|
-
// Replay a previously played track (by URL)
|
|
340
|
-
WaveformBar.replay('audio/old-track.mp3');
|
|
341
|
-
|
|
342
|
-
// Clear upcoming tracks
|
|
343
361
|
WaveformBar.clearQueue();
|
|
344
|
-
|
|
345
|
-
// Clear play history
|
|
346
|
-
WaveformBar.clearHistory();
|
|
347
362
|
```
|
|
348
363
|
|
|
349
|
-
|
|
350
|
-
**. Tracks can be removed individually from the queue via the × button on hover.
|
|
351
|
-
|
|
352
|
-
### Favorites
|
|
364
|
+
### Favorites & Cart
|
|
353
365
|
|
|
354
366
|
```javascript
|
|
355
|
-
WaveformBar.toggleFavorite();
|
|
356
|
-
WaveformBar.isFavorited('beat-001');
|
|
367
|
+
WaveformBar.toggleFavorite();
|
|
368
|
+
WaveformBar.isFavorited('beat-001');
|
|
369
|
+
WaveformBar.addToCart();
|
|
370
|
+
WaveformBar.isInCart('beat-001');
|
|
357
371
|
```
|
|
358
372
|
|
|
359
|
-
Favorites persist in localStorage. Server-side state can be seeded via `data-wb-favorited="true"` on trigger elements —
|
|
360
|
-
this is authoritative and overrides localStorage on page load.
|
|
361
|
-
|
|
362
|
-
When a track is favorited/unfavorited, the `.wb-favorited` class is toggled on all matching trigger elements on the
|
|
363
|
-
page.
|
|
364
|
-
|
|
365
|
-
### Cart
|
|
366
|
-
|
|
367
|
-
```javascript
|
|
368
|
-
WaveformBar.addToCart(); // Add current track to cart
|
|
369
|
-
WaveformBar.isInCart('beat-001'); // Check by track ID
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
Cart state is NOT persisted to localStorage — it's server-authoritative. Seed from your server via
|
|
373
|
-
`data-wb-in-cart="true"`. The `.wb-in-cart` class is toggled on matching trigger elements.
|
|
374
|
-
|
|
375
|
-
If an `actions.cart.endpoint` is configured, a POST request fires automatically with the track data.
|
|
376
|
-
|
|
377
373
|
### State
|
|
378
374
|
|
|
379
375
|
```javascript
|
|
380
|
-
WaveformBar.getCurrentTrack();
|
|
381
|
-
WaveformBar.isCurrentlyPlaying('song.mp3');
|
|
382
|
-
WaveformBar.isCurrentTrack('song.mp3');
|
|
383
|
-
WaveformBar.getQueue();
|
|
384
|
-
WaveformBar.
|
|
385
|
-
WaveformBar.getPlayer();
|
|
376
|
+
WaveformBar.getCurrentTrack();
|
|
377
|
+
WaveformBar.isCurrentlyPlaying('song.mp3');
|
|
378
|
+
WaveformBar.isCurrentTrack('song.mp3');
|
|
379
|
+
WaveformBar.getQueue();
|
|
380
|
+
WaveformBar.getCurrentIndex();
|
|
381
|
+
WaveformBar.getPlayer();
|
|
386
382
|
```
|
|
387
383
|
|
|
388
384
|
### UI
|
|
389
385
|
|
|
390
386
|
```javascript
|
|
391
|
-
WaveformBar.show();
|
|
392
|
-
WaveformBar.hide();
|
|
393
|
-
WaveformBar.toggleQueuePanel();
|
|
394
|
-
WaveformBar.toggleVolumePopup();
|
|
395
|
-
WaveformBar.
|
|
396
|
-
WaveformBar.closeVolumePopup();
|
|
397
|
-
WaveformBar.destroy(); // Remove bar and clean up
|
|
387
|
+
WaveformBar.show();
|
|
388
|
+
WaveformBar.hide();
|
|
389
|
+
WaveformBar.toggleQueuePanel();
|
|
390
|
+
WaveformBar.toggleVolumePopup();
|
|
391
|
+
WaveformBar.destroy();
|
|
398
392
|
```
|
|
399
393
|
|
|
400
394
|
## DOM Events
|
|
@@ -405,10 +399,6 @@ All events bubble from the bar element and are prefixed with `waveformbar:`.
|
|
|
405
399
|
document.addEventListener('waveformbar:play', (e) => {
|
|
406
400
|
console.log('Playing:', e.detail.track);
|
|
407
401
|
});
|
|
408
|
-
|
|
409
|
-
document.addEventListener('waveformbar:trackchange', (e) => {
|
|
410
|
-
console.log('Track changed:', e.detail.track, 'Index:', e.detail.index);
|
|
411
|
-
});
|
|
412
402
|
```
|
|
413
403
|
|
|
414
404
|
| Event | Detail |
|
|
@@ -434,16 +424,10 @@ WaveformBar automatically adds CSS classes to trigger elements on the page:
|
|
|
434
424
|
| `.wb-favorited` | The element's track is favorited |
|
|
435
425
|
| `.wb-in-cart` | The element's track is in the cart |
|
|
436
426
|
|
|
437
|
-
These update in real-time as the user interacts with the bar.
|
|
438
|
-
|
|
439
427
|
## Helper CSS Classes
|
|
440
428
|
|
|
441
|
-
Ready-made utility classes for common UI patterns. Add them to your trigger elements.
|
|
442
|
-
|
|
443
429
|
### Icon Swap (`wb-icon-swap`)
|
|
444
430
|
|
|
445
|
-
Swaps play/pause content automatically based on playback state.
|
|
446
|
-
|
|
447
431
|
```html
|
|
448
432
|
|
|
449
433
|
<button data-wb-play data-url="song.mp3" class="wb-icon-swap">
|
|
@@ -454,100 +438,74 @@ Swaps play/pause content automatically based on playback state.
|
|
|
454
438
|
|
|
455
439
|
### Equalizer Bars (`wb-eq-bars`)
|
|
456
440
|
|
|
457
|
-
Animated equalizer bars that activate when the track is playing.
|
|
458
|
-
|
|
459
441
|
```html
|
|
460
|
-
|
|
461
|
-
<tr data-wb-play data-url="song.mp3">
|
|
462
|
-
<td>
|
|
463
|
-
<span class="wb-eq-bars">
|
|
464
|
-
<span></span><span></span><span></span><span></span>
|
|
465
|
-
</span>
|
|
466
|
-
</td>
|
|
467
|
-
<td>Track Title</td>
|
|
468
|
-
</tr>
|
|
442
|
+
<span class="wb-eq-bars"><span></span><span></span><span></span><span></span></span>
|
|
469
443
|
```
|
|
470
444
|
|
|
471
445
|
### Card Highlight (`wb-card-highlight`)
|
|
472
446
|
|
|
473
|
-
Adds accent border
|
|
474
|
-
|
|
475
|
-
```html
|
|
476
|
-
|
|
477
|
-
<div data-wb-play data-url="song.mp3"
|
|
478
|
-
class="wb-card-highlight"
|
|
479
|
-
style="border: 1px solid transparent; border-radius: 8px;">
|
|
480
|
-
<h3>Track Title</h3>
|
|
481
|
-
</div>
|
|
482
|
-
```
|
|
447
|
+
Adds accent border when the element's track is current.
|
|
483
448
|
|
|
484
449
|
### Accent Text (`wb-accent-current`)
|
|
485
450
|
|
|
486
451
|
Colors text with the accent color when the parent track is current.
|
|
487
452
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
<div data-wb-play data-url="song.mp3">
|
|
491
|
-
<span class="wb-accent-current">Track Title</span>
|
|
492
|
-
</div>
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
### Pulse Animation (`wb-pulse-playing`)
|
|
496
|
-
|
|
497
|
-
Subtle opacity pulse on playing elements.
|
|
453
|
+
### Favorite/Cart Visibility
|
|
498
454
|
|
|
499
455
|
```html
|
|
500
|
-
|
|
501
|
-
<
|
|
502
|
-
|
|
503
|
-
</
|
|
456
|
+
<span class="wb-hide-if-fav">♡ Save</span>
|
|
457
|
+
<span class="wb-show-if-fav">❤ Saved</span>
|
|
458
|
+
<span class="wb-hide-if-cart">🛒 Add</span>
|
|
459
|
+
<span class="wb-show-if-cart">✓ In Cart</span>
|
|
504
460
|
```
|
|
505
461
|
|
|
506
|
-
|
|
462
|
+
## Page Icons
|
|
507
463
|
|
|
508
|
-
|
|
464
|
+
The optional `waveform-bar-icons.css` provides a lightweight SVG icon set for use in your page elements (play/pause
|
|
465
|
+
overlays, hearts, carts, etc.):
|
|
509
466
|
|
|
510
467
|
```html
|
|
511
468
|
|
|
512
|
-
<
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
469
|
+
<link rel="stylesheet" href="https://unpkg.com/@arraypress/waveform-bar@latest/dist/waveform-bar-icons.css">
|
|
470
|
+
|
|
471
|
+
<span class="wbi wbi-play"></span>
|
|
472
|
+
<span class="wbi wbi-pause"></span>
|
|
473
|
+
<span class="wbi wbi-heart"></span>
|
|
474
|
+
<span class="wbi wbi-heart-filled"></span>
|
|
475
|
+
<span class="wbi wbi-cart"></span>
|
|
476
|
+
<span class="wbi wbi-cart-check"></span>
|
|
477
|
+
<span class="wbi wbi-queue"></span>
|
|
478
|
+
<span class="wbi wbi-skip-back"></span>
|
|
479
|
+
<span class="wbi wbi-skip-forward"></span>
|
|
480
|
+
<span class="wbi wbi-volume-high"></span>
|
|
481
|
+
<span class="wbi wbi-volume-low"></span>
|
|
482
|
+
<span class="wbi wbi-volume-mute"></span>
|
|
483
|
+
<span class="wbi wbi-repeat"></span>
|
|
484
|
+
<span class="wbi wbi-repeat-one"></span>
|
|
485
|
+
<span class="wbi wbi-close"></span>
|
|
486
|
+
<span class="wbi wbi-check"></span>
|
|
487
|
+
<span class="wbi wbi-link"></span>
|
|
488
|
+
<span class="wbi wbi-download"></span>
|
|
489
|
+
<span class="wbi wbi-share"></span>
|
|
490
|
+
<span class="wbi wbi-music-note"></span>
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
Icons inherit `color` from their parent and scale with `font-size`. Size variants: `.wbi-sm`, `.wbi-lg`, `.wbi-xl`,
|
|
494
|
+
`.wbi-2x`, `.wbi-3x`.
|
|
495
|
+
|
|
496
|
+
These are for your page elements only — the bar itself uses its own internal SVG icons and does not require this file.
|
|
523
497
|
|
|
524
498
|
## Persistence
|
|
525
499
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
**sessionStorage** (cleared when browser closes):
|
|
500
|
+
**sessionStorage** (cleared when browser closes): queue, current track index, playback position, playing state.
|
|
529
501
|
|
|
530
|
-
|
|
531
|
-
- Current track index
|
|
532
|
-
- Playback position
|
|
533
|
-
- Playing state
|
|
534
|
-
|
|
535
|
-
**localStorage** (persists across sessions):
|
|
536
|
-
|
|
537
|
-
- Volume level
|
|
538
|
-
- Mute state
|
|
539
|
-
- Favorite track IDs
|
|
540
|
-
|
|
541
|
-
When a user navigates between pages, the bar restores the queue, seeks to the saved position, and optionally resumes
|
|
542
|
-
playback (if `autoResume: true`). Position is saved on every page unload for accuracy.
|
|
502
|
+
**localStorage** (persists across sessions): volume level, mute state, favorite track IDs.
|
|
543
503
|
|
|
544
504
|
Cart state is intentionally NOT persisted — it seeds from `data-wb-in-cart` attributes on page load, making the server
|
|
545
505
|
the source of truth.
|
|
546
506
|
|
|
547
507
|
## Custom Styling
|
|
548
508
|
|
|
549
|
-
Override CSS custom properties to theme the bar:
|
|
550
|
-
|
|
551
509
|
```css
|
|
552
510
|
.waveform-bar,
|
|
553
511
|
.wb-queue-panel {
|
|
@@ -555,178 +513,32 @@ Override CSS custom properties to theme the bar:
|
|
|
555
513
|
--wb-border: rgba(255, 255, 255, 0.1);
|
|
556
514
|
--wb-text: #ffffff;
|
|
557
515
|
--wb-text-muted: rgba(255, 255, 255, 0.5);
|
|
558
|
-
--wb-accent: #1db954;
|
|
516
|
+
--wb-accent: #1db954;
|
|
559
517
|
--wb-accent-light: #1ed760;
|
|
560
518
|
--wb-hover: rgba(255, 255, 255, 0.08);
|
|
561
|
-
--wb-tag-bg: rgba(29, 185, 84, 0.12);
|
|
562
|
-
--wb-tag-text: #1db954;
|
|
563
519
|
--wb-fav-color: #ef4444;
|
|
564
520
|
--wb-cart-color: #4ade80;
|
|
565
521
|
}
|
|
566
522
|
```
|
|
567
523
|
|
|
568
|
-
A `.wb-light` class is available for light backgrounds:
|
|
569
|
-
|
|
570
|
-
```css
|
|
571
|
-
.waveform-bar.wb-light {
|
|
572
|
-
--wb-bg: rgba(255, 255, 255, 0.95);
|
|
573
|
-
--wb-text: #1a1a1a;
|
|
574
|
-
--wb-text-muted: rgba(0, 0, 0, 0.5);
|
|
575
|
-
}
|
|
576
|
-
```
|
|
577
|
-
|
|
578
|
-
## Layout
|
|
579
|
-
|
|
580
|
-
The bar uses a three-zone flex layout:
|
|
581
|
-
|
|
582
|
-
```
|
|
583
|
-
[Left: controls + track info] — [Centre: waveform + time] — [Right: meta + actions + volume + queue]
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
- **Left zone** — Prev, Play/Pause, Next, Repeat buttons + artwork + title/artist
|
|
587
|
-
- **Centre zone** — Full-width waveform visualization + elapsed/total time
|
|
588
|
-
- **Right zone** — BPM/key tags, favorite/cart buttons, volume popup, queue toggle
|
|
589
|
-
|
|
590
|
-
On mobile (≤768px):
|
|
591
|
-
|
|
592
|
-
- Left + Right share the top row
|
|
593
|
-
- Waveform drops to a full-width second row
|
|
594
|
-
- Meta tags, actions, and time display are hidden
|
|
595
|
-
- Queue panel becomes full-width
|
|
596
|
-
|
|
597
|
-
On small mobile (≤480px):
|
|
598
|
-
|
|
599
|
-
- Prev/Next and Repeat buttons are hidden
|
|
600
|
-
- Volume is hidden
|
|
601
|
-
- Only play button, track info, and waveform remain
|
|
602
|
-
|
|
603
|
-
## Use Cases
|
|
604
|
-
|
|
605
|
-
### Beat Store
|
|
606
|
-
|
|
607
|
-
```html
|
|
608
|
-
|
|
609
|
-
<div class="beat-card wb-card-highlight"
|
|
610
|
-
data-wb-play
|
|
611
|
-
data-url="beats/trap-42.mp3"
|
|
612
|
-
data-id="beat-42"
|
|
613
|
-
data-title="Trap Beat #42"
|
|
614
|
-
data-artist="ProducerName"
|
|
615
|
-
data-bpm="140"
|
|
616
|
-
data-key="Cm"
|
|
617
|
-
data-artwork="covers/trap-42.jpg"
|
|
618
|
-
data-link="/beats/trap-42"
|
|
619
|
-
data-wb-favorited="true">
|
|
620
|
-
<img src="covers/trap-42.jpg">
|
|
621
|
-
<h3 class="wb-accent-current">Trap Beat #42</h3>
|
|
622
|
-
<p>140 BPM · Cm · $29.99</p>
|
|
623
|
-
<span class="wb-eq-bars"><span></span><span></span><span></span><span></span></span>
|
|
624
|
-
<button>
|
|
625
|
-
<span class="wb-hide-if-fav">♡ Save</span>
|
|
626
|
-
<span class="wb-show-if-fav">❤ Saved</span>
|
|
627
|
-
</button>
|
|
628
|
-
</div>
|
|
629
|
-
<button data-wb-queue data-url="beats/trap-42.mp3" data-title="Trap Beat #42">+ Queue</button>
|
|
630
|
-
```
|
|
631
|
-
|
|
632
|
-
### Sample Library Table
|
|
633
|
-
|
|
634
|
-
```html
|
|
635
|
-
|
|
636
|
-
<table>
|
|
637
|
-
<tr data-wb-play
|
|
638
|
-
data-url="samples/kick.wav"
|
|
639
|
-
data-title="808 Kick"
|
|
640
|
-
data-artist="Drum Kit Vol. 3"
|
|
641
|
-
data-bpm="128"
|
|
642
|
-
data-meta='{"format":"WAV"}'>
|
|
643
|
-
<td><span class="wb-eq-bars"><span></span><span></span><span></span><span></span></span></td>
|
|
644
|
-
<td class="wb-accent-current">808 Kick</td>
|
|
645
|
-
<td>128 BPM</td>
|
|
646
|
-
<td>WAV</td>
|
|
647
|
-
</tr>
|
|
648
|
-
</table>
|
|
649
|
-
```
|
|
650
|
-
|
|
651
|
-
### DJ Mix with Clickable Tracklist
|
|
652
|
-
|
|
653
|
-
```html
|
|
654
|
-
<!-- Mix trigger with markers -->
|
|
655
|
-
<div data-wb-play
|
|
656
|
-
data-url="mixes/guestmix.mp3"
|
|
657
|
-
data-title="Guest Mix"
|
|
658
|
-
data-artist="Various Artists"
|
|
659
|
-
data-wb-waveform='[0.1,0.3,0.5,...]'
|
|
660
|
-
data-wb-markers='[
|
|
661
|
-
{"time":0,"label":"Intro","title":"Opening","artist":"DJ One"},
|
|
662
|
-
{"time":180,"label":"Drop","title":"Big Tune","artist":"Producer X"},
|
|
663
|
-
{"time":360,"label":"Chill","title":"Wind Down","artist":"Ambient Co"}
|
|
664
|
-
]'>
|
|
665
|
-
🎧 Play Guest Mix
|
|
666
|
-
</div>
|
|
667
|
-
|
|
668
|
-
<!-- Clickable tracklist buttons -->
|
|
669
|
-
<button onclick="WaveformBar.seekToMarkerByLabel('Intro')">00:00 — DJ One — Opening</button>
|
|
670
|
-
<button onclick="WaveformBar.seekToMarkerByLabel('Drop')">03:00 — Producer X — Big Tune</button>
|
|
671
|
-
<button onclick="WaveformBar.seekToMarkerByLabel('Chill')">06:00 — Ambient Co — Wind Down</button>
|
|
672
|
-
```
|
|
673
|
-
|
|
674
|
-
### Podcast
|
|
675
|
-
|
|
676
|
-
```html
|
|
677
|
-
|
|
678
|
-
<button data-wb-play
|
|
679
|
-
data-url="episodes/ep42.mp3"
|
|
680
|
-
data-title="Ep 42: The Future of AI"
|
|
681
|
-
data-artist="with Dr. Sarah Chen"
|
|
682
|
-
data-artwork="ep42-cover.jpg"
|
|
683
|
-
data-link="/episodes/42"
|
|
684
|
-
class="wb-icon-swap">
|
|
685
|
-
<span class="wb-show-play">▶ Listen</span>
|
|
686
|
-
<span class="wb-show-pause">⏸ Playing</span>
|
|
687
|
-
</button>
|
|
688
|
-
```
|
|
689
|
-
|
|
690
|
-
## Pre-Generating Waveforms
|
|
691
|
-
|
|
692
|
-
Use [@arraypress/waveform-gen](https://github.com/arraypress/waveform-gen) to batch-generate waveform data:
|
|
693
|
-
|
|
694
|
-
```bash
|
|
695
|
-
# Generate JSON files for all audio
|
|
696
|
-
npx @arraypress/waveform-gen ./audio/*.mp3 --output ./waveforms/
|
|
697
|
-
|
|
698
|
-
# Generate ready-to-paste HTML markup
|
|
699
|
-
npx @arraypress/waveform-gen ./audio/*.mp3 --format html --output ./
|
|
700
|
-
```
|
|
701
|
-
|
|
702
|
-
Then reference the data in your HTML:
|
|
703
|
-
|
|
704
|
-
```html
|
|
705
|
-
|
|
706
|
-
<div data-wb-play
|
|
707
|
-
data-url="song.mp3"
|
|
708
|
-
data-title="My Song"
|
|
709
|
-
data-wb-waveform='[0.12,0.45,0.89,0.34,0.67]'>
|
|
710
|
-
</div>
|
|
711
|
-
```
|
|
712
|
-
|
|
713
524
|
## Browser Support
|
|
714
525
|
|
|
715
|
-
|
|
716
|
-
- Firefox 88+
|
|
717
|
-
- Safari 14+
|
|
718
|
-
- Mobile browsers (iOS Safari, Chrome Android)
|
|
526
|
+
Chrome/Edge 90+, Firefox 88+, Safari 14+, iOS Safari, Chrome Android
|
|
719
527
|
|
|
720
528
|
## Dependencies
|
|
721
529
|
|
|
722
|
-
- [WaveformPlayer](https://github.com/arraypress/waveform-player) ≥1.
|
|
530
|
+
- [WaveformPlayer](https://github.com/arraypress/waveform-player) ≥1.5.0 — must be loaded before WaveformBar
|
|
723
531
|
|
|
724
|
-
##
|
|
532
|
+
## Ecosystem
|
|
725
533
|
|
|
726
|
-
|
|
534
|
+
| Package | Description |
|
|
535
|
+
|-------------------------------------------------------------------------|-------------------------------------------------------------------|
|
|
536
|
+
| **[WaveformPlayer](https://github.com/arraypress/waveform-player)** | Core audio player with waveform visualization |
|
|
537
|
+
| **[WaveformBar](https://github.com/arraypress/waveform-bar)** | Persistent bottom-bar player with queue, favorites, cart, DJ mode |
|
|
538
|
+
| **[WaveformGen](https://github.com/arraypress/waveform-generator)** | CLI tool to pre-generate waveform JSON from audio files |
|
|
539
|
+
| **[WaveformPlaylist](https://github.com/arraypress/waveform-playlist)** | Playlist and chapter support addon |
|
|
540
|
+
| **[WaveformTracker](https://github.com/arraypress/waveform-tracker)** | Audio engagement analytics |
|
|
727
541
|
|
|
728
|
-
##
|
|
542
|
+
## License
|
|
729
543
|
|
|
730
|
-
|
|
731
|
-
- [WaveformPlaylist](https://github.com/arraypress/waveform-playlist) — On-page playlist and chapter navigation
|
|
732
|
-
- [WaveformGen](https://github.com/arraypress/waveform-generator) — CLI tool for batch waveform data generation
|
|
544
|
+
MIT © [ArrayPress](https://github.com/arraypress)
|
|
@@ -5,12 +5,21 @@
|
|
|
5
5
|
* MIT License
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
|
-
*
|
|
8
|
+
* <span class="wbi wbi-play"></span>
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
.wbi
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
.wbi {
|
|
12
|
+
display: inline-block;
|
|
13
|
+
width: 1em;
|
|
14
|
+
height: 1em;
|
|
15
|
+
background-color: currentColor;
|
|
16
|
+
-webkit-mask-size: contain;
|
|
17
|
+
mask-size: contain;
|
|
18
|
+
-webkit-mask-repeat: no-repeat;
|
|
19
|
+
mask-repeat: no-repeat;
|
|
20
|
+
-webkit-mask-position: center;
|
|
21
|
+
mask-position: center;
|
|
22
|
+
vertical-align: -0.125em;
|
|
14
23
|
}
|
|
15
24
|
|
|
16
25
|
.wbi-sm {
|
|
@@ -136,4 +145,4 @@
|
|
|
136
145
|
.wbi-share {
|
|
137
146
|
-webkit-mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'><path d='M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z'/></svg>");
|
|
138
147
|
mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'><path d='M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z'/></svg>");
|
|
139
|
-
}
|
|
148
|
+
}
|
package/dist/waveform-bar.esm.js
CHANGED
|
@@ -971,7 +971,9 @@ var WaveformBar = class {
|
|
|
971
971
|
this._updateTrackDisplay(track);
|
|
972
972
|
this._updateFavoriteUI();
|
|
973
973
|
const loadOpts = { artwork: track.artwork, album: track.album };
|
|
974
|
-
if (track.waveform)
|
|
974
|
+
if (track.waveform) {
|
|
975
|
+
loadOpts.waveform = track.waveform;
|
|
976
|
+
}
|
|
975
977
|
if (track.markers && track.markers.length) {
|
|
976
978
|
const defaultColor = this.config.markerColor;
|
|
977
979
|
loadOpts.markers = track.markers.map((m) => ({
|
|
@@ -1309,7 +1311,9 @@ var WaveformBar = class {
|
|
|
1309
1311
|
this._updateTrackDisplay(track);
|
|
1310
1312
|
this._updateFavoriteUI();
|
|
1311
1313
|
this._updateNavButtons();
|
|
1312
|
-
if (track.waveform)
|
|
1314
|
+
if (track.waveform) {
|
|
1315
|
+
this.player.options.waveform = track.waveform;
|
|
1316
|
+
}
|
|
1313
1317
|
this.player.options.title = track.title || "";
|
|
1314
1318
|
this.player.options.subtitle = track.artist || "";
|
|
1315
1319
|
if (track.markers && track.markers.length) {
|
|
@@ -1326,7 +1330,6 @@ var WaveformBar = class {
|
|
|
1326
1330
|
this._currentMarkerIndex = -1;
|
|
1327
1331
|
this.player.load(track.url).then(() => {
|
|
1328
1332
|
if (this.player) this.player.setVolume(this.isMuted ? 0 : this.volume);
|
|
1329
|
-
console.log("RESTORE: position =", state.position, "duration =", this.player?.audio?.duration);
|
|
1330
1333
|
if (state.isPlaying && this.config.autoResume) {
|
|
1331
1334
|
try {
|
|
1332
1335
|
const p = this.player.play();
|
package/dist/waveform-bar.js
CHANGED
|
@@ -972,7 +972,9 @@
|
|
|
972
972
|
this._updateTrackDisplay(track);
|
|
973
973
|
this._updateFavoriteUI();
|
|
974
974
|
const loadOpts = { artwork: track.artwork, album: track.album };
|
|
975
|
-
if (track.waveform)
|
|
975
|
+
if (track.waveform) {
|
|
976
|
+
loadOpts.waveform = track.waveform;
|
|
977
|
+
}
|
|
976
978
|
if (track.markers && track.markers.length) {
|
|
977
979
|
const defaultColor = this.config.markerColor;
|
|
978
980
|
loadOpts.markers = track.markers.map((m) => ({
|
|
@@ -1310,7 +1312,9 @@
|
|
|
1310
1312
|
this._updateTrackDisplay(track);
|
|
1311
1313
|
this._updateFavoriteUI();
|
|
1312
1314
|
this._updateNavButtons();
|
|
1313
|
-
if (track.waveform)
|
|
1315
|
+
if (track.waveform) {
|
|
1316
|
+
this.player.options.waveform = track.waveform;
|
|
1317
|
+
}
|
|
1314
1318
|
this.player.options.title = track.title || "";
|
|
1315
1319
|
this.player.options.subtitle = track.artist || "";
|
|
1316
1320
|
if (track.markers && track.markers.length) {
|
|
@@ -1327,7 +1331,6 @@
|
|
|
1327
1331
|
this._currentMarkerIndex = -1;
|
|
1328
1332
|
this.player.load(track.url).then(() => {
|
|
1329
1333
|
if (this.player) this.player.setVolume(this.isMuted ? 0 : this.volume);
|
|
1330
|
-
console.log("RESTORE: position =", state.position, "duration =", this.player?.audio?.duration);
|
|
1331
1334
|
if (state.isPlaying && this.config.autoResume) {
|
|
1332
1335
|
try {
|
|
1333
1336
|
const p = this.player.play();
|
package/dist/waveform-bar.min.js
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
<div class="wb-queue-item-title">${u(h.title)}</div>
|
|
42
42
|
<div class="wb-queue-item-artist">${u(h.artist)}</div>
|
|
43
43
|
</div>
|
|
44
|
-
</div>`}}s.innerHTML=l,s.querySelectorAll(".wb-queue-item[data-qi]").forEach(o=>{o.addEventListener("click",h=>{h.target.closest(".wb-queue-remove")||r.onSkipTo&&r.onSkipTo(parseInt(o.dataset.qi))})}),s.querySelectorAll(".wb-queue-remove").forEach(o=>{o.addEventListener("click",h=>{h.stopPropagation(),r.onRemove&&r.onRemove(parseInt(o.dataset.qi))})})}var x={persist:!0,autoResume:!0,continuous:!0,repeat:"off",showRepeat:!0,showQueue:!0,showPrevNext:!0,showVolume:!0,showMute:!0,showMeta:!0,showTime:!0,showTrackLink:!0,maxMeta:3,defaultArtwork:null,theme:null,waveformStyle:"mirror",waveformHeight:32,barWidth:2,barSpacing:0,waveformColor:null,progressColor:null,markerColor:"rgba(255, 255, 255, 0.25)",volume:1,storageKey:"waveform-bar",actions:null,onPlay:null,onPause:null,onTrackChange:null,onQueueChange:null,onVolumeChange:null,onFavorite:null,onCart:null},f=class{constructor(){this.config=null,this.player=null,this.queue=[],this.currentIndex=-1,this.isPlaying=!1,this.isInitialized=!1,this.queueOpen=!1,this.volume=1,this.isMuted=!1,this._volumeBeforeMute=1,this._lastPosition=0,this._favorites=new Set,this._cartItems=new Set,this._observer=null,this._activeMarkers=null,this._currentMarkerIndex=-1,this.repeat="off",this.barEl=null,this.queueEl=null,this.waveformContainer=null,this.volumePopupEl=null,this.titleEl=null,this.artistEl=null,this.metaEl=null,this.playBtnEl=null,this.repeatBtnEl=null,this.queueBtnEl=null,this.queueBodyEl=null,this.queueCountEl=null,this.volumeSliderEl=null,this.muteBtnEl=null,this.favBtnEl=null,this.cartBtnEl=null,this.timeCurrentEl=null,this.timeTotalEl=null}init(t={}){return this.isInitialized&&this.destroy(),this.config={...x,...t},this.volume=this.config.volume,typeof window.WaveformPlayer>"u"?(console.error("WaveformBar: WaveformPlayer is required."),this):(this._createBar(),this._createQueue(),this._initPlayer(),this._bindTriggers(),this._observeDOM(),this.config.persist&&(this._restoreVolume(),this._restoreFavorites()),this._seedFromAttributes(),this.config.persist&&this._restoreState(),this.isInitialized=!0,this._beforeUnloadHandler=()=>this._saveState(),window.addEventListener("beforeunload",this._beforeUnloadHandler),this)}destroy(){return this.player&&(this.player.destroy(),this.player=null),this.barEl&&(this.barEl.remove(),this.barEl=null),this.queueEl&&(this.queueEl.remove(),this.queueEl=null),this._observer&&(this._observer.disconnect(),this._observer=null),this._beforeUnloadHandler&&(window.removeEventListener("beforeunload",this._beforeUnloadHandler),this._beforeUnloadHandler=null),document.querySelectorAll("[data-wb-play],[data-wb-queue]").forEach(t=>delete t._wbBound),document.querySelectorAll(".wb-current,.wb-playing").forEach(t=>t.classList.remove("wb-current","wb-playing")),this.queue=[],this.currentIndex=-1,this.isPlaying=!1,this.queueOpen=!1,this.isInitialized=!1,this.config=null,this}_createBar(){this.barEl=document.createElement("div"),this.barEl.className="waveform-bar";let t=this.config.theme||this._detectTheme();t==="light"&&this.barEl.classList.add("wb-light"),this._resolvedTheme=t,this.barEl.id="waveform-bar",this.barEl.innerHTML=k(this.config),document.body.appendChild(this.barEl),this.titleEl=this.barEl.querySelector(".wb-title"),this.artistEl=this.barEl.querySelector(".wb-artist"),this.metaEl=this.barEl.querySelector(".wb-meta"),this.playBtnEl=this.barEl.querySelector(".wb-play"),this.waveformContainer=this.barEl.querySelector(".wb-waveform-container"),this.queueBtnEl=this.barEl.querySelector(".wb-queue-btn"),this.muteBtnEl=this.barEl.querySelector(".wb-mute"),this.volumeSliderEl=this.barEl.querySelector(".wb-volume-slider"),this.favBtnEl=this.barEl.querySelector(".wb-fav"),this.cartBtnEl=this.barEl.querySelector(".wb-cart"),this.timeCurrentEl=this.barEl.querySelector(".wb-time-current"),this.timeTotalEl=this.barEl.querySelector(".wb-time-total"),this.playBtnEl.addEventListener("click",()=>this.togglePlay());let e=this.barEl.querySelector(".wb-prev"),i=this.barEl.querySelector(".wb-next");e&&e.addEventListener("click",()=>this.previous()),i&&i.addEventListener("click",()=>this.next()),this.repeatBtnEl=this.barEl.querySelector(".wb-repeat"),this.repeatBtnEl&&(this.repeat=this.config.repeat||"off",this._updateRepeatButton(),this.repeatBtnEl.addEventListener("click",()=>this.cycleRepeat())),this.queueBtnEl&&this.queueBtnEl.addEventListener("click",()=>this.toggleQueuePanel()),this.volumePopupEl=this.barEl.querySelector(".wb-volume-popup");let r=this.barEl.querySelector(".wb-volume");if(this.muteBtnEl&&this.muteBtnEl.addEventListener("click",a=>{a.stopPropagation(),this.toggleMute()}),r&&this.volumePopupEl){let a;r.addEventListener("mouseenter",()=>{clearTimeout(a),this.openVolumePopup()}),r.addEventListener("mouseleave",()=>{a=setTimeout(()=>this.closeVolumePopup(),300)})}this.volumeSliderEl&&this.volumeSliderEl.addEventListener("input",a=>{a.stopPropagation(),this.setVolume(parseInt(a.target.value)/100)}),document.addEventListener("click",a=>{this.volumePopupEl?.classList.contains("wb-volume-open")&&!this.barEl.querySelector(".wb-volume")?.contains(a.target)&&this.closeVolumePopup()}),this.favBtnEl&&this.favBtnEl.addEventListener("click",()=>this.toggleFavorite()),this.cartBtnEl&&this.cartBtnEl.addEventListener("click",()=>this.addToCart()),this.config.showTrackLink&&this.barEl.querySelector(".wb-track").addEventListener("click",()=>{let a=this.getCurrentTrack();a&&a.link&&(window.location.href=a.link)})}_createQueue(){this.config.showQueue&&(this.queueEl=q(),this._resolvedTheme==="light"&&this.queueEl.classList.add("wb-light"),document.body.appendChild(this.queueEl),this.queueBodyEl=this.queueEl.querySelector(".wb-queue-body"),this.queueCountEl=this.queueEl.querySelector(".wb-queue-count"),this.queueEl.querySelector(".wb-queue-clear").addEventListener("click",()=>this.clearQueue()),document.addEventListener("click",t=>{this.queueOpen&&!this.queueEl.contains(t.target)&&!this.queueBtnEl.contains(t.target)&&this.closeQueuePanel()}))}_initPlayer(){let t={showControls:!1,showInfo:!1,waveformStyle:this.config.waveformStyle,height:this.config.waveformHeight,barWidth:this.config.barWidth,barSpacing:this.config.barSpacing,singlePlay:!1,onPlay:()=>{this.isPlaying=!0,this._updatePlayButton(),this._syncPageState();let e=this.getCurrentTrack();this._emit("play",{track:e}),this.config.onPlay&&this.config.onPlay(e)},onPause:()=>{this.isPlaying=!1,this._updatePlayButton(),this._syncPageState(),this._saveState();let e=this.getCurrentTrack();this._emit("pause",{track:e}),this.config.onPause&&this.config.onPause(e)},onEnd:()=>{if(this.isPlaying=!1,this._updatePlayButton(),this._syncPageState(),this.timeCurrentEl&&(this.timeCurrentEl.textContent="0:00"),this.repeat==="one"){this.player&&(this.player.seekTo(0),this.player.play().catch(()=>{}));return}this.config.continuous&&this.currentIndex<this.queue.length-1?(this.currentIndex++,this._loadCurrentTrack()):this.repeat==="all"&&this.queue.length>0&&(this.currentIndex=0,this._loadCurrentTrack())},onTimeUpdate:(e,i)=>{this._lastPosition=e,this.timeCurrentEl&&(this.timeCurrentEl.textContent=v(e)),this.timeTotalEl&&(this.timeTotalEl.textContent=v(i)),(!this._lastSaveTime||e-this._lastSaveTime>2)&&(this._lastSaveTime=e,this._saveState()),this._activeMarkers&&this._checkMarkerBoundary(e)},onLoad:null};this.config.waveformColor&&(t.waveformColor=this.config.waveformColor),this.config.progressColor&&(t.progressColor=this.config.progressColor),this.player=new window.WaveformPlayer(this.waveformContainer,t),this.player.setVolume(this.volume)}_bindTriggers(){document.querySelectorAll("[data-wb-play]").forEach(t=>{t._wbBound||(t._wbBound=!0,t.addEventListener("click",e=>{e.preventDefault();let i=m(t);i&&this.play(i)}))}),document.querySelectorAll("[data-wb-queue]").forEach(t=>{t._wbBound||(t._wbBound=!0,t.addEventListener("click",e=>{e.preventDefault(),e.stopPropagation();let i=m(t);i&&this.addToQueue(i)}))})}_observeDOM(){typeof MutationObserver>"u"||(this._observer=new MutationObserver(()=>{this._bindTriggers(),this._syncPageState()}),this._observer.observe(document.body,{childList:!0,subtree:!0}))}play(t){let e=typeof t=="string"?{url:t,id:t,title:d(t)}:t;if(!e||!e.url)return this;let i=this.getCurrentTrack();if(i&&i.url===e.url)return this.togglePlay(),this;let r=this.queue.findIndex(a=>a.url===e.url);if(r>=0)this.queue[r]={...this.queue[r],...e},this.currentIndex=r;else{let a=this.currentIndex+1;this.queue.splice(a,0,e),this.currentIndex=a}return this._loadCurrentTrack(),this}addToQueue(t){let e=typeof t=="string"?{url:t,id:t,title:d(t)}:t;return!e||!e.url?this:this.queue.find(i=>i.url===e.url)?this:(this.queue.push(e),this._renderQueue(),this._saveState(),this._updateNavButtons(),this.currentIndex===-1&&(this.currentIndex=0,this._loadCurrentTrack()),this.config.onQueueChange&&this.config.onQueueChange(this.queue,this.currentIndex),this)}togglePlay(){return this.player?(this.isPlaying?this.player.pause():this.player.play(),this):this}pause(){return this.player&&this.isPlaying&&this.player.pause(),this}next(){return this.currentIndex<this.queue.length-1?(this.currentIndex++,this._loadCurrentTrack()):this.repeat==="all"&&this.queue.length>0&&(this.currentIndex=0,this._loadCurrentTrack()),this}previous(){return this.player&&this.player.audio&&this.player.audio.currentTime>3?(this.player.seekTo(0),this):(this.currentIndex>0?(this.currentIndex--,this._loadCurrentTrack()):this.repeat==="all"&&this.queue.length>0&&(this.currentIndex=this.queue.length-1,this._loadCurrentTrack()),this)}skipTo(t){return t<0||t>=this.queue.length?this:t===this.currentIndex?(this.togglePlay(),this):(this.currentIndex=t,this._loadCurrentTrack(),this)}seekToMarker(t){if(!this._activeMarkers||t<0||t>=this._activeMarkers.length)return this;let e=this._activeMarkers[t];return this.player&&(this.player.seekTo(e.time),this.isPlaying||this.togglePlay()),this}seekToMarkerByLabel(t){if(!this._activeMarkers)return this;let e=this._activeMarkers.findIndex(i=>(i.label||i.title||"").toLowerCase()===t.toLowerCase());return e>=0&&this.seekToMarker(e),this}setVolume(t){return this.volume=Math.max(0,Math.min(1,t)),this.isMuted=this.volume===0,this.player&&this.player.setVolume(this.volume),this._updateVolumeUI(),y(this.config.storageKey,this.volume,this.isMuted,this._volumeBeforeMute),this._emit("volumechange",{volume:this.volume}),this.config.onVolumeChange&&this.config.onVolumeChange(this.volume),this}getVolume(){return this.volume}toggleMute(){return this.isMuted?this.setVolume(this._volumeBeforeMute||1):(this._volumeBeforeMute=this.volume,this.isMuted=!0,this.player&&this.player.setVolume(0),this._updateVolumeUI()),this}toggleFavorite(){let t=this.getCurrentTrack();if(!t)return this;let e=t.id||t.url,i=this._favorites.has(e);return i?this._favorites.delete(e):this._favorites.add(e),this._updateFavoriteUI(),this._syncFavoriteAttributes(t.url,!i),p(this.config.storageKey,this._favorites),this._emit("favorite",{track:t,favorited:!i}),this.config.onFavorite&&this.config.onFavorite(t,!i),this.config.actions?.favorite&&b(this.config.actions.favorite,{action:"favorite",id:e,url:t.url,title:t.title,favorited:!i}),this}addToCart(){let t=this.getCurrentTrack();if(!t)return this;let e=t.id||t.url;return this._cartItems.add(e),this.cartBtnEl&&(this.cartBtnEl.classList.add("wb-action-done"),setTimeout(()=>this.cartBtnEl.classList.remove("wb-action-done"),1500)),this._syncCartAttributes(t.url,!0),this._emit("cart",{track:t}),this.config.onCart&&this.config.onCart(t),this.config.actions?.cart&&b(this.config.actions.cart,{action:"cart",id:e,url:t.url,title:t.title}),this}isFavorited(t){if(!t){let e=this.getCurrentTrack();t=e?e.id||e.url:null}return t?this._favorites.has(t):!1}isInCart(t){if(!t){let e=this.getCurrentTrack();t=e?e.id||e.url:null}return t?this._cartItems.has(t):!1}removeFromQueue(t){return t<0||t>=this.queue.length||t===this.currentIndex?this:(this.queue.splice(t,1),t<this.currentIndex&&this.currentIndex--,this._renderQueue(),this._saveState(),this._updateNavButtons(),this._emit("queuechange",{queue:this.queue,currentIndex:this.currentIndex}),this.config.onQueueChange&&this.config.onQueueChange(this.queue,this.currentIndex),this)}clearQueue(){let t=this.getCurrentTrack();return this.queue=t?[t]:[],this.currentIndex=t?0:-1,this._renderQueue(),this._saveState(),this._updateNavButtons(),this._emit("queuechange",{queue:this.queue,currentIndex:this.currentIndex}),this.config.onQueueChange&&this.config.onQueueChange(this.queue,this.currentIndex),this}getCurrentTrack(){return this.currentIndex>=0&&this.currentIndex<this.queue.length?this.queue[this.currentIndex]:null}getQueue(){return[...this.queue]}getCurrentIndex(){return this.currentIndex}isCurrentlyPlaying(t){let e=this.getCurrentTrack();return this.isPlaying&&e&&e.url===t}isCurrentTrack(t){let e=this.getCurrentTrack();return e&&e.url===t}getPlayer(){return this.player}_emit(t,e={}){this.barEl&&this.barEl.dispatchEvent(new CustomEvent("waveformbar:"+t,{bubbles:!0,detail:e}))}show(){return this.barEl&&this.barEl.classList.add("wb-active"),this}hide(){return this.barEl&&this.barEl.classList.remove("wb-active"),this.closeQueuePanel(),this.closeVolumePopup(),this}toggleQueuePanel(){return this.queueOpen?this.closeQueuePanel():this.openQueuePanel()}openQueuePanel(){if(!this.queueEl)return this;if(this.queueOpen=!0,this.closeVolumePopup(),this.queueBtnEl){let t=this.queueBtnEl.getBoundingClientRect();this.queueEl.style.right=window.innerWidth-t.right+"px"}return this.queueEl.classList.add("wb-queue-open"),this.queueBtnEl&&this.queueBtnEl.classList.add("wb-active"),this._renderQueue(),this}closeQueuePanel(){return this.queueEl?(this.queueOpen=!1,this.queueEl.classList.remove("wb-queue-open"),this.queueBtnEl&&this.queueBtnEl.classList.remove("wb-active"),this):this}toggleVolumePopup(){return this.volumePopupEl?.classList.contains("wb-volume-open")?this.closeVolumePopup():this.openVolumePopup(),this}openVolumePopup(){return this.volumePopupEl?(this.closeQueuePanel(),this.volumePopupEl.classList.add("wb-volume-open"),this.muteBtnEl&&this.muteBtnEl.classList.add("wb-active"),this):this}closeVolumePopup(){return this.volumePopupEl?(this.volumePopupEl.classList.remove("wb-volume-open"),this.muteBtnEl&&this.muteBtnEl.classList.remove("wb-active"),this):this}_loadCurrentTrack(){let t=this.getCurrentTrack();if(!t||!this.player)return;this.show(),this._updateTrackDisplay(t),this._updateFavoriteUI();let e={artwork:t.artwork,album:t.album};if(t.waveform&&(e.waveform=t.waveform),t.markers&&t.markers.length){let i=this.config.markerColor;e.markers=t.markers.map(r=>({...r,color:r.color||i}))}else e.markers=[];this.player.loadTrack(t.url,t.title,t.artist,e),this._activeMarkers=t.markers&&t.markers.length?t.markers:null,this._currentMarkerIndex=-1,this.player&&this.player.setVolume(this.isMuted?0:this.volume),this._renderQueue(),this._syncPageState(),this._saveState(),this._updateNavButtons(),this._emit("trackchange",{track:t,index:this.currentIndex}),this.config.onTrackChange&&this.config.onTrackChange(t,this.currentIndex)}_updateTrackDisplay(t){this.titleEl&&this._setScrollText(this.titleEl,t.title||"Untitled"),this.artistEl&&this._setScrollText(this.artistEl,t.artist||"");let e=this.barEl.querySelector(".wb-artwork");if(e){let r=t.artwork||this.config.defaultArtwork;e.innerHTML=r?`<img src="${u(r)}" alt="${u(t.title)}" />`:n.music}this.metaEl&&this.config.showMeta&&this._renderMeta(t);let i=this.barEl.querySelector(".wb-track");i&&(i.style.cursor=t.link?"pointer":"default"),this.timeCurrentEl&&(this.timeCurrentEl.textContent="0:00"),this.timeTotalEl&&(this.timeTotalEl.textContent="0:00")}_setScrollText(t,e){t.classList.remove("wb-scrolling"),t.textContent=e,requestAnimationFrame(()=>{if(t.scrollWidth>t.clientWidth){let i=t.scrollWidth-t.clientWidth,r=Math.max(4,i/20);t.innerHTML=`<span class="wb-scroll-inner">${u(e)}</span>`,t.style.setProperty("--wb-scroll-distance",`-${i+48}px`),t.style.setProperty("--wb-scroll-duration",`${r}s`),t.classList.add("wb-scrolling")}})}_renderMeta(t){if(!this.metaEl)return;let e=[];if(t.bpm&&e.push({label:t.bpm+" BPM",type:"bpm"}),t.key&&e.push({label:t.key,type:"key"}),t.duration&&e.push({label:t.duration,type:"duration"}),t.meta)for(let[r,a]of Object.entries(t.meta))a&&e.length<this.config.maxMeta&&e.push({label:String(a),type:r});let i=e.slice(0,this.config.maxMeta);this.metaEl.style.display=i.length?"flex":"none",this.metaEl.innerHTML=i.map(r=>`<span class="wb-tag wb-tag-${u(r.type)}">${u(r.label)}</span>`).join("")}_updatePlayButton(){if(!this.playBtnEl)return;let t=this.playBtnEl.querySelector(".wb-icon-play"),e=this.playBtnEl.querySelector(".wb-icon-pause");t&&(t.style.display=this.isPlaying?"none":"block"),e&&(e.style.display=this.isPlaying?"block":"none"),this.playBtnEl.title=this.isPlaying?"Pause":"Play"}_updateNavButtons(){let t=this.barEl?.querySelector(".wb-prev"),e=this.barEl?.querySelector(".wb-next");this.repeat==="all"?(t&&t.classList.remove("wb-disabled"),e&&e.classList.remove("wb-disabled")):(t&&t.classList.toggle("wb-disabled",this.currentIndex<=0),e&&e.classList.toggle("wb-disabled",this.currentIndex>=this.queue.length-1))}cycleRepeat(){let t=["off","all","one"],e=t.indexOf(this.repeat);return this.repeat=t[(e+1)%t.length],this._updateRepeatButton(),this._updateNavButtons(),this._emit("repeatchange",{mode:this.repeat}),this}setRepeat(t){return["off","all","one"].includes(t)&&(this.repeat=t,this._updateRepeatButton(),this._updateNavButtons(),this._emit("repeatchange",{mode:this.repeat})),this}_updateRepeatButton(){if(!this.repeatBtnEl)return;let t={off:n.repeatOff,all:n.repeatAll,one:n.repeatOne},e={off:"Repeat: Off",all:"Repeat: All",one:"Repeat: One"};this.repeatBtnEl.innerHTML=t[this.repeat],this.repeatBtnEl.title=e[this.repeat],this.repeatBtnEl.classList.toggle("wb-repeat-active",this.repeat!=="off")}_checkMarkerBoundary(t){if(!this._activeMarkers)return;let e=-1;for(let l=this._activeMarkers.length-1;l>=0;l--)if(t>=this._activeMarkers[l].time){e=l;break}if(e===this._currentMarkerIndex||(this._currentMarkerIndex=e,e<0))return;let i=this._activeMarkers[e],r=this.getCurrentTrack();i.title&&this.titleEl&&this._setScrollText(this.titleEl,i.title),i.artist&&this.artistEl&&this._setScrollText(this.artistEl,i.artist);let a=this.waveformContainer?.querySelectorAll(".waveform-marker");if(a&&a.forEach((l,c)=>l.classList.toggle("wb-marker-active",c===e)),i.artwork){let l=this.barEl.querySelector(".wb-artwork");l&&(l.innerHTML=`<img src="${i.artwork}" alt="${i.title||""}" />`)}if(this.metaEl&&(i.bpm||i.key)){let l={...r||{},bpm:i.bpm||"",key:i.key||""};this._renderMeta(l)}this._emit("markerchange",{marker:i,index:e,track:r})}_updateVolumeUI(){this.volumeSliderEl&&(this.volumeSliderEl.value=this.isMuted?0:Math.round(this.volume*100)),this.muteBtnEl&&(this.isMuted||this.volume===0?(this.muteBtnEl.innerHTML=n.volMute,this.muteBtnEl.classList.add("wb-muted"),this.muteBtnEl.title="Unmute"):this.volume<.5?(this.muteBtnEl.innerHTML=n.volLow,this.muteBtnEl.classList.remove("wb-muted"),this.muteBtnEl.title="Mute"):(this.muteBtnEl.innerHTML=n.volHigh,this.muteBtnEl.classList.remove("wb-muted"),this.muteBtnEl.title="Mute"))}_detectTheme(){let t=document.documentElement,e=document.body,i=["dark","dark-mode","theme-dark"],r=["light","light-mode","theme-light"];for(let a of i)if(t.classList.contains(a)||e.classList.contains(a))return"dark";if(t.getAttribute("data-theme")==="dark"||e.getAttribute("data-theme")==="dark")return"dark";for(let a of r)if(t.classList.contains(a)||e.classList.contains(a))return"light";if(t.getAttribute("data-theme")==="light"||e.getAttribute("data-theme")==="light")return"light";try{let l=getComputedStyle(e).backgroundColor.match(/\d+/g);if(l&&l.length>=3){let c=(l[0]*299+l[1]*587+l[2]*114)/1e3;if(c>128)return"light";if(c<128)return"dark"}}catch{}return window.matchMedia?.("(prefers-color-scheme: light)").matches?"light":"dark"}_updateFavoriteUI(){if(!this.favBtnEl)return;let t=this.isFavorited();this.favBtnEl.innerHTML=t?n.heartFilled:n.heart,this.favBtnEl.classList.toggle("wb-fav-active",t)}_renderQueue(){B(this.queueBodyEl,this.queueCountEl,this.queue,this.currentIndex,{onSkipTo:t=>this.skipTo(t),onRemove:t=>this.removeFromQueue(t)})}_syncPageState(){let t=this.getCurrentTrack(),e=t?t.url:null;document.querySelectorAll("[data-wb-play]").forEach(i=>{let r=i.dataset.wbUrl||i.dataset.url,a=i.dataset.wbId||i.dataset.id||r,l=r&&r===e;i.classList.toggle("wb-current",l),i.classList.toggle("wb-playing",l&&this.isPlaying),i.classList.toggle("wb-favorited",this._favorites.has(a)),i.classList.toggle("wb-in-cart",this._cartItems.has(a))})}_seedFromAttributes(){let t=!1,e=!1;document.querySelectorAll("[data-wb-play]").forEach(i=>{let r=i.dataset.wbId||i.dataset.id||i.dataset.wbUrl||i.dataset.url;r&&(i.dataset.wbFavorited==="true"&&(this._favorites.add(r),t=!0),i.dataset.wbInCart==="true"&&(this._cartItems.add(r),e=!0))}),t&&p(this.config.storageKey,this._favorites)}_syncFavoriteAttributes(t,e){document.querySelectorAll("[data-wb-play]").forEach(i=>{(i.dataset.wbUrl||i.dataset.url)===t&&(i.dataset.wbFavorited=e?"true":"false",i.classList.toggle("wb-favorited",e))})}_syncCartAttributes(t,e){document.querySelectorAll("[data-wb-play]").forEach(i=>{(i.dataset.wbUrl||i.dataset.url)===t&&(i.dataset.wbInCart=e?"true":"false",i.classList.toggle("wb-in-cart",e))})}_saveState(){this.config.persist&&g(this.config.storageKey,{queue:this.queue,currentIndex:this.currentIndex,position:this._lastPosition||0,isPlaying:this.isPlaying})}_restoreState(){if(!this.config.persist)return;let t=w(this.config.storageKey);if(!t)return;this.queue=t.queue,this.currentIndex=t.currentIndex;let e=this.getCurrentTrack();if(e){if(this.show(),this._updateTrackDisplay(e),this._updateFavoriteUI(),this._updateNavButtons(),e.waveform&&(this.player.options.waveform=e.waveform),this.player.options.title=e.title||"",this.player.options.subtitle=e.artist||"",e.markers&&e.markers.length){let i=this.config.markerColor;this.player.options.markers=e.markers.map(r=>({...r,color:r.color||i})),this._activeMarkers=e.markers}else this.player.options.markers=[],this._activeMarkers=null;this._currentMarkerIndex=-1,this.player.load(e.url).then(()=>{if(this.player&&this.player.setVolume(this.isMuted?0:this.volume),console.log("RESTORE: position =",t.position,"duration =",this.player?.audio?.duration),t.isPlaying&&this.config.autoResume)try{let i=this.player.play();i&&typeof i.catch=="function"&&i.catch(()=>{this.isPlaying=!1,this._updatePlayButton(),this._syncPageState()})}catch{this.isPlaying=!1,this._updatePlayButton(),this._syncPageState()}t.position>0&&setTimeout(()=>{this.player&&(this.player.seekTo(t.position),this._lastPosition=t.position)},100)}).catch(()=>{}),this._renderQueue(),this._syncPageState()}}_restoreVolume(){let t=E(this.config.storageKey);t&&(this.volume=t.volume,this.isMuted=t.muted,this._volumeBeforeMute=t.volumeBeforeMute,this.player&&this.player.setVolume(this.isMuted?0:this.volume),this._updateVolumeUI())}_restoreFavorites(){this._favorites=_(this.config.storageKey)}};var C=new f;typeof window<"u"&&(window.WaveformBar=C);var W=C;})();
|
|
44
|
+
</div>`}}s.innerHTML=l,s.querySelectorAll(".wb-queue-item[data-qi]").forEach(o=>{o.addEventListener("click",h=>{h.target.closest(".wb-queue-remove")||r.onSkipTo&&r.onSkipTo(parseInt(o.dataset.qi))})}),s.querySelectorAll(".wb-queue-remove").forEach(o=>{o.addEventListener("click",h=>{h.stopPropagation(),r.onRemove&&r.onRemove(parseInt(o.dataset.qi))})})}var x={persist:!0,autoResume:!0,continuous:!0,repeat:"off",showRepeat:!0,showQueue:!0,showPrevNext:!0,showVolume:!0,showMute:!0,showMeta:!0,showTime:!0,showTrackLink:!0,maxMeta:3,defaultArtwork:null,theme:null,waveformStyle:"mirror",waveformHeight:32,barWidth:2,barSpacing:0,waveformColor:null,progressColor:null,markerColor:"rgba(255, 255, 255, 0.25)",volume:1,storageKey:"waveform-bar",actions:null,onPlay:null,onPause:null,onTrackChange:null,onQueueChange:null,onVolumeChange:null,onFavorite:null,onCart:null},f=class{constructor(){this.config=null,this.player=null,this.queue=[],this.currentIndex=-1,this.isPlaying=!1,this.isInitialized=!1,this.queueOpen=!1,this.volume=1,this.isMuted=!1,this._volumeBeforeMute=1,this._lastPosition=0,this._favorites=new Set,this._cartItems=new Set,this._observer=null,this._activeMarkers=null,this._currentMarkerIndex=-1,this.repeat="off",this.barEl=null,this.queueEl=null,this.waveformContainer=null,this.volumePopupEl=null,this.titleEl=null,this.artistEl=null,this.metaEl=null,this.playBtnEl=null,this.repeatBtnEl=null,this.queueBtnEl=null,this.queueBodyEl=null,this.queueCountEl=null,this.volumeSliderEl=null,this.muteBtnEl=null,this.favBtnEl=null,this.cartBtnEl=null,this.timeCurrentEl=null,this.timeTotalEl=null}init(t={}){return this.isInitialized&&this.destroy(),this.config={...x,...t},this.volume=this.config.volume,typeof window.WaveformPlayer>"u"?(console.error("WaveformBar: WaveformPlayer is required."),this):(this._createBar(),this._createQueue(),this._initPlayer(),this._bindTriggers(),this._observeDOM(),this.config.persist&&(this._restoreVolume(),this._restoreFavorites()),this._seedFromAttributes(),this.config.persist&&this._restoreState(),this.isInitialized=!0,this._beforeUnloadHandler=()=>this._saveState(),window.addEventListener("beforeunload",this._beforeUnloadHandler),this)}destroy(){return this.player&&(this.player.destroy(),this.player=null),this.barEl&&(this.barEl.remove(),this.barEl=null),this.queueEl&&(this.queueEl.remove(),this.queueEl=null),this._observer&&(this._observer.disconnect(),this._observer=null),this._beforeUnloadHandler&&(window.removeEventListener("beforeunload",this._beforeUnloadHandler),this._beforeUnloadHandler=null),document.querySelectorAll("[data-wb-play],[data-wb-queue]").forEach(t=>delete t._wbBound),document.querySelectorAll(".wb-current,.wb-playing").forEach(t=>t.classList.remove("wb-current","wb-playing")),this.queue=[],this.currentIndex=-1,this.isPlaying=!1,this.queueOpen=!1,this.isInitialized=!1,this.config=null,this}_createBar(){this.barEl=document.createElement("div"),this.barEl.className="waveform-bar";let t=this.config.theme||this._detectTheme();t==="light"&&this.barEl.classList.add("wb-light"),this._resolvedTheme=t,this.barEl.id="waveform-bar",this.barEl.innerHTML=k(this.config),document.body.appendChild(this.barEl),this.titleEl=this.barEl.querySelector(".wb-title"),this.artistEl=this.barEl.querySelector(".wb-artist"),this.metaEl=this.barEl.querySelector(".wb-meta"),this.playBtnEl=this.barEl.querySelector(".wb-play"),this.waveformContainer=this.barEl.querySelector(".wb-waveform-container"),this.queueBtnEl=this.barEl.querySelector(".wb-queue-btn"),this.muteBtnEl=this.barEl.querySelector(".wb-mute"),this.volumeSliderEl=this.barEl.querySelector(".wb-volume-slider"),this.favBtnEl=this.barEl.querySelector(".wb-fav"),this.cartBtnEl=this.barEl.querySelector(".wb-cart"),this.timeCurrentEl=this.barEl.querySelector(".wb-time-current"),this.timeTotalEl=this.barEl.querySelector(".wb-time-total"),this.playBtnEl.addEventListener("click",()=>this.togglePlay());let e=this.barEl.querySelector(".wb-prev"),i=this.barEl.querySelector(".wb-next");e&&e.addEventListener("click",()=>this.previous()),i&&i.addEventListener("click",()=>this.next()),this.repeatBtnEl=this.barEl.querySelector(".wb-repeat"),this.repeatBtnEl&&(this.repeat=this.config.repeat||"off",this._updateRepeatButton(),this.repeatBtnEl.addEventListener("click",()=>this.cycleRepeat())),this.queueBtnEl&&this.queueBtnEl.addEventListener("click",()=>this.toggleQueuePanel()),this.volumePopupEl=this.barEl.querySelector(".wb-volume-popup");let r=this.barEl.querySelector(".wb-volume");if(this.muteBtnEl&&this.muteBtnEl.addEventListener("click",a=>{a.stopPropagation(),this.toggleMute()}),r&&this.volumePopupEl){let a;r.addEventListener("mouseenter",()=>{clearTimeout(a),this.openVolumePopup()}),r.addEventListener("mouseleave",()=>{a=setTimeout(()=>this.closeVolumePopup(),300)})}this.volumeSliderEl&&this.volumeSliderEl.addEventListener("input",a=>{a.stopPropagation(),this.setVolume(parseInt(a.target.value)/100)}),document.addEventListener("click",a=>{this.volumePopupEl?.classList.contains("wb-volume-open")&&!this.barEl.querySelector(".wb-volume")?.contains(a.target)&&this.closeVolumePopup()}),this.favBtnEl&&this.favBtnEl.addEventListener("click",()=>this.toggleFavorite()),this.cartBtnEl&&this.cartBtnEl.addEventListener("click",()=>this.addToCart()),this.config.showTrackLink&&this.barEl.querySelector(".wb-track").addEventListener("click",()=>{let a=this.getCurrentTrack();a&&a.link&&(window.location.href=a.link)})}_createQueue(){this.config.showQueue&&(this.queueEl=q(),this._resolvedTheme==="light"&&this.queueEl.classList.add("wb-light"),document.body.appendChild(this.queueEl),this.queueBodyEl=this.queueEl.querySelector(".wb-queue-body"),this.queueCountEl=this.queueEl.querySelector(".wb-queue-count"),this.queueEl.querySelector(".wb-queue-clear").addEventListener("click",()=>this.clearQueue()),document.addEventListener("click",t=>{this.queueOpen&&!this.queueEl.contains(t.target)&&!this.queueBtnEl.contains(t.target)&&this.closeQueuePanel()}))}_initPlayer(){let t={showControls:!1,showInfo:!1,waveformStyle:this.config.waveformStyle,height:this.config.waveformHeight,barWidth:this.config.barWidth,barSpacing:this.config.barSpacing,singlePlay:!1,onPlay:()=>{this.isPlaying=!0,this._updatePlayButton(),this._syncPageState();let e=this.getCurrentTrack();this._emit("play",{track:e}),this.config.onPlay&&this.config.onPlay(e)},onPause:()=>{this.isPlaying=!1,this._updatePlayButton(),this._syncPageState(),this._saveState();let e=this.getCurrentTrack();this._emit("pause",{track:e}),this.config.onPause&&this.config.onPause(e)},onEnd:()=>{if(this.isPlaying=!1,this._updatePlayButton(),this._syncPageState(),this.timeCurrentEl&&(this.timeCurrentEl.textContent="0:00"),this.repeat==="one"){this.player&&(this.player.seekTo(0),this.player.play().catch(()=>{}));return}this.config.continuous&&this.currentIndex<this.queue.length-1?(this.currentIndex++,this._loadCurrentTrack()):this.repeat==="all"&&this.queue.length>0&&(this.currentIndex=0,this._loadCurrentTrack())},onTimeUpdate:(e,i)=>{this._lastPosition=e,this.timeCurrentEl&&(this.timeCurrentEl.textContent=v(e)),this.timeTotalEl&&(this.timeTotalEl.textContent=v(i)),(!this._lastSaveTime||e-this._lastSaveTime>2)&&(this._lastSaveTime=e,this._saveState()),this._activeMarkers&&this._checkMarkerBoundary(e)},onLoad:null};this.config.waveformColor&&(t.waveformColor=this.config.waveformColor),this.config.progressColor&&(t.progressColor=this.config.progressColor),this.player=new window.WaveformPlayer(this.waveformContainer,t),this.player.setVolume(this.volume)}_bindTriggers(){document.querySelectorAll("[data-wb-play]").forEach(t=>{t._wbBound||(t._wbBound=!0,t.addEventListener("click",e=>{e.preventDefault();let i=m(t);i&&this.play(i)}))}),document.querySelectorAll("[data-wb-queue]").forEach(t=>{t._wbBound||(t._wbBound=!0,t.addEventListener("click",e=>{e.preventDefault(),e.stopPropagation();let i=m(t);i&&this.addToQueue(i)}))})}_observeDOM(){typeof MutationObserver>"u"||(this._observer=new MutationObserver(()=>{this._bindTriggers(),this._syncPageState()}),this._observer.observe(document.body,{childList:!0,subtree:!0}))}play(t){let e=typeof t=="string"?{url:t,id:t,title:d(t)}:t;if(!e||!e.url)return this;let i=this.getCurrentTrack();if(i&&i.url===e.url)return this.togglePlay(),this;let r=this.queue.findIndex(a=>a.url===e.url);if(r>=0)this.queue[r]={...this.queue[r],...e},this.currentIndex=r;else{let a=this.currentIndex+1;this.queue.splice(a,0,e),this.currentIndex=a}return this._loadCurrentTrack(),this}addToQueue(t){let e=typeof t=="string"?{url:t,id:t,title:d(t)}:t;return!e||!e.url?this:this.queue.find(i=>i.url===e.url)?this:(this.queue.push(e),this._renderQueue(),this._saveState(),this._updateNavButtons(),this.currentIndex===-1&&(this.currentIndex=0,this._loadCurrentTrack()),this.config.onQueueChange&&this.config.onQueueChange(this.queue,this.currentIndex),this)}togglePlay(){return this.player?(this.isPlaying?this.player.pause():this.player.play(),this):this}pause(){return this.player&&this.isPlaying&&this.player.pause(),this}next(){return this.currentIndex<this.queue.length-1?(this.currentIndex++,this._loadCurrentTrack()):this.repeat==="all"&&this.queue.length>0&&(this.currentIndex=0,this._loadCurrentTrack()),this}previous(){return this.player&&this.player.audio&&this.player.audio.currentTime>3?(this.player.seekTo(0),this):(this.currentIndex>0?(this.currentIndex--,this._loadCurrentTrack()):this.repeat==="all"&&this.queue.length>0&&(this.currentIndex=this.queue.length-1,this._loadCurrentTrack()),this)}skipTo(t){return t<0||t>=this.queue.length?this:t===this.currentIndex?(this.togglePlay(),this):(this.currentIndex=t,this._loadCurrentTrack(),this)}seekToMarker(t){if(!this._activeMarkers||t<0||t>=this._activeMarkers.length)return this;let e=this._activeMarkers[t];return this.player&&(this.player.seekTo(e.time),this.isPlaying||this.togglePlay()),this}seekToMarkerByLabel(t){if(!this._activeMarkers)return this;let e=this._activeMarkers.findIndex(i=>(i.label||i.title||"").toLowerCase()===t.toLowerCase());return e>=0&&this.seekToMarker(e),this}setVolume(t){return this.volume=Math.max(0,Math.min(1,t)),this.isMuted=this.volume===0,this.player&&this.player.setVolume(this.volume),this._updateVolumeUI(),y(this.config.storageKey,this.volume,this.isMuted,this._volumeBeforeMute),this._emit("volumechange",{volume:this.volume}),this.config.onVolumeChange&&this.config.onVolumeChange(this.volume),this}getVolume(){return this.volume}toggleMute(){return this.isMuted?this.setVolume(this._volumeBeforeMute||1):(this._volumeBeforeMute=this.volume,this.isMuted=!0,this.player&&this.player.setVolume(0),this._updateVolumeUI()),this}toggleFavorite(){let t=this.getCurrentTrack();if(!t)return this;let e=t.id||t.url,i=this._favorites.has(e);return i?this._favorites.delete(e):this._favorites.add(e),this._updateFavoriteUI(),this._syncFavoriteAttributes(t.url,!i),p(this.config.storageKey,this._favorites),this._emit("favorite",{track:t,favorited:!i}),this.config.onFavorite&&this.config.onFavorite(t,!i),this.config.actions?.favorite&&b(this.config.actions.favorite,{action:"favorite",id:e,url:t.url,title:t.title,favorited:!i}),this}addToCart(){let t=this.getCurrentTrack();if(!t)return this;let e=t.id||t.url;return this._cartItems.add(e),this.cartBtnEl&&(this.cartBtnEl.classList.add("wb-action-done"),setTimeout(()=>this.cartBtnEl.classList.remove("wb-action-done"),1500)),this._syncCartAttributes(t.url,!0),this._emit("cart",{track:t}),this.config.onCart&&this.config.onCart(t),this.config.actions?.cart&&b(this.config.actions.cart,{action:"cart",id:e,url:t.url,title:t.title}),this}isFavorited(t){if(!t){let e=this.getCurrentTrack();t=e?e.id||e.url:null}return t?this._favorites.has(t):!1}isInCart(t){if(!t){let e=this.getCurrentTrack();t=e?e.id||e.url:null}return t?this._cartItems.has(t):!1}removeFromQueue(t){return t<0||t>=this.queue.length||t===this.currentIndex?this:(this.queue.splice(t,1),t<this.currentIndex&&this.currentIndex--,this._renderQueue(),this._saveState(),this._updateNavButtons(),this._emit("queuechange",{queue:this.queue,currentIndex:this.currentIndex}),this.config.onQueueChange&&this.config.onQueueChange(this.queue,this.currentIndex),this)}clearQueue(){let t=this.getCurrentTrack();return this.queue=t?[t]:[],this.currentIndex=t?0:-1,this._renderQueue(),this._saveState(),this._updateNavButtons(),this._emit("queuechange",{queue:this.queue,currentIndex:this.currentIndex}),this.config.onQueueChange&&this.config.onQueueChange(this.queue,this.currentIndex),this}getCurrentTrack(){return this.currentIndex>=0&&this.currentIndex<this.queue.length?this.queue[this.currentIndex]:null}getQueue(){return[...this.queue]}getCurrentIndex(){return this.currentIndex}isCurrentlyPlaying(t){let e=this.getCurrentTrack();return this.isPlaying&&e&&e.url===t}isCurrentTrack(t){let e=this.getCurrentTrack();return e&&e.url===t}getPlayer(){return this.player}_emit(t,e={}){this.barEl&&this.barEl.dispatchEvent(new CustomEvent("waveformbar:"+t,{bubbles:!0,detail:e}))}show(){return this.barEl&&this.barEl.classList.add("wb-active"),this}hide(){return this.barEl&&this.barEl.classList.remove("wb-active"),this.closeQueuePanel(),this.closeVolumePopup(),this}toggleQueuePanel(){return this.queueOpen?this.closeQueuePanel():this.openQueuePanel()}openQueuePanel(){if(!this.queueEl)return this;if(this.queueOpen=!0,this.closeVolumePopup(),this.queueBtnEl){let t=this.queueBtnEl.getBoundingClientRect();this.queueEl.style.right=window.innerWidth-t.right+"px"}return this.queueEl.classList.add("wb-queue-open"),this.queueBtnEl&&this.queueBtnEl.classList.add("wb-active"),this._renderQueue(),this}closeQueuePanel(){return this.queueEl?(this.queueOpen=!1,this.queueEl.classList.remove("wb-queue-open"),this.queueBtnEl&&this.queueBtnEl.classList.remove("wb-active"),this):this}toggleVolumePopup(){return this.volumePopupEl?.classList.contains("wb-volume-open")?this.closeVolumePopup():this.openVolumePopup(),this}openVolumePopup(){return this.volumePopupEl?(this.closeQueuePanel(),this.volumePopupEl.classList.add("wb-volume-open"),this.muteBtnEl&&this.muteBtnEl.classList.add("wb-active"),this):this}closeVolumePopup(){return this.volumePopupEl?(this.volumePopupEl.classList.remove("wb-volume-open"),this.muteBtnEl&&this.muteBtnEl.classList.remove("wb-active"),this):this}_loadCurrentTrack(){let t=this.getCurrentTrack();if(!t||!this.player)return;this.show(),this._updateTrackDisplay(t),this._updateFavoriteUI();let e={artwork:t.artwork,album:t.album};if(t.waveform&&(e.waveform=t.waveform),t.markers&&t.markers.length){let i=this.config.markerColor;e.markers=t.markers.map(r=>({...r,color:r.color||i}))}else e.markers=[];this.player.loadTrack(t.url,t.title,t.artist,e),this._activeMarkers=t.markers&&t.markers.length?t.markers:null,this._currentMarkerIndex=-1,this.player&&this.player.setVolume(this.isMuted?0:this.volume),this._renderQueue(),this._syncPageState(),this._saveState(),this._updateNavButtons(),this._emit("trackchange",{track:t,index:this.currentIndex}),this.config.onTrackChange&&this.config.onTrackChange(t,this.currentIndex)}_updateTrackDisplay(t){this.titleEl&&this._setScrollText(this.titleEl,t.title||"Untitled"),this.artistEl&&this._setScrollText(this.artistEl,t.artist||"");let e=this.barEl.querySelector(".wb-artwork");if(e){let r=t.artwork||this.config.defaultArtwork;e.innerHTML=r?`<img src="${u(r)}" alt="${u(t.title)}" />`:n.music}this.metaEl&&this.config.showMeta&&this._renderMeta(t);let i=this.barEl.querySelector(".wb-track");i&&(i.style.cursor=t.link?"pointer":"default"),this.timeCurrentEl&&(this.timeCurrentEl.textContent="0:00"),this.timeTotalEl&&(this.timeTotalEl.textContent="0:00")}_setScrollText(t,e){t.classList.remove("wb-scrolling"),t.textContent=e,requestAnimationFrame(()=>{if(t.scrollWidth>t.clientWidth){let i=t.scrollWidth-t.clientWidth,r=Math.max(4,i/20);t.innerHTML=`<span class="wb-scroll-inner">${u(e)}</span>`,t.style.setProperty("--wb-scroll-distance",`-${i+48}px`),t.style.setProperty("--wb-scroll-duration",`${r}s`),t.classList.add("wb-scrolling")}})}_renderMeta(t){if(!this.metaEl)return;let e=[];if(t.bpm&&e.push({label:t.bpm+" BPM",type:"bpm"}),t.key&&e.push({label:t.key,type:"key"}),t.duration&&e.push({label:t.duration,type:"duration"}),t.meta)for(let[r,a]of Object.entries(t.meta))a&&e.length<this.config.maxMeta&&e.push({label:String(a),type:r});let i=e.slice(0,this.config.maxMeta);this.metaEl.style.display=i.length?"flex":"none",this.metaEl.innerHTML=i.map(r=>`<span class="wb-tag wb-tag-${u(r.type)}">${u(r.label)}</span>`).join("")}_updatePlayButton(){if(!this.playBtnEl)return;let t=this.playBtnEl.querySelector(".wb-icon-play"),e=this.playBtnEl.querySelector(".wb-icon-pause");t&&(t.style.display=this.isPlaying?"none":"block"),e&&(e.style.display=this.isPlaying?"block":"none"),this.playBtnEl.title=this.isPlaying?"Pause":"Play"}_updateNavButtons(){let t=this.barEl?.querySelector(".wb-prev"),e=this.barEl?.querySelector(".wb-next");this.repeat==="all"?(t&&t.classList.remove("wb-disabled"),e&&e.classList.remove("wb-disabled")):(t&&t.classList.toggle("wb-disabled",this.currentIndex<=0),e&&e.classList.toggle("wb-disabled",this.currentIndex>=this.queue.length-1))}cycleRepeat(){let t=["off","all","one"],e=t.indexOf(this.repeat);return this.repeat=t[(e+1)%t.length],this._updateRepeatButton(),this._updateNavButtons(),this._emit("repeatchange",{mode:this.repeat}),this}setRepeat(t){return["off","all","one"].includes(t)&&(this.repeat=t,this._updateRepeatButton(),this._updateNavButtons(),this._emit("repeatchange",{mode:this.repeat})),this}_updateRepeatButton(){if(!this.repeatBtnEl)return;let t={off:n.repeatOff,all:n.repeatAll,one:n.repeatOne},e={off:"Repeat: Off",all:"Repeat: All",one:"Repeat: One"};this.repeatBtnEl.innerHTML=t[this.repeat],this.repeatBtnEl.title=e[this.repeat],this.repeatBtnEl.classList.toggle("wb-repeat-active",this.repeat!=="off")}_checkMarkerBoundary(t){if(!this._activeMarkers)return;let e=-1;for(let l=this._activeMarkers.length-1;l>=0;l--)if(t>=this._activeMarkers[l].time){e=l;break}if(e===this._currentMarkerIndex||(this._currentMarkerIndex=e,e<0))return;let i=this._activeMarkers[e],r=this.getCurrentTrack();i.title&&this.titleEl&&this._setScrollText(this.titleEl,i.title),i.artist&&this.artistEl&&this._setScrollText(this.artistEl,i.artist);let a=this.waveformContainer?.querySelectorAll(".waveform-marker");if(a&&a.forEach((l,c)=>l.classList.toggle("wb-marker-active",c===e)),i.artwork){let l=this.barEl.querySelector(".wb-artwork");l&&(l.innerHTML=`<img src="${i.artwork}" alt="${i.title||""}" />`)}if(this.metaEl&&(i.bpm||i.key)){let l={...r||{},bpm:i.bpm||"",key:i.key||""};this._renderMeta(l)}this._emit("markerchange",{marker:i,index:e,track:r})}_updateVolumeUI(){this.volumeSliderEl&&(this.volumeSliderEl.value=this.isMuted?0:Math.round(this.volume*100)),this.muteBtnEl&&(this.isMuted||this.volume===0?(this.muteBtnEl.innerHTML=n.volMute,this.muteBtnEl.classList.add("wb-muted"),this.muteBtnEl.title="Unmute"):this.volume<.5?(this.muteBtnEl.innerHTML=n.volLow,this.muteBtnEl.classList.remove("wb-muted"),this.muteBtnEl.title="Mute"):(this.muteBtnEl.innerHTML=n.volHigh,this.muteBtnEl.classList.remove("wb-muted"),this.muteBtnEl.title="Mute"))}_detectTheme(){let t=document.documentElement,e=document.body,i=["dark","dark-mode","theme-dark"],r=["light","light-mode","theme-light"];for(let a of i)if(t.classList.contains(a)||e.classList.contains(a))return"dark";if(t.getAttribute("data-theme")==="dark"||e.getAttribute("data-theme")==="dark")return"dark";for(let a of r)if(t.classList.contains(a)||e.classList.contains(a))return"light";if(t.getAttribute("data-theme")==="light"||e.getAttribute("data-theme")==="light")return"light";try{let l=getComputedStyle(e).backgroundColor.match(/\d+/g);if(l&&l.length>=3){let c=(l[0]*299+l[1]*587+l[2]*114)/1e3;if(c>128)return"light";if(c<128)return"dark"}}catch{}return window.matchMedia?.("(prefers-color-scheme: light)").matches?"light":"dark"}_updateFavoriteUI(){if(!this.favBtnEl)return;let t=this.isFavorited();this.favBtnEl.innerHTML=t?n.heartFilled:n.heart,this.favBtnEl.classList.toggle("wb-fav-active",t)}_renderQueue(){B(this.queueBodyEl,this.queueCountEl,this.queue,this.currentIndex,{onSkipTo:t=>this.skipTo(t),onRemove:t=>this.removeFromQueue(t)})}_syncPageState(){let t=this.getCurrentTrack(),e=t?t.url:null;document.querySelectorAll("[data-wb-play]").forEach(i=>{let r=i.dataset.wbUrl||i.dataset.url,a=i.dataset.wbId||i.dataset.id||r,l=r&&r===e;i.classList.toggle("wb-current",l),i.classList.toggle("wb-playing",l&&this.isPlaying),i.classList.toggle("wb-favorited",this._favorites.has(a)),i.classList.toggle("wb-in-cart",this._cartItems.has(a))})}_seedFromAttributes(){let t=!1,e=!1;document.querySelectorAll("[data-wb-play]").forEach(i=>{let r=i.dataset.wbId||i.dataset.id||i.dataset.wbUrl||i.dataset.url;r&&(i.dataset.wbFavorited==="true"&&(this._favorites.add(r),t=!0),i.dataset.wbInCart==="true"&&(this._cartItems.add(r),e=!0))}),t&&p(this.config.storageKey,this._favorites)}_syncFavoriteAttributes(t,e){document.querySelectorAll("[data-wb-play]").forEach(i=>{(i.dataset.wbUrl||i.dataset.url)===t&&(i.dataset.wbFavorited=e?"true":"false",i.classList.toggle("wb-favorited",e))})}_syncCartAttributes(t,e){document.querySelectorAll("[data-wb-play]").forEach(i=>{(i.dataset.wbUrl||i.dataset.url)===t&&(i.dataset.wbInCart=e?"true":"false",i.classList.toggle("wb-in-cart",e))})}_saveState(){this.config.persist&&g(this.config.storageKey,{queue:this.queue,currentIndex:this.currentIndex,position:this._lastPosition||0,isPlaying:this.isPlaying})}_restoreState(){if(!this.config.persist)return;let t=w(this.config.storageKey);if(!t)return;this.queue=t.queue,this.currentIndex=t.currentIndex;let e=this.getCurrentTrack();if(e){if(this.show(),this._updateTrackDisplay(e),this._updateFavoriteUI(),this._updateNavButtons(),e.waveform&&(this.player.options.waveform=e.waveform),this.player.options.title=e.title||"",this.player.options.subtitle=e.artist||"",e.markers&&e.markers.length){let i=this.config.markerColor;this.player.options.markers=e.markers.map(r=>({...r,color:r.color||i})),this._activeMarkers=e.markers}else this.player.options.markers=[],this._activeMarkers=null;this._currentMarkerIndex=-1,this.player.load(e.url).then(()=>{if(this.player&&this.player.setVolume(this.isMuted?0:this.volume),t.isPlaying&&this.config.autoResume)try{let i=this.player.play();i&&typeof i.catch=="function"&&i.catch(()=>{this.isPlaying=!1,this._updatePlayButton(),this._syncPageState()})}catch{this.isPlaying=!1,this._updatePlayButton(),this._syncPageState()}t.position>0&&setTimeout(()=>{this.player&&(this.player.seekTo(t.position),this._lastPosition=t.position)},100)}).catch(()=>{}),this._renderQueue(),this._syncPageState()}}_restoreVolume(){let t=E(this.config.storageKey);t&&(this.volume=t.volume,this.isMuted=t.muted,this._volumeBeforeMute=t.volumeBeforeMute,this.player&&this.player.setVolume(this.isMuted?0:this.volume),this._updateVolumeUI())}_restoreFavorites(){this._favorites=_(this.config.storageKey)}};var C=new f;typeof window<"u"&&(window.WaveformBar=C);var W=C;})();
|
|
45
45
|
/**
|
|
46
46
|
* WaveformBar v1.0.0
|
|
47
47
|
* Persistent bottom audio player bar for WaveformPlayer
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arraypress/waveform-bar",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Persistent bottom audio player bar for WaveformPlayer - queue management, page persistence, and seamless playback.",
|
|
5
5
|
"main": "dist/waveform-bar.js",
|
|
6
6
|
"module": "dist/waveform-bar.esm.js",
|
|
@@ -5,12 +5,21 @@
|
|
|
5
5
|
* MIT License
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
|
-
*
|
|
8
|
+
* <span class="wbi wbi-play"></span>
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
.wbi
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
.wbi {
|
|
12
|
+
display: inline-block;
|
|
13
|
+
width: 1em;
|
|
14
|
+
height: 1em;
|
|
15
|
+
background-color: currentColor;
|
|
16
|
+
-webkit-mask-size: contain;
|
|
17
|
+
mask-size: contain;
|
|
18
|
+
-webkit-mask-repeat: no-repeat;
|
|
19
|
+
mask-repeat: no-repeat;
|
|
20
|
+
-webkit-mask-position: center;
|
|
21
|
+
mask-position: center;
|
|
22
|
+
vertical-align: -0.125em;
|
|
14
23
|
}
|
|
15
24
|
|
|
16
25
|
.wbi-sm {
|
|
@@ -136,4 +145,4 @@
|
|
|
136
145
|
.wbi-share {
|
|
137
146
|
-webkit-mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'><path d='M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z'/></svg>");
|
|
138
147
|
mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'><path d='M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z'/></svg>");
|
|
139
|
-
}
|
|
148
|
+
}
|
package/src/js/core.js
CHANGED
|
@@ -5,7 +5,14 @@
|
|
|
5
5
|
|
|
6
6
|
import {ICONS} from './icons.js';
|
|
7
7
|
import {extractTitle, escapeHtml, formatTime, parseTrackFromElement} from './utils.js';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
saveQueueState,
|
|
10
|
+
restoreQueueState,
|
|
11
|
+
saveVolume,
|
|
12
|
+
restoreVolume,
|
|
13
|
+
saveFavorites,
|
|
14
|
+
restoreFavorites
|
|
15
|
+
} from './storage.js';
|
|
9
16
|
import {fireAction} from './actions.js';
|
|
10
17
|
import {buildBarHTML} from './dom.js';
|
|
11
18
|
import {createQueuePanel, renderQueue} from './queue.js';
|
|
@@ -142,11 +149,26 @@ export class WaveformBar {
|
|
|
142
149
|
* @returns {WaveformBar}
|
|
143
150
|
*/
|
|
144
151
|
destroy() {
|
|
145
|
-
if (this.player) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (this.
|
|
152
|
+
if (this.player) {
|
|
153
|
+
this.player.destroy();
|
|
154
|
+
this.player = null;
|
|
155
|
+
}
|
|
156
|
+
if (this.barEl) {
|
|
157
|
+
this.barEl.remove();
|
|
158
|
+
this.barEl = null;
|
|
159
|
+
}
|
|
160
|
+
if (this.queueEl) {
|
|
161
|
+
this.queueEl.remove();
|
|
162
|
+
this.queueEl = null;
|
|
163
|
+
}
|
|
164
|
+
if (this._observer) {
|
|
165
|
+
this._observer.disconnect();
|
|
166
|
+
this._observer = null;
|
|
167
|
+
}
|
|
168
|
+
if (this._beforeUnloadHandler) {
|
|
169
|
+
window.removeEventListener('beforeunload', this._beforeUnloadHandler);
|
|
170
|
+
this._beforeUnloadHandler = null;
|
|
171
|
+
}
|
|
150
172
|
|
|
151
173
|
document.querySelectorAll('[data-wb-play],[data-wb-queue]').forEach(el => delete el._wbBound);
|
|
152
174
|
document.querySelectorAll('.wb-current,.wb-playing').forEach(el => el.classList.remove('wb-current', 'wb-playing'));
|
|
@@ -315,7 +337,8 @@ export class WaveformBar {
|
|
|
315
337
|
// Repeat current track
|
|
316
338
|
if (this.player) {
|
|
317
339
|
this.player.seekTo(0);
|
|
318
|
-
this.player.play().catch(() => {
|
|
340
|
+
this.player.play().catch(() => {
|
|
341
|
+
});
|
|
319
342
|
}
|
|
320
343
|
return;
|
|
321
344
|
}
|
|
@@ -549,7 +572,9 @@ export class WaveformBar {
|
|
|
549
572
|
return this;
|
|
550
573
|
}
|
|
551
574
|
|
|
552
|
-
getVolume() {
|
|
575
|
+
getVolume() {
|
|
576
|
+
return this.volume;
|
|
577
|
+
}
|
|
553
578
|
|
|
554
579
|
toggleMute() {
|
|
555
580
|
if (this.isMuted) {
|
|
@@ -672,11 +697,27 @@ export class WaveformBar {
|
|
|
672
697
|
return (this.currentIndex >= 0 && this.currentIndex < this.queue.length) ? this.queue[this.currentIndex] : null;
|
|
673
698
|
}
|
|
674
699
|
|
|
675
|
-
getQueue() {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
700
|
+
getQueue() {
|
|
701
|
+
return [...this.queue];
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
getCurrentIndex() {
|
|
705
|
+
return this.currentIndex;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
isCurrentlyPlaying(url) {
|
|
709
|
+
const c = this.getCurrentTrack();
|
|
710
|
+
return this.isPlaying && c && c.url === url;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
isCurrentTrack(url) {
|
|
714
|
+
const c = this.getCurrentTrack();
|
|
715
|
+
return c && c.url === url;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
getPlayer() {
|
|
719
|
+
return this.player;
|
|
720
|
+
}
|
|
680
721
|
|
|
681
722
|
// =====================================================================
|
|
682
723
|
// Events
|
|
@@ -712,10 +753,21 @@ export class WaveformBar {
|
|
|
712
753
|
// UI: Bar visibility & Queue panel
|
|
713
754
|
// =====================================================================
|
|
714
755
|
|
|
715
|
-
show() {
|
|
716
|
-
|
|
756
|
+
show() {
|
|
757
|
+
if (this.barEl) this.barEl.classList.add('wb-active');
|
|
758
|
+
return this;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
hide() {
|
|
762
|
+
if (this.barEl) this.barEl.classList.remove('wb-active');
|
|
763
|
+
this.closeQueuePanel();
|
|
764
|
+
this.closeVolumePopup();
|
|
765
|
+
return this;
|
|
766
|
+
}
|
|
717
767
|
|
|
718
|
-
toggleQueuePanel() {
|
|
768
|
+
toggleQueuePanel() {
|
|
769
|
+
return this.queueOpen ? this.closeQueuePanel() : this.openQueuePanel();
|
|
770
|
+
}
|
|
719
771
|
|
|
720
772
|
openQueuePanel() {
|
|
721
773
|
if (!this.queueEl) return this;
|
|
@@ -779,7 +831,11 @@ export class WaveformBar {
|
|
|
779
831
|
this._updateFavoriteUI();
|
|
780
832
|
|
|
781
833
|
const loadOpts = {artwork: track.artwork, album: track.album};
|
|
782
|
-
|
|
834
|
+
|
|
835
|
+
// Pass pre-existing waveform data if available
|
|
836
|
+
if (track.waveform) {
|
|
837
|
+
loadOpts.waveform = track.waveform;
|
|
838
|
+
}
|
|
783
839
|
|
|
784
840
|
// Always pass markers — empty array clears previous track's markers
|
|
785
841
|
if (track.markers && track.markers.length) {
|
|
@@ -1045,7 +1101,8 @@ export class WaveformBar {
|
|
|
1045
1101
|
if (brightness > 128) return 'light';
|
|
1046
1102
|
if (brightness < 128) return 'dark';
|
|
1047
1103
|
}
|
|
1048
|
-
} catch (e) {
|
|
1104
|
+
} catch (e) {
|
|
1105
|
+
}
|
|
1049
1106
|
|
|
1050
1107
|
// 3. System preference
|
|
1051
1108
|
if (window.matchMedia?.('(prefers-color-scheme: light)').matches) return 'light';
|
|
@@ -1199,7 +1256,10 @@ export class WaveformBar {
|
|
|
1199
1256
|
|
|
1200
1257
|
// Use load() instead of loadTrack() to avoid auto-play.
|
|
1201
1258
|
// We handle seek and play manually after the audio is ready.
|
|
1202
|
-
if (track.waveform)
|
|
1259
|
+
if (track.waveform) {
|
|
1260
|
+
this.player.options.waveform = track.waveform;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1203
1263
|
this.player.options.title = track.title || '';
|
|
1204
1264
|
this.player.options.subtitle = track.artist || '';
|
|
1205
1265
|
|
|
@@ -1220,8 +1280,6 @@ export class WaveformBar {
|
|
|
1220
1280
|
this.player.load(track.url).then(() => {
|
|
1221
1281
|
if (this.player) this.player.setVolume(this.isMuted ? 0 : this.volume);
|
|
1222
1282
|
|
|
1223
|
-
console.log('RESTORE: position =', state.position, 'duration =', this.player?.audio?.duration);
|
|
1224
|
-
|
|
1225
1283
|
if (state.isPlaying && this.config.autoResume) {
|
|
1226
1284
|
try {
|
|
1227
1285
|
const p = this.player.play();
|
|
@@ -1249,7 +1307,8 @@ export class WaveformBar {
|
|
|
1249
1307
|
}
|
|
1250
1308
|
}, 100);
|
|
1251
1309
|
}
|
|
1252
|
-
}).catch(() => {
|
|
1310
|
+
}).catch(() => {
|
|
1311
|
+
});
|
|
1253
1312
|
|
|
1254
1313
|
this._renderQueue();
|
|
1255
1314
|
this._syncPageState();
|
|
@@ -1268,4 +1327,5 @@ export class WaveformBar {
|
|
|
1268
1327
|
_restoreFavorites() {
|
|
1269
1328
|
this._favorites = restoreFavorites(this.config.storageKey);
|
|
1270
1329
|
}
|
|
1271
|
-
|
|
1330
|
+
|
|
1331
|
+
}
|