@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/README.md CHANGED
@@ -351,7 +351,7 @@ When `data-keyboard` is enabled (default):
351
351
  - `Space` / `K` - Play/Pause
352
352
  - `M` - Mute/Unmute
353
353
  - `F` - Fullscreen
354
- - `←` / `→` - Seek -5s / +5s
354
+ - `<-` / `->` - Seek -5s / +5s
355
355
  - `↑` / `↓` - Volume +10% / -10%
356
356
 
357
357
  ## Examples
@@ -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
@@ -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
@@ -4062,7 +4062,7 @@ function createAudioUIPlugin(config) {
4062
4062
  if (layout === "mini") {
4063
4063
  const wrapper = titleEl.parentElement;
4064
4064
  if (wrapper && titleEl.scrollWidth > wrapper.clientWidth) {
4065
- titleEl.textContent = `${title} ${title} `;
4065
+ titleEl.textContent = `${title} - ${title} - `;
4066
4066
  titleEl.classList.add("scrolling");
4067
4067
  } else {
4068
4068
  titleEl.classList.remove("scrolling");
@@ -4220,6 +4220,272 @@ function createAudioUIPlugin(config) {
4220
4220
  };
4221
4221
  return plugin;
4222
4222
  }
4223
+ var PLAYLIST_ICONS = {
4224
+ 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>',
4225
+ next: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="M16 6h2v12h-2V6zM6 6l8.5 6L6 18V6z"/></svg>',
4226
+ 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>'
4227
+ };
4228
+ var PlaylistSkipButton = class {
4229
+ constructor(plugin, direction) {
4230
+ this.plugin = plugin;
4231
+ this.direction = direction;
4232
+ this.clickHandler = () => {
4233
+ if (this.direction === "next") {
4234
+ this.plugin.next();
4235
+ } else {
4236
+ this.plugin.previous();
4237
+ }
4238
+ };
4239
+ const label = direction === "next" ? "Next item" : "Previous item";
4240
+ this.el = document.createElement("button");
4241
+ this.el.type = "button";
4242
+ this.el.className = `sp-control sp-playlist-skip sp-playlist-skip--${direction}`;
4243
+ this.el.setAttribute("aria-label", label);
4244
+ this.el.setAttribute("title", label);
4245
+ this.el.innerHTML = PLAYLIST_ICONS[direction];
4246
+ this.el.addEventListener("click", this.clickHandler);
4247
+ }
4248
+ render() {
4249
+ return this.el;
4250
+ }
4251
+ update() {
4252
+ const state = this.plugin.getState();
4253
+ if (state.tracks.length <= 1) {
4254
+ this.el.style.display = "none";
4255
+ return;
4256
+ }
4257
+ this.el.style.display = "";
4258
+ const enabled = this.direction === "next" ? state.hasNext : state.hasPrevious;
4259
+ this.el.disabled = !enabled;
4260
+ }
4261
+ destroy() {
4262
+ this.el.removeEventListener("click", this.clickHandler);
4263
+ this.el.remove();
4264
+ }
4265
+ };
4266
+ var PlaylistPanel = class {
4267
+ constructor(plugin, options) {
4268
+ this.plugin = plugin;
4269
+ this.options = options;
4270
+ this.open = false;
4271
+ this.renderedIds = "";
4272
+ this.toggleHandler = (event) => {
4273
+ event.stopPropagation();
4274
+ this.setOpen(!this.open);
4275
+ };
4276
+ this.documentClickHandler = () => {
4277
+ if (this.open) this.setOpen(false);
4278
+ };
4279
+ this.keydownHandler = (event) => {
4280
+ if (event.key === "Escape" && this.open) {
4281
+ this.setOpen(false);
4282
+ this.button.focus();
4283
+ }
4284
+ };
4285
+ this.el = document.createElement("div");
4286
+ this.el.className = "sp-playlist";
4287
+ this.button = document.createElement("button");
4288
+ this.button.type = "button";
4289
+ this.button.className = "sp-control sp-playlist__button";
4290
+ this.button.setAttribute("aria-label", "Playlist");
4291
+ this.button.setAttribute("title", "Playlist");
4292
+ this.button.setAttribute("aria-haspopup", "true");
4293
+ this.button.setAttribute("aria-expanded", "false");
4294
+ this.button.innerHTML = PLAYLIST_ICONS.list;
4295
+ this.panel = document.createElement("div");
4296
+ this.panel.className = "sp-playlist__panel";
4297
+ this.panel.setAttribute("role", "menu");
4298
+ this.panel.hidden = true;
4299
+ this.el.appendChild(this.button);
4300
+ this.el.appendChild(this.panel);
4301
+ this.button.addEventListener("click", this.toggleHandler);
4302
+ document.addEventListener("click", this.documentClickHandler);
4303
+ document.addEventListener("keydown", this.keydownHandler);
4304
+ }
4305
+ render() {
4306
+ return this.el;
4307
+ }
4308
+ update() {
4309
+ const state = this.plugin.getState();
4310
+ if (state.tracks.length <= 1) {
4311
+ this.el.style.display = "none";
4312
+ return;
4313
+ }
4314
+ this.el.style.display = "";
4315
+ const signature = state.tracks.map((track) => track.id).join("|");
4316
+ if (signature !== this.renderedIds) {
4317
+ this.renderedIds = signature;
4318
+ this.renderPanel(state.tracks, state.currentIndex);
4319
+ return;
4320
+ }
4321
+ this.markActive(state.currentIndex);
4322
+ }
4323
+ destroy() {
4324
+ this.button.removeEventListener("click", this.toggleHandler);
4325
+ document.removeEventListener("click", this.documentClickHandler);
4326
+ document.removeEventListener("keydown", this.keydownHandler);
4327
+ this.el.remove();
4328
+ }
4329
+ setOpen(open) {
4330
+ this.open = open;
4331
+ this.panel.hidden = !open;
4332
+ this.button.setAttribute("aria-expanded", String(open));
4333
+ this.el.classList.toggle("sp-playlist--open", open);
4334
+ }
4335
+ markActive(currentIndex) {
4336
+ const items = this.panel.querySelectorAll(".sp-playlist__item");
4337
+ items.forEach((item, index) => {
4338
+ item.classList.toggle("sp-playlist__item--active", index === currentIndex);
4339
+ item.setAttribute("aria-current", index === currentIndex ? "true" : "false");
4340
+ });
4341
+ }
4342
+ renderPanel(tracks, currentIndex) {
4343
+ this.panel.textContent = "";
4344
+ tracks.forEach((track, index) => {
4345
+ const item = document.createElement("button");
4346
+ item.type = "button";
4347
+ item.className = "sp-playlist__item";
4348
+ item.setAttribute("role", "menuitem");
4349
+ item.setAttribute("aria-current", index === currentIndex ? "true" : "false");
4350
+ if (index === currentIndex) {
4351
+ item.classList.add("sp-playlist__item--active");
4352
+ }
4353
+ const position = document.createElement("span");
4354
+ position.className = "sp-playlist__position";
4355
+ position.textContent = String(index + 1);
4356
+ const text = document.createElement("span");
4357
+ text.className = "sp-playlist__text";
4358
+ const title = document.createElement("span");
4359
+ title.className = "sp-playlist__title";
4360
+ title.textContent = track.title ?? `Item ${index + 1}`;
4361
+ text.appendChild(title);
4362
+ if (track.artist) {
4363
+ const artist = document.createElement("span");
4364
+ artist.className = "sp-playlist__artist";
4365
+ artist.textContent = track.artist;
4366
+ text.appendChild(artist);
4367
+ }
4368
+ item.appendChild(position);
4369
+ item.appendChild(text);
4370
+ item.addEventListener("click", (event) => {
4371
+ event.stopPropagation();
4372
+ this.options.onSelect(index);
4373
+ this.setOpen(false);
4374
+ });
4375
+ this.panel.appendChild(item);
4376
+ });
4377
+ }
4378
+ };
4379
+ var STYLE_ID = "sp-playlist-styles";
4380
+ var styles = `
4381
+ .sp-playlist-skip[disabled] {
4382
+ opacity: 0.4;
4383
+ cursor: default;
4384
+ }
4385
+
4386
+ .sp-playlist {
4387
+ position: relative;
4388
+ display: inline-flex;
4389
+ }
4390
+
4391
+ .sp-playlist__button svg,
4392
+ .sp-playlist-skip svg {
4393
+ width: 20px;
4394
+ height: 20px;
4395
+ }
4396
+
4397
+ .sp-playlist__panel {
4398
+ position: absolute;
4399
+ bottom: calc(100% + 8px);
4400
+ right: 0;
4401
+ z-index: 20;
4402
+ display: flex;
4403
+ flex-direction: column;
4404
+ min-width: 240px;
4405
+ max-width: 320px;
4406
+ max-height: 300px;
4407
+ overflow-y: auto;
4408
+ padding: 4px;
4409
+ border-radius: 8px;
4410
+ background: rgba(20, 20, 20, 0.96);
4411
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
4412
+ }
4413
+
4414
+ .sp-playlist__panel[hidden] {
4415
+ display: none;
4416
+ }
4417
+
4418
+ .sp-playlist__item {
4419
+ display: flex;
4420
+ gap: 10px;
4421
+ align-items: flex-start;
4422
+ width: 100%;
4423
+ padding: 8px 10px;
4424
+ border: 0;
4425
+ border-radius: 6px;
4426
+ background: transparent;
4427
+ color: #fff;
4428
+ font: inherit;
4429
+ text-align: left;
4430
+ cursor: pointer;
4431
+ }
4432
+
4433
+ .sp-playlist__item:hover,
4434
+ .sp-playlist__item:focus-visible {
4435
+ background: rgba(255, 255, 255, 0.12);
4436
+ }
4437
+
4438
+ .sp-playlist__item--active {
4439
+ background: rgba(255, 255, 255, 0.08);
4440
+ }
4441
+
4442
+ .sp-playlist__item--active .sp-playlist__title {
4443
+ font-weight: 600;
4444
+ }
4445
+
4446
+ .sp-playlist__position {
4447
+ flex: 0 0 auto;
4448
+ min-width: 18px;
4449
+ color: rgba(255, 255, 255, 0.7);
4450
+ font-variant-numeric: tabular-nums;
4451
+ font-size: 12px;
4452
+ line-height: 18px;
4453
+ }
4454
+
4455
+ .sp-playlist__text {
4456
+ display: flex;
4457
+ flex-direction: column;
4458
+ min-width: 0;
4459
+ }
4460
+
4461
+ .sp-playlist__title {
4462
+ font-size: 13px;
4463
+ line-height: 18px;
4464
+ }
4465
+
4466
+ .sp-playlist__artist {
4467
+ color: rgba(255, 255, 255, 0.6);
4468
+ font-size: 11px;
4469
+ line-height: 16px;
4470
+ }
4471
+
4472
+ @media (max-width: 480px) {
4473
+ .sp-playlist__panel {
4474
+ min-width: 200px;
4475
+ max-width: 76vw;
4476
+ }
4477
+ }
4478
+ `;
4479
+ function injectStyles() {
4480
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID)) {
4481
+ return null;
4482
+ }
4483
+ const el = document.createElement("style");
4484
+ el.id = STYLE_ID;
4485
+ el.textContent = styles;
4486
+ document.head.appendChild(el);
4487
+ return el;
4488
+ }
4223
4489
  var DEFAULT_CONFIG$1 = {
4224
4490
  autoAdvance: true,
4225
4491
  preloadNext: true,
@@ -4409,8 +4675,42 @@ function createPlaylistPlugin(config) {
4409
4675
  api?.emit("playlist:ended", void 0);
4410
4676
  }
4411
4677
  });
