@scarlett-player/embed 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/embed.js CHANGED
@@ -180,8 +180,8 @@ class StateManager {
180
180
  * Register a state key at runtime, for state a plugin owns.
181
181
  *
182
182
  * Core cannot know every plugin's keys, and {@link get} deliberately throws
183
- * for unregistered ones that throw is a useful typo-catcher and is worth
184
- * keeping so a plugin declares its keys before first use.
183
+ * for unregistered ones - that throw is a useful typo-catcher and is worth
184
+ * keeping - so a plugin declares its keys before first use.
185
185
  *
186
186
  * Idempotent by design: re-defining an existing key leaves the current value
187
187
  * untouched. Plugins commonly re-run setup after a source change, and that
@@ -920,9 +920,9 @@ class Logger {
920
920
  * ```
921
921
  */
922
922
  removeHandler(handler) {
923
- const index = this.handlers.indexOf(handler);
924
- if (index !== -1) {
925
- this.handlers.splice(index, 1);
923
+ const index2 = this.handlers.indexOf(handler);
924
+ if (index2 !== -1) {
925
+ this.handlers.splice(index2, 1);
926
926
  }
927
927
  }
928
928
  /**
@@ -1259,7 +1259,7 @@ class PluginAPI {
1259
1259
  /**
1260
1260
  * Register a state key this plugin owns, before first use.
1261
1261
  *
1262
- * Idempotent re-defining an existing key keeps its current value.
1262
+ * Idempotent - re-defining an existing key keeps its current value.
1263
1263
  * See {@link IPluginAPI.defineState}.
1264
1264
  *
1265
1265
  * @param key - State property key
@@ -1983,7 +1983,7 @@ class ScarlettPlayer {
1983
1983
  * Set quality level (-1 for auto).
1984
1984
  * @param index - Quality level index
1985
1985
  */
1986
- setQuality(index) {
1986
+ setQuality(index2) {
1987
1987
  this.checkDestroyed();
1988
1988
  if (!this._currentProvider) {
1989
1989
  this.logger.warn("No provider available for quality change");
@@ -1991,17 +1991,17 @@ class ScarlettPlayer {
1991
1991
  }
1992
1992
  const provider = this._currentProvider;
1993
1993
  if (typeof provider.setLevel === "function") {
1994
- if (index !== -1) {
1994
+ if (index2 !== -1) {
1995
1995
  const levels = this.getQualities();
1996
- if (levels.length > 0 && (index < 0 || index >= levels.length)) {
1997
- this.logger.warn(`Invalid quality index: ${index} (available: ${levels.length})`);
1996
+ if (levels.length > 0 && (index2 < 0 || index2 >= levels.length)) {
1997
+ this.logger.warn(`Invalid quality index: ${index2} (available: ${levels.length})`);
1998
1998
  return;
1999
1999
  }
2000
2000
  }
2001
- provider.setLevel(index);
2001
+ provider.setLevel(index2);
2002
2002
  this.eventBus.emit("quality:change", {
2003
- quality: index === -1 ? "auto" : `level-${index}`,
2004
- auto: index === -1
2003
+ quality: index2 === -1 ? "auto" : `level-${index2}`,
2004
+ auto: index2 === -1
2005
2005
  });
2006
2006
  }
2007
2007
  }
@@ -2323,8 +2323,8 @@ function formatBitrate(bitrate) {
2323
2323
  return `${bitrate} bps`;
2324
2324
  }
2325
2325
  function mapLevels(levels, _currentLevel) {
2326
- return levels.map((level, index) => ({
2327
- index,
2326
+ return levels.map((level, index2) => ({
2327
+ index: index2,
2328
2328
  width: level.width || 0,
2329
2329
  height: level.height || 0,
2330
2330
  bitrate: level.bitrate || 0,
@@ -2379,13 +2379,13 @@ function setupHlsEventHandlers(hls, api, callbacks) {
2379
2379
  };
2380
2380
  addHandler("hlsManifestParsed", (_event, data) => {
2381
2381
  api.logger.debug("HLS manifest parsed", { levels: data.levels.length });
2382
- const levels = data.levels.map((level, index) => ({
2383
- id: `level-${index}`,
2382
+ const levels = data.levels.map((level, index2) => ({
2383
+ id: `level-${index2}`,
2384
2384
  label: formatLevel(level),
2385
2385
  width: level.width,
2386
2386
  height: level.height,
2387
2387
  bitrate: level.bitrate,
2388
- active: index === hls.currentLevel
2388
+ active: index2 === hls.currentLevel
2389
2389
  }));
2390
2390
  api.setState("qualities", levels);
2391
2391
  api.emit("quality:levels", {
@@ -3298,12 +3298,12 @@ function createHLSPluginWith(loader, variant, config) {
3298
3298
  if (isNative || !hls) return -1;
3299
3299
  return hls.currentLevel;
3300
3300
  },
3301
- setLevel(index) {
3301
+ setLevel(index2) {
3302
3302
  if (isNative || !hls) {
3303
3303
  api?.logger.warn("Quality selection not available in native HLS mode");
3304
3304
  return;
3305
3305
  }
3306
- hls.currentLevel = index;
3306
+ hls.currentLevel = index2;
3307
3307
  },
3308
3308
  getLevels() {
3309
3309
  if (isNative || !hls) return [];
@@ -3488,7 +3488,7 @@ function createHLSPlugin(config) {
3488
3488
  config
3489
3489
  );
3490
3490
  }
3491
- var styles = `
3491
+ var styles$1 = `
3492
3492
  /* ============================================
3493
3493
  Container & Base
3494
3494
  ============================================ */
@@ -3635,6 +3635,25 @@ var styles = `
3635
3635
  border-radius: inherit;
3636
3636
  }
3637
3637
 
3638
+ /* Chapter markers */
3639
+ .sp-progress__markers {
3640
+ position: absolute;
3641
+ top: 0;
3642
+ left: 0;
3643
+ width: 100%;
3644
+ height: 100%;
3645
+ pointer-events: none;
3646
+ }
3647
+
3648
+ .sp-progress__marker {
3649
+ position: absolute;
3650
+ top: 0;
3651
+ width: 2px;
3652
+ height: 100%;
3653
+ margin-left: -1px;
3654
+ background: rgba(0, 0, 0, 0.65);
3655
+ }
3656
+
3638
3657
  .sp-progress__handle {
3639
3658
  position: absolute;
3640
3659
  top: 50%;
@@ -3694,6 +3713,16 @@ var styles = `
3694
3713
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
3695
3714
  }
3696
3715
 
3716
+ .sp-progress__tooltip-chapter {
3717
+ display: block;
3718
+ max-width: 220px;
3719
+ overflow: hidden;
3720
+ color: rgba(255, 255, 255, 0.75);
3721
+ font-weight: 400;
3722
+ font-variant-numeric: normal;
3723
+ text-overflow: ellipsis;
3724
+ }
3725
+
3697
3726
  @media (hover: hover) {
3698
3727
  .sp-progress-wrapper:hover .sp-progress__tooltip {
3699
3728
  opacity: 1;
@@ -4504,9 +4533,9 @@ var ThumbnailPreview = class {
4504
4533
  return;
4505
4534
  }
4506
4535
  const { src, width, height, columns, interval } = this.config;
4507
- const index = Math.floor(time / interval);
4508
- const col = index % columns;
4509
- const row = Math.floor(index / columns);
4536
+ const index2 = Math.floor(time / interval);
4537
+ const col = index2 % columns;
4538
+ const row = Math.floor(index2 / columns);
4510
4539
  this.img.style.backgroundImage = `url(${src})`;
4511
4540
  this.img.style.backgroundPosition = `-${col * width}px -${row * height}px`;
4512
4541
  this.img.style.backgroundSize = `${columns * width}px auto`;
@@ -4531,6 +4560,8 @@ var ProgressBar = class {
4531
4560
  this.lastSeekTime = 0;
4532
4561
  this.seekThrottleMs = 100;
4533
4562
  this.wasPlayingBeforeDrag = false;
4563
+ this.renderedChapters = null;
4564
+ this.renderedDuration = 0;
4534
4565
  this.onMouseDown = (e) => {
4535
4566
  e.preventDefault();
4536
4567
  const video = getVideo(this.api.container);
@@ -4665,12 +4696,14 @@ var ProgressBar = class {
4665
4696
  const track = createElement("div", { className: "sp-progress__track" });
4666
4697
  this.buffered = createElement("div", { className: "sp-progress__buffered" });
4667
4698
  this.filled = createElement("div", { className: "sp-progress__filled" });
4699
+ this.markers = createElement("div", { className: "sp-progress__markers" });
4668
4700
  this.handle = createElement("div", { className: "sp-progress__handle" });
4669
4701
  this.tooltip = createElement("div", { className: "sp-progress__tooltip" });
4670
4702
  this.tooltip.textContent = "0:00";
4671
4703
  this.thumbnailPreview = new ThumbnailPreview();
4672
4704
  track.appendChild(this.buffered);
4673
4705
  track.appendChild(this.filled);
4706
+ track.appendChild(this.markers);
4674
4707
  track.appendChild(this.handle);
4675
4708
  this.el.appendChild(track);
4676
4709
  this.el.appendChild(this.thumbnailPreview.getElement());
@@ -4720,6 +4753,7 @@ var ProgressBar = class {
4720
4753
  this.thumbnailPreview.setConfig(thumbnails);
4721
4754
  }
4722
4755
  this.el.classList.toggle("sp-progress--live", !!live);
4756
+ this.updateMarkers(duration, live, seekableRange);
4723
4757
  if (live && seekableRange) {
4724
4758
  const rangeLength = seekableRange.end - seekableRange.start;
4725
4759
  if (rangeLength > 0) {
@@ -4752,6 +4786,70 @@ var ProgressBar = class {
4752
4786
  this.el.setAttribute("aria-valuetext", formatTime$1(currentTime));
4753
4787
  }
4754
4788
  }
4789
+ /**
4790
+ * Label of the chapter containing a point on the timeline.
4791
+ *
4792
+ * Mirrors the chapters plugin's own lookup: a start time belongs to its
4793
+ * chapter, an end time belongs to the next one, and a point in a gap between
4794
+ * sparse chapters belongs to neither.
4795
+ *
4796
+ * @param time - Position in seconds
4797
+ * @returns The chapter label, or null when the point is outside every chapter
4798
+ */
4799
+ chapterLabelAt(time) {
4800
+ const chapters = this.api.getState("chapters") ?? [];
4801
+ for (let i = chapters.length - 1; i >= 0; i--) {
4802
+ const chapter = chapters[i];
4803
+ if (time < chapter.time) continue;
4804
+ const next = chapters[i + 1];
4805
+ const end = chapter.endTime ?? (next ? next.time : Infinity);
4806
+ return time < end ? chapter.label : null;
4807
+ }
4808
+ return null;
4809
+ }
4810
+ /**
4811
+ * Paint chapter dividers along the track.
4812
+ *
4813
+ * Reads the `chapters` state that core owns, so this works whether the list
4814
+ * came from the chapters plugin or the host set it directly, and renders
4815
+ * nothing at all when there are none.
4816
+ *
4817
+ * Rebuilds only when the list or the duration actually changed. `update()`
4818
+ * runs on every time update, and rebuilding a dozen nodes 4 times a second
4819
+ * would churn the DOM for no reason.
4820
+ *
4821
+ * @param duration - Media duration in seconds
4822
+ * @param live - Whether the media is live
4823
+ * @param seekableRange - DVR window, when the media is live
4824
+ */
4825
+ updateMarkers(duration, live, seekableRange) {
4826
+ const chapters = this.api.getState("chapters") ?? [];
4827
+ const range = live ? seekableRange ? seekableRange.end - seekableRange.start : 0 : duration;
4828
+ if (chapters.length === 0 || range <= 0) {
4829
+ if (this.renderedChapters !== null) {
4830
+ this.markers.textContent = "";
4831
+ this.renderedChapters = null;
4832
+ this.renderedDuration = 0;
4833
+ }
4834
+ return;
4835
+ }
4836
+ if (chapters === this.renderedChapters && range === this.renderedDuration) {
4837
+ return;
4838
+ }
4839
+ const origin = live && seekableRange ? seekableRange.start : 0;
4840
+ this.markers.textContent = "";
4841
+ for (const chapter of chapters) {
4842
+ if (chapter.time <= origin) continue;
4843
+ const percent = (chapter.time - origin) / range * 100;
4844
+ if (percent <= 0 || percent >= 100) continue;
4845
+ const marker = createElement("div", { className: "sp-progress__marker" });
4846
+ marker.style.left = `${percent}%`;
4847
+ marker.title = chapter.label;
4848
+ this.markers.appendChild(marker);
4849
+ }
4850
+ this.renderedChapters = chapters;
4851
+ this.renderedDuration = range;
4852
+ }
4755
4853
  getTimeFromPosition(clientX) {
4756
4854
  const rect = this.el.getBoundingClientRect();
4757
4855
  const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
@@ -4776,6 +4874,12 @@ var ProgressBar = class {
4776
4874
  } else {
4777
4875
  this.tooltip.textContent = formatTime$1(time);
4778
4876
  }
4877
+ const chapterLabel = this.chapterLabelAt(time);
4878
+ if (chapterLabel) {
4879
+ const label = createElement("span", { className: "sp-progress__tooltip-chapter" });
4880
+ label.textContent = chapterLabel;
4881
+ this.tooltip.appendChild(label);
4882
+ }
4779
4883
  this.tooltip.style.left = `${percent * 100}%`;
4780
4884
  if (this.thumbnailPreview.isConfigured()) {
4781
4885
  this.thumbnailPreview.show(time, percent);
@@ -6024,6 +6128,12 @@ var BandwidthIndicator = class {
6024
6128
  };
6025
6129
  var registry = /* @__PURE__ */ new Map();
6026
6130
  var listeners = /* @__PURE__ */ new Set();
6131
+ function registerControl(id, factory) {
6132
+ registry.set(id, factory);
6133
+ for (const listener of listeners) {
6134
+ listener(id);
6135
+ }
6136
+ }
6027
6137
  function getControlFactory(id) {
6028
6138
  return registry.get(id) ?? null;
6029
6139
  }
@@ -6187,7 +6297,15 @@ function uiPlugin(config = {}) {
6187
6297
  }
6188
6298
  hideTimeout = setTimeout(hideControls, hideDelay);
6189
6299
  };
6300
+ let last_pointer_type = null;
6301
+ const handlePointerActivity = (event) => {
6302
+ last_pointer_type = event.pointerType;
6303
+ };
6190
6304
  const handleInteraction = () => {
6305
+ if (last_pointer_type === "touch") {
6306
+ const gestures = api?.getPlugin("gestures");
6307
+ if (gestures?.ownsTapInteraction()) return;
6308
+ }
6191
6309
  showControls();
6192
6310
  };
6193
6311
  const handleMouseLeave = () => {
@@ -6266,7 +6384,7 @@ function uiPlugin(config = {}) {
6266
6384
  async init(pluginApi) {
6267
6385
  api = pluginApi;
6268
6386
  styleEl = document.createElement("style");
6269
- styleEl.textContent = styles;
6387
+ styleEl.textContent = styles$1;
6270
6388
  document.head.appendChild(styleEl);
6271
6389
  if (config.theme) {
6272
6390
  this.setTheme(config.theme);
@@ -6321,6 +6439,8 @@ function uiPlugin(config = {}) {
6321
6439
  api.logger.debug(`Control "${id}" registered after init, rebuilding control bar`);
6322
6440
  rebuildControlBar();
6323
6441
  });
6442
+ container.addEventListener("pointerdown", handlePointerActivity, { passive: true });
6443
+ container.addEventListener("pointermove", handlePointerActivity, { passive: true });
6324
6444
  container.addEventListener("mousemove", handleInteraction);
6325
6445
  container.addEventListener("mouseenter", handleInteraction);
6326
6446
  container.addEventListener("mouseleave", handleMouseLeave);
@@ -6358,6 +6478,8 @@ function uiPlugin(config = {}) {
6358
6478
  recoveredUnsubscribe?.();
6359
6479
  recoveredUnsubscribe = null;
6360
6480
  if (api?.container) {
6481
+ api.container.removeEventListener("pointerdown", handlePointerActivity);
6482
+ api.container.removeEventListener("pointermove", handlePointerActivity);
6361
6483
  api.container.removeEventListener("mousemove", handleInteraction);
6362
6484
  api.container.removeEventListener("mouseenter", handleInteraction);
6363
6485
  api.container.removeEventListener("mouseleave", handleMouseLeave);
@@ -6419,6 +6541,16 @@ function uiPlugin(config = {}) {
6419
6541
  }
6420
6542
  };
6421
6543
  }
6544
+ const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
6545
+ __proto__: null,
6546
+ formatLiveTime,
6547
+ formatTime: formatTime$1,
6548
+ getControlFactory,
6549
+ icons,
6550
+ registerControl,
6551
+ styles: styles$1,
6552
+ uiPlugin
6553
+ }, Symbol.toStringTag, { value: "Module" }));
6422
6554
  var DEFAULT_THEME = {
6423
6555
  primary: "#6366f1",
6424
6556
  background: "#18181b",
@@ -6993,7 +7125,7 @@ function createAudioUIPlugin(config) {
6993
7125
  if (layout === "mini") {
6994
7126
  const wrapper = titleEl.parentElement;
6995
7127
  if (wrapper && titleEl.scrollWidth > wrapper.clientWidth) {
6996
- titleEl.textContent = `${title} ${title} `;
7128
+ titleEl.textContent = `${title} - ${title} - `;
6997
7129
  titleEl.classList.add("scrolling");
6998
7130
  } else {
6999
7131
  titleEl.classList.remove("scrolling");
@@ -7732,6 +7864,272 @@ function createAnalyticsPlugin(config) {
7732
7864
  }
7733
7865
  };
7734
7866
  }
7867
+ var PLAYLIST_ICONS = {
7868
+ previous: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="M6 6h2v12H6V6zm3.5 6L18 6v12l-8.5-6z"/></svg>',
7869
+ next: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="M16 6h2v12h-2V6zM6 6l8.5 6L6 18V6z"/></svg>',
7870
+ list: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="M3 6h11v2H3V6zm0 5h11v2H3v-2zm0 5h7v2H3v-2zm13-1.5V8l6 3.5-6 3.5z"/></svg>'
7871
+ };
7872
+ var PlaylistSkipButton = class {
7873
+ constructor(plugin, direction) {
7874
+ this.plugin = plugin;
7875
+ this.direction = direction;
7876
+ this.clickHandler = () => {
7877
+ if (this.direction === "next") {
7878
+ this.plugin.next();
7879
+ } else {
7880
+ this.plugin.previous();
7881
+ }
7882
+ };
7883
+ const label = direction === "next" ? "Next item" : "Previous item";
7884
+ this.el = document.createElement("button");
7885
+ this.el.type = "button";
7886
+ this.el.className = `sp-control sp-playlist-skip sp-playlist-skip--${direction}`;
7887
+ this.el.setAttribute("aria-label", label);
7888
+ this.el.setAttribute("title", label);
7889
+ this.el.innerHTML = PLAYLIST_ICONS[direction];
7890
+ this.el.addEventListener("click", this.clickHandler);
7891
+ }
7892
+ render() {
7893
+ return this.el;
7894
+ }
7895
+ update() {
7896
+ const state = this.plugin.getState();
7897
+ if (state.tracks.length <= 1) {
7898
+ this.el.style.display = "none";
7899
+ return;
7900
+ }
7901
+ this.el.style.display = "";
7902
+ const enabled = this.direction === "next" ? state.hasNext : state.hasPrevious;
7903
+ this.el.disabled = !enabled;
7904
+ }
7905
+ destroy() {
7906
+ this.el.removeEventListener("click", this.clickHandler);
7907
+ this.el.remove();
7908
+ }
7909
+ };
7910
+ var PlaylistPanel = class {
7911
+ constructor(plugin, options) {
7912
+ this.plugin = plugin;
7913
+ this.options = options;
7914
+ this.open = false;
7915
+ this.renderedIds = "";
7916
+ this.toggleHandler = (event) => {
7917
+ event.stopPropagation();
7918
+ this.setOpen(!this.open);
7919
+ };
7920
+ this.documentClickHandler = () => {
7921
+ if (this.open) this.setOpen(false);
7922
+ };
7923
+ this.keydownHandler = (event) => {
7924
+ if (event.key === "Escape" && this.open) {
7925
+ this.setOpen(false);
7926
+ this.button.focus();
7927
+ }
7928
+ };
7929
+ this.el = document.createElement("div");
7930
+ this.el.className = "sp-playlist";
7931
+ this.button = document.createElement("button");
7932
+ this.button.type = "button";
7933
+ this.button.className = "sp-control sp-playlist__button";
7934
+ this.button.setAttribute("aria-label", "Playlist");
7935
+ this.button.setAttribute("title", "Playlist");
7936
+ this.button.setAttribute("aria-haspopup", "true");
7937
+ this.button.setAttribute("aria-expanded", "false");
7938
+ this.button.innerHTML = PLAYLIST_ICONS.list;
7939
+ this.panel = document.createElement("div");
7940
+ this.panel.className = "sp-playlist__panel";
7941
+ this.panel.setAttribute("role", "menu");
7942
+ this.panel.hidden = true;
7943
+ this.el.appendChild(this.button);
7944
+ this.el.appendChild(this.panel);
7945
+ this.button.addEventListener("click", this.toggleHandler);
7946
+ document.addEventListener("click", this.documentClickHandler);
7947
+ document.addEventListener("keydown", this.keydownHandler);
7948
+ }
7949
+ render() {
7950
+ return this.el;
7951
+ }
7952
+ update() {
7953
+ const state = this.plugin.getState();
7954
+ if (state.tracks.length <= 1) {
7955
+ this.el.style.display = "none";
7956
+ return;
7957
+ }
7958
+ this.el.style.display = "";
7959
+ const signature = state.tracks.map((track) => track.id).join("|");
7960
+ if (signature !== this.renderedIds) {
7961
+ this.renderedIds = signature;
7962
+ this.renderPanel(state.tracks, state.currentIndex);
7963
+ return;
7964
+ }
7965
+ this.markActive(state.currentIndex);
7966
+ }
7967
+ destroy() {
7968
+ this.button.removeEventListener("click", this.toggleHandler);
7969
+ document.removeEventListener("click", this.documentClickHandler);
7970
+ document.removeEventListener("keydown", this.keydownHandler);
7971
+ this.el.remove();
7972
+ }
7973
+ setOpen(open) {
7974
+ this.open = open;
7975
+ this.panel.hidden = !open;
7976
+ this.button.setAttribute("aria-expanded", String(open));
7977
+ this.el.classList.toggle("sp-playlist--open", open);
7978
+ }
7979
+ markActive(currentIndex) {
7980
+ const items = this.panel.querySelectorAll(".sp-playlist__item");
7981
+ items.forEach((item, index2) => {
7982
+ item.classList.toggle("sp-playlist__item--active", index2 === currentIndex);
7983
+ item.setAttribute("aria-current", index2 === currentIndex ? "true" : "false");
7984
+ });
7985
+ }
7986
+ renderPanel(tracks, currentIndex) {
7987
+ this.panel.textContent = "";
7988
+ tracks.forEach((track, index2) => {
7989
+ const item = document.createElement("button");
7990
+ item.type = "button";
7991
+ item.className = "sp-playlist__item";
7992
+ item.setAttribute("role", "menuitem");
7993
+ item.setAttribute("aria-current", index2 === currentIndex ? "true" : "false");
7994
+ if (index2 === currentIndex) {
7995
+ item.classList.add("sp-playlist__item--active");
7996
+ }
7997
+ const position = document.createElement("span");
7998
+ position.className = "sp-playlist__position";
7999
+ position.textContent = String(index2 + 1);
8000
+ const text = document.createElement("span");
8001
+ text.className = "sp-playlist__text";
8002
+ const title = document.createElement("span");
8003
+ title.className = "sp-playlist__title";
8004
+ title.textContent = track.title ?? `Item ${index2 + 1}`;
8005
+ text.appendChild(title);
8006
+ if (track.artist) {
8007
+ const artist = document.createElement("span");
8008
+ artist.className = "sp-playlist__artist";
8009
+ artist.textContent = track.artist;
8010
+ text.appendChild(artist);
8011
+ }
8012
+ item.appendChild(position);
8013
+ item.appendChild(text);
8014
+ item.addEventListener("click", (event) => {
8015
+ event.stopPropagation();
8016
+ this.options.onSelect(index2);
8017
+ this.setOpen(false);
8018
+ });
8019
+ this.panel.appendChild(item);
8020
+ });
8021
+ }
8022
+ };
8023
+ var STYLE_ID = "sp-playlist-styles";
8024
+ var styles = `
8025
+ .sp-playlist-skip[disabled] {
8026
+ opacity: 0.4;
8027
+ cursor: default;
8028
+ }
8029
+
8030
+ .sp-playlist {
8031
+ position: relative;
8032
+ display: inline-flex;
8033
+ }
8034
+
8035
+ .sp-playlist__button svg,
8036
+ .sp-playlist-skip svg {
8037
+ width: 20px;
8038
+ height: 20px;
8039
+ }
8040
+
8041
+ .sp-playlist__panel {
8042
+ position: absolute;
8043
+ bottom: calc(100% + 8px);
8044
+ right: 0;
8045
+ z-index: 20;
8046
+ display: flex;
8047
+ flex-direction: column;
8048
+ min-width: 240px;
8049
+ max-width: 320px;
8050
+ max-height: 300px;
8051
+ overflow-y: auto;
8052
+ padding: 4px;
8053
+ border-radius: 8px;
8054
+ background: rgba(20, 20, 20, 0.96);
8055
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
8056
+ }
8057
+
8058
+ .sp-playlist__panel[hidden] {
8059
+ display: none;
8060
+ }
8061
+
8062
+ .sp-playlist__item {
8063
+ display: flex;
8064
+ gap: 10px;
8065
+ align-items: flex-start;
8066
+ width: 100%;
8067
+ padding: 8px 10px;
8068
+ border: 0;
8069
+ border-radius: 6px;
8070
+ background: transparent;
8071
+ color: #fff;
8072
+ font: inherit;
8073
+ text-align: left;
8074
+ cursor: pointer;
8075
+ }
8076
+
8077
+ .sp-playlist__item:hover,
8078
+ .sp-playlist__item:focus-visible {
8079
+ background: rgba(255, 255, 255, 0.12);
8080
+ }
8081
+
8082
+ .sp-playlist__item--active {
8083
+ background: rgba(255, 255, 255, 0.08);
8084
+ }
8085
+
8086
+ .sp-playlist__item--active .sp-playlist__title {
8087
+ font-weight: 600;
8088
+ }
8089
+
8090
+ .sp-playlist__position {
8091
+ flex: 0 0 auto;
8092
+ min-width: 18px;
8093
+ color: rgba(255, 255, 255, 0.7);
8094
+ font-variant-numeric: tabular-nums;
8095
+ font-size: 12px;
8096
+ line-height: 18px;
8097
+ }
8098
+
8099
+ .sp-playlist__text {
8100
+ display: flex;
8101
+ flex-direction: column;
8102
+ min-width: 0;
8103
+ }
8104
+
8105
+ .sp-playlist__title {
8106
+ font-size: 13px;
8107
+ line-height: 18px;
8108
+ }
8109
+
8110
+ .sp-playlist__artist {
8111
+ color: rgba(255, 255, 255, 0.6);
8112
+ font-size: 11px;
8113
+ line-height: 16px;
8114
+ }
8115
+
8116
+ @media (max-width: 480px) {
8117
+ .sp-playlist__panel {
8118
+ min-width: 200px;
8119
+ max-width: 76vw;
8120
+ }
8121
+ }
8122
+ `;
8123
+ function injectStyles() {
8124
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID)) {
8125
+ return null;
8126
+ }
8127
+ const el = document.createElement("style");
8128
+ el.id = STYLE_ID;
8129
+ el.textContent = styles;
8130
+ document.head.appendChild(el);
8131
+ return el;
8132
+ }
7735
8133
  var DEFAULT_CONFIG$1 = {
7736
8134
  autoAdvance: true,
7737
8135
  preloadNext: true,
@@ -7871,14 +8269,14 @@ function createPlaylistPlugin(config) {
7871
8269
  api?.emit("playlist:change", { track, index: currentIndex });
7872
8270
  persistPlaylist();
7873
8271
  };
7874
- const setCurrentTrack = (index) => {
7875
- if (index < 0 || index >= tracks.length) {
7876
- api?.logger.warn("Invalid track index", { index });
8272
+ const setCurrentTrack = (index2) => {
8273
+ if (index2 < 0 || index2 >= tracks.length) {
8274
+ api?.logger.warn("Invalid track index", { index: index2 });
7877
8275
  return;
7878
8276
  }
7879
- const track = tracks[index];
7880
- currentIndex = index;
7881
- api?.logger.info("Track changed", { index, title: track.title, src: track.src });
8277
+ const track = tracks[index2];
8278
+ currentIndex = index2;
8279
+ api?.logger.info("Track changed", { index: index2, title: track.title, src: track.src });
7882
8280
  api?.setState("title", track.title || "");
7883
8281
  if (track.artwork) {
7884
8282
  api?.setState("poster", track.artwork);
@@ -7921,8 +8319,42 @@ function createPlaylistPlugin(config) {
7921
8319
  api?.emit("playlist:ended", void 0);
7922
8320
  }
7923
8321
  });
8322
+ injectStyles();
8323
+ void Promise.resolve().then(() => index).then(({ registerControl: registerControl2 }) => {
8324
+ const self = plugin;
8325
+ registerControl2(
8326
+ "playlist-previous",
8327
+ () => new PlaylistSkipButton(self, "previous")
8328
+ );
8329
+ registerControl2("playlist-next", () => new PlaylistSkipButton(self, "next"));
8330
+ registerControl2(
8331
+ "playlist",
8332
+ () => new PlaylistPanel(self, {
8333
+ onSelect: (index2) => self.play(index2)
8334
+ })
8335
+ );
8336
+ }).catch(() => {
8337
+ api?.logger.debug("@scarlett-player/ui not present, playlist controls not registered");
8338
+ });
8339
+ const onKeyDown = (event) => {
8340
+ if (!api || !api.container.contains(document.activeElement)) return;
8341
+ const activeEl = document.activeElement;
8342
+ if (activeEl instanceof HTMLInputElement || activeEl instanceof HTMLTextAreaElement || activeEl instanceof HTMLSelectElement || activeEl?.isContentEditable) {
8343
+ return;
8344
+ }
8345
+ if (event.metaKey || event.ctrlKey || event.altKey) return;
8346
+ if (event.key === "n" || event.key === "N") {
8347
+ event.preventDefault();
8348
+ plugin.next();
8349
+ } else if (event.key === "p" || event.key === "P") {
8350
+ event.preventDefault();
8351
+ plugin.previous();
8352
+ }
8353
+ };
8354
+ document.addEventListener("keydown", onKeyDown);
7924
8355
  api.onDestroy(() => {
7925
8356
  unsubEnded();
8357
+ document.removeEventListener("keydown", onKeyDown);
7926
8358
  if (advanceTimeout) {
7927
8359
  clearTimeout(advanceTimeout);
7928
8360
  advanceTimeout = null;
@@ -7939,10 +8371,10 @@ function createPlaylistPlugin(config) {
7939
8371
  const newTracks = Array.isArray(trackOrTracks) ? trackOrTracks : [trackOrTracks];
7940
8372
  newTracks.forEach((track) => {
7941
8373
  const normalizedTrack = { ...track, id: track.id || generateId() };
7942
- const index = tracks.length;
8374
+ const index2 = tracks.length;
7943
8375
  tracks.push(normalizedTrack);
7944
- api?.emit("playlist:add", { track: normalizedTrack, index });
7945
- api?.logger.debug("Track added", { title: normalizedTrack.title, index });
8376
+ api?.emit("playlist:add", { track: normalizedTrack, index: index2 });
8377
+ api?.logger.debug("Track added", { title: normalizedTrack.title, index: index2 });
7946
8378
  });
7947
8379
  if (shuffle) {
7948
8380
  const startIndex = tracks.length - newTracks.length;
@@ -7953,9 +8385,9 @@ function createPlaylistPlugin(config) {
7953
8385
  }
7954
8386
  persistPlaylist();
7955
8387
  },
7956
- insert(index, track) {
8388
+ insert(index2, track) {
7957
8389
  const normalizedTrack = { ...track, id: track.id || generateId() };
7958
- const clampedIndex = Math.max(0, Math.min(index, tracks.length));
8390
+ const clampedIndex = Math.max(0, Math.min(index2, tracks.length));
7959
8391
  tracks.splice(clampedIndex, 0, normalizedTrack);
7960
8392
  if (currentIndex >= clampedIndex) {
7961
8393
  currentIndex++;
@@ -7969,33 +8401,33 @@ function createPlaylistPlugin(config) {
7969
8401
  persistPlaylist();
7970
8402
  },
7971
8403
  remove(idOrIndex) {
7972
- let index;
8404
+ let index2;
7973
8405
  if (typeof idOrIndex === "string") {
7974
- index = tracks.findIndex((t) => t.id === idOrIndex);
7975
- if (index === -1) {
8406
+ index2 = tracks.findIndex((t) => t.id === idOrIndex);
8407
+ if (index2 === -1) {
7976
8408
  api?.logger.warn("Track not found", { id: idOrIndex });
7977
8409
  return;
7978
8410
  }
7979
8411
  } else {
7980
- index = idOrIndex;
8412
+ index2 = idOrIndex;
7981
8413
  }
7982
- if (index < 0 || index >= tracks.length) {
7983
- api?.logger.warn("Invalid track index", { index });
8414
+ if (index2 < 0 || index2 >= tracks.length) {
8415
+ api?.logger.warn("Invalid track index", { index: index2 });
7984
8416
  return;
7985
8417
  }
7986
- const [removedTrack] = tracks.splice(index, 1);
7987
- if (index < currentIndex) {
8418
+ const [removedTrack] = tracks.splice(index2, 1);
8419
+ if (index2 < currentIndex) {
7988
8420
  currentIndex--;
7989
- } else if (index === currentIndex) {
8421
+ } else if (index2 === currentIndex) {
7990
8422
  if (currentIndex >= tracks.length) {
7991
8423
  currentIndex = tracks.length - 1;
7992
8424
  }
7993
8425
  emitChange();
7994
8426
  }
7995
8427
  if (shuffle) {
7996
- shuffleOrder = shuffleOrder.filter((i) => i !== index).map((i) => i > index ? i - 1 : i);
8428
+ shuffleOrder = shuffleOrder.filter((i) => i !== index2).map((i) => i > index2 ? i - 1 : i);
7997
8429
  }
7998
- api?.emit("playlist:remove", { track: removedTrack, index });
8430
+ api?.emit("playlist:remove", { track: removedTrack, index: index2 });
7999
8431
  persistPlaylist();
8000
8432
  },
8001
8433
  clear() {
@@ -8006,23 +8438,23 @@ function createPlaylistPlugin(config) {
8006
8438
  emitChange();
8007
8439
  },
8008
8440
  play(idOrIndex) {
8009
- let index;
8441
+ let index2;
8010
8442
  if (idOrIndex === void 0) {
8011
- index = currentIndex >= 0 ? currentIndex : shuffle ? getActualIndex(0) : 0;
8443
+ index2 = currentIndex >= 0 ? currentIndex : shuffle ? getActualIndex(0) : 0;
8012
8444
  } else if (typeof idOrIndex === "string") {
8013
- index = tracks.findIndex((t) => t.id === idOrIndex);
8014
- if (index === -1) {
8445
+ index2 = tracks.findIndex((t) => t.id === idOrIndex);
8446
+ if (index2 === -1) {
8015
8447
  api?.logger.warn("Track not found", { id: idOrIndex });
8016
8448
  return;
8017
8449
  }
8018
8450
  } else {
8019
- index = idOrIndex;
8451
+ index2 = idOrIndex;
8020
8452
  }
8021
8453
  if (tracks.length === 0) {
8022
8454
  api?.logger.warn("Playlist is empty");
8023
8455
  return;
8024
8456
  }
8025
- setCurrentTrack(index);
8457
+ setCurrentTrack(index2);
8026
8458
  },
8027
8459
  next() {
8028
8460
  const nextIdx = getNextIndex();
@@ -8965,8 +9397,8 @@ async function createEmbedPlayer(container, config, pluginCreators2, availableTy
8965
9397
  const plugins = [pluginCreators2.hls()];
8966
9398
  if (pluginCreators2.playlist && config.playlist?.length) {
8967
9399
  plugins.push(pluginCreators2.playlist({
8968
- items: config.playlist.map((item, index) => ({
8969
- id: `item-${index}`,
9400
+ items: config.playlist.map((item, index2) => ({
9401
+ id: `item-${index2}`,
8970
9402
  src: item.src,
8971
9403
  title: item.title,
8972
9404
  artist: item.artist,