@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 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 Waveform Data
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
- Skip client-side audio analysis by providing pre-generated waveform peaks.
136
- Use [waveform-gen](https://github.com/arraypress/waveform-gen) to generate the data.
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='[0.12,0.45,0.89,0.34,0.67,0.92,0.15,0.78]'>
159
+ data-wb-waveform="[0.12,0.45,0.89,0.34,0.67]">
144
160
  </div>
145
161
  ```
146
162
 
147
- The waveform displays instantly on load without downloading and decoding the audio file.
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', // POST URL
284
+ endpoint: '/api/favorites',
242
285
  method: 'POST'
243
286
  },
244
287
  cart: {
245
- endpoint: '/api/cart', // POST URL
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(); // Next track (wraps if repeat: 'all')
291
- WaveformBar.previous(); // Previous track (restarts if >3s in)
292
- WaveformBar.skipTo(3); // Jump to queue index 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); // Set to 50%
323
- WaveformBar.getVolume(); // Returns 0.5
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
- The queue panel shows three sections: **Now Playing** (click to toggle play/pause), **Up Next**, and **Previously Played
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(); // Toggle current track's favorite state
356
- WaveformBar.isFavorited('beat-001'); // Check by track ID
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(); // { url, title, artist, bpm, key, ... }
381
- WaveformBar.isCurrentlyPlaying('song.mp3'); // true if this URL is actively playing
382
- WaveformBar.isCurrentTrack('song.mp3'); // true if current (playing or paused)
383
- WaveformBar.getQueue(); // Full queue array
384
- WaveformBar.getHistory(); // Previously played tracks
385
- WaveformBar.getPlayer(); // Underlying WaveformPlayer instance
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(); // Show the bar
392
- WaveformBar.hide(); // Hide the bar
393
- WaveformBar.toggleQueuePanel(); // Toggle queue panel visibility
394
- WaveformBar.toggleVolumePopup(); // Toggle volume popup
395
- WaveformBar.closeQueuePanel();
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 and glow when the element's track is current.
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
- ```html
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
- <div data-wb-play data-url="song.mp3" class="wb-pulse-playing">
502
- <img src="cover.jpg">
503
- </div>
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
- ### Favorite/Cart Visibility
462
+ ## Page Icons
507
463
 
508
- Show or hide content based on favorite/cart state.
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
- <div data-wb-play data-url="song.mp3" data-id="beat-001">
513
- <button>
514
- <span class="wb-hide-if-fav">♡ Save</span>
515
- <span class="wb-show-if-fav">❤ Saved</span>
516
- </button>
517
- <button>
518
- <span class="wb-hide-if-cart">🛒 Add to Cart</span>
519
- <span class="wb-show-if-cart">✓ In Cart</span>
520
- </button>
521
- </div>
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
- WaveformBar uses two storage mechanisms:
527
-
528
- **sessionStorage** (cleared when browser closes):
500
+ **sessionStorage** (cleared when browser closes): queue, current track index, playback position, playing state.
529
501
 
530
- - Queue contents and order
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; /* e.g. Spotify green */
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
- - Chrome/Edge 90+
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.3.5 — must be loaded before WaveformBar
530
+ - [WaveformPlayer](https://github.com/arraypress/waveform-player) ≥1.5.0 — must be loaded before WaveformBar
723
531
 
724
- ## License
532
+ ## Ecosystem
725
533
 
726
- MIT © [ArrayPress](https://github.com/arraypress)
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
- ## Related
542
+ ## License
729
543
 
730
- - [WaveformPlayer](https://github.com/arraypress/waveform-player) — Core audio player with waveform visualization
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
- * <span class="wbi wbi-play"></span>
8
+ * <span class="wbi wbi-play"></span>
9
9
  */
10
10
 
11
- .wbi-heart-filled {
12
- -webkit-mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor' stroke='currentColor' stroke-width='2'><path d='M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z'/></svg>");
13
- mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor' stroke='currentColor' stroke-width='2'><path d='M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z'/></svg>");
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
+ }
@@ -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) loadOpts.waveform = 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) this.player.options.waveform = 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();
@@ -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) loadOpts.waveform = 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) this.player.options.waveform = 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();
@@ -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.0",
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
- * <span class="wbi wbi-play"></span>
8
+ * <span class="wbi wbi-play"></span>
9
9
  */
10
10
 
11
- .wbi-heart-filled {
12
- -webkit-mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor' stroke='currentColor' stroke-width='2'><path d='M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z'/></svg>");
13
- mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor' stroke='currentColor' stroke-width='2'><path d='M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z'/></svg>");
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 {saveQueueState, restoreQueueState, saveVolume, restoreVolume, saveFavorites, restoreFavorites} from './storage.js';
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) { this.player.destroy(); this.player = null; }
146
- if (this.barEl) { this.barEl.remove(); this.barEl = null; }
147
- if (this.queueEl) { this.queueEl.remove(); this.queueEl = null; }
148
- if (this._observer) { this._observer.disconnect(); this._observer = null; }
149
- if (this._beforeUnloadHandler) { window.removeEventListener('beforeunload', this._beforeUnloadHandler); this._beforeUnloadHandler = null; }
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() { return this.volume; }
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() { return [...this.queue]; }
676
- getCurrentIndex() { return this.currentIndex; }
677
- isCurrentlyPlaying(url) { const c = this.getCurrentTrack(); return this.isPlaying && c && c.url === url; }
678
- isCurrentTrack(url) { const c = this.getCurrentTrack(); return c && c.url === url; }
679
- getPlayer() { return this.player; }
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() { if (this.barEl) this.barEl.classList.add('wb-active'); return this; }
716
- hide() { if (this.barEl) this.barEl.classList.remove('wb-active'); this.closeQueuePanel(); this.closeVolumePopup(); return this; }
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() { return this.queueOpen ? this.closeQueuePanel() : this.openQueuePanel(); }
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
- if (track.waveform) loadOpts.waveform = track.waveform;
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) this.player.options.waveform = 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
+ }