4678
+ injectStyles();
4679
+ void import("./hls2.js").then(({ registerControl }) => {
4680
+ const self = plugin;
4681
+ registerControl(
4682
+ "playlist-previous",
4683
+ () => new PlaylistSkipButton(self, "previous")
4684
+ );
4685
+ registerControl("playlist-next", () => new PlaylistSkipButton(self, "next"));
4686
+ registerControl(
4687
+ "playlist",
4688
+ () => new PlaylistPanel(self, {
4689
+ onSelect: (index) => self.play(index)
4690
+ })
4691
+ );
4692
+ }).catch(() => {
4693
+ api?.logger.debug("@scarlett-player/ui not present, playlist controls not registered");
4694
+ });
4695
+ const onKeyDown = (event) => {
4696
+ if (!api || !api.container.contains(document.activeElement)) return;
4697
+ const activeEl = document.activeElement;
4698
+ if (activeEl instanceof HTMLInputElement || activeEl instanceof HTMLTextAreaElement || activeEl instanceof HTMLSelectElement || activeEl?.isContentEditable) {
4699
+ return;
4700
+ }
4701
+ if (event.metaKey || event.ctrlKey || event.altKey) return;
4702
+ if (event.key === "n" || event.key === "N") {
4703
+ event.preventDefault();
4704
+ plugin.next();
4705
+ } else if (event.key === "p" || event.key === "P") {
4706
+ event.preventDefault();
4707
+ plugin.previous();
4708
+ }
4709
+ };
4710
+ document.addEventListener("keydown", onKeyDown);
4412
4711
  api.onDestroy(() => {
4413
4712
  unsubEnded();
4713
+ document.removeEventListener("keydown", onKeyDown);
4414
4714
  if (advanceTimeout) {
4415
4715
  clearTimeout(advanceTimeout);
4416
4716
  advanceTimeout = null;