@scarlett-player/playlist 1.8.1 → 1.10.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/index.cjs CHANGED
@@ -37,6 +37,7 @@ __export(index_exports, {
37
37
  default: () => index_default
38
38
  });
39
39
  module.exports = __toCommonJS(index_exports);
40
+ var import_core2 = require("@scarlett-player/core");
40
41
 
41
42
  // src/controls.ts
42
43
  var PLAYLIST_ICONS = {
@@ -197,6 +198,7 @@ var PlaylistPanel = class {
197
198
  };
198
199
 
199
200
  // src/styles.ts
201
+ var import_core = require("@scarlett-player/core");
200
202
  var STYLE_ID = "sp-playlist-styles";
201
203
  var styles = `
202
204
  .sp-playlist-skip[disabled] {
@@ -298,18 +300,11 @@ var styles = `
298
300
  }
299
301
  `;
300
302
  function injectStyles() {
301
- if (typeof document === "undefined" || document.getElementById(STYLE_ID)) {
302
- return null;
303
- }
304
- const el = document.createElement("style");
305
- el.id = STYLE_ID;
306
- el.textContent = styles;
307
- document.head.appendChild(el);
308
- return el;
303
+ return (0, import_core.injectSharedStyles)(STYLE_ID, styles);
309
304
  }
310
305
 
311
306
  // src/version.ts
312
- var PKG_VERSION = true ? "1.8.1" : "0.0.0-dev";
307
+ var PKG_VERSION = true ? "1.10.0" : "0.0.0-dev";
313
308
 
314
309
  // src/index.ts
315
310
  var DEFAULT_CONFIG = {
@@ -336,6 +331,9 @@ function shuffleArray(array) {
336
331
  function createPlaylistPlugin(config) {
337
332
  const mergedConfig = { ...DEFAULT_CONFIG, ...config };
338
333
  let api = null;
334
+ let releaseStyles = null;
335
+ let releaseControls = null;
336
+ let lifecycle = 0;
339
337
  let tracks = mergedConfig.tracks || [];
340
338
  let currentIndex = mergedConfig.initialIndex ?? -1;
341
339
  if (currentIndex < -1 || currentIndex >= tracks.length) {
@@ -430,18 +428,24 @@ function createPlaylistPlugin(config) {
430
428
  api?.logger.warn("Failed to persist playlist", e);
431
429
  }
432
430
  };
431
+ const isTrack = (t) => typeof t === "object" && t !== null && typeof t.src === "string";
433
432
  const loadPersistedPlaylist = () => {
434
433
  if (!mergedConfig.persist) return;
434
+ if (mergedConfig.tracks !== void 0) return;
435
435
  try {
436
436
  const data = localStorage.getItem(mergedConfig.persistKey);
437
- if (data) {
438
- const parsed = JSON.parse(data);
439
- tracks = parsed.tracks || [];
440
- currentIndex = parsed.currentIndex ?? -1;
441
- shuffle = parsed.shuffle ?? false;
442
- repeat = parsed.repeat ?? "none";
443
- shuffleOrder = parsed.shuffleOrder || [];
444
- }
437
+ if (!data) return;
438
+ const parsed = JSON.parse(data);
439
+ const restored = Array.isArray(parsed.tracks) ? parsed.tracks.filter(isTrack) : [];
440
+ if (restored.length === 0) return;
441
+ tracks = restored;
442
+ const index = Number.isInteger(parsed.currentIndex) ? parsed.currentIndex : -1;
443
+ currentIndex = index >= 0 ? Math.min(index, tracks.length - 1) : -1;
444
+ shuffle = parsed.shuffle === true;
445
+ repeat = parsed.repeat === "one" || parsed.repeat === "all" ? parsed.repeat : "none";
446
+ shuffleOrder = Array.isArray(parsed.shuffleOrder) ? parsed.shuffleOrder.filter(
447
+ (n) => typeof n === "number" && n >= 0 && n < tracks.length
448
+ ) : [];
445
449
  } catch (e) {
446
450
  api?.logger.warn("Failed to load persisted playlist", e);
447
451
  }
@@ -451,20 +455,20 @@ function createPlaylistPlugin(config) {
451
455
  api?.emit("playlist:change", { track, index: currentIndex });
452
456
  persistPlaylist();
453
457
  };
454
- const setCurrentTrack = (index) => {
458
+ const setCurrentTrack = (index, options = {}) => {
455
459
  if (index < 0 || index >= tracks.length) {
456
460
  api?.logger.warn("Invalid track index", { index });
457
461
  return;
458
462
  }
459
463
  const track = tracks[index];
460
464
  currentIndex = index;
461
- api?.logger.info("Track changed", { index, title: track.title, src: track.src });
465
+ api?.logger.info("Track changed", { index, title: track.title, src: (0, import_core2.sanitizeUrl)(track.src) });
462
466
  api?.setState("title", track.title || "");
463
467
  api?.setState("poster", track.artwork || "");
464
468
  api?.setState("mediaType", track.type || "audio");
465
469
  emitChange();
466
470
  if (mergedConfig.autoLoad !== false && track.src) {
467
- api?.emit("media:load-request", { src: track.src, autoplay: true });
471
+ api?.emit("media:load-request", { src: track.src, autoplay: options.autoplay ?? true });
468
472
  }
469
473
  };
470
474
  const plugin = {
@@ -474,10 +478,11 @@ function createPlaylistPlugin(config) {
474
478
  type: "feature",
475
479
  description: "Playlist management with shuffle, repeat, and gapless playback",
476
480
  async init(pluginApi) {
481
+ const generation = ++lifecycle;
477
482
  api = pluginApi;
478
483
  api.logger.info("Playlist plugin initialized");
479
484
  loadPersistedPlaylist();
480
- if (shuffle && tracks.length > 0) {
485
+ if (shuffle && tracks.length > 0 && shuffleOrder.length !== tracks.length) {
481
486
  generateShuffleOrder();
482
487
  }
483
488
  let advanceTimeout = null;
@@ -485,11 +490,13 @@ function createPlaylistPlugin(config) {
485
490
  if (!mergedConfig.autoAdvance) return;
486
491
  const nextIdx = getNextIndex();
487
492
  if (nextIdx >= 0) {
493
+ const wasPlaying = !api?.getState("paused");
488
494
  const advance = () => {
489
495
  api?.logger.debug("Auto-advancing to next track", { nextIdx });
490
- setCurrentTrack(nextIdx);
496
+ setCurrentTrack(nextIdx, { autoplay: wasPlaying });
491
497
  };
492
498
  if (mergedConfig.advanceDelay) {
499
+ if (advanceTimeout) clearTimeout(advanceTimeout);
493
500
  advanceTimeout = setTimeout(advance, mergedConfig.advanceDelay);
494
501
  } else {
495
502
  advance();
@@ -499,20 +506,31 @@ function createPlaylistPlugin(config) {
499
506
  api?.emit("playlist:ended", void 0);
500
507
  }
501
508
  });
502
- injectStyles();
503
- void import("@scarlett-player/ui").then(({ registerControl }) => {
509
+ releaseStyles = injectStyles();
510
+ const owner = api.container;
511
+ void import("@scarlett-player/ui").then(({ registerControl, unregisterControl }) => {
512
+ if (generation !== lifecycle) return;
504
513
  const self = plugin;
505
514
  registerControl(
506
515
  "playlist-previous",
507
- () => new PlaylistSkipButton(self, "previous")
516
+ () => new PlaylistSkipButton(self, "previous"),
517
+ { owner }
508
518
  );
509
- registerControl("playlist-next", () => new PlaylistSkipButton(self, "next"));
519
+ registerControl("playlist-next", () => new PlaylistSkipButton(self, "next"), {
520
+ owner
521
+ });
510
522
  registerControl(
511
523
  "playlist",
512
524
  () => new PlaylistPanel(self, {
513
525
  onSelect: (index) => self.play(index)
514
- })
526
+ }),
527
+ { owner }
515
528
  );
529
+ releaseControls = () => {
530
+ unregisterControl("playlist-previous", { owner });
531
+ unregisterControl("playlist-next", { owner });
532
+ unregisterControl("playlist", { owner });
533
+ };
516
534
  }).catch(() => {
517
535
  api?.logger.debug("@scarlett-player/ui not present, playlist controls not registered");
518
536
  });
@@ -543,8 +561,13 @@ function createPlaylistPlugin(config) {
543
561
  });
544
562
  },
545
563
  async destroy() {
564
+ lifecycle++;
546
565
  api?.logger.info("Playlist plugin destroying");
547
566
  persistPlaylist();
567
+ releaseStyles?.();
568
+ releaseStyles = null;
569
+ releaseControls?.();
570
+ releaseControls = null;
548
571
  api = null;
549
572
  },
550
573
  add(trackOrTracks) {
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ // src/index.ts
2
+ import { sanitizeUrl } from "@scarlett-player/core";
3
+
1
4
  // src/controls.ts
2
5
  var PLAYLIST_ICONS = {
3
6
  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>',
@@ -157,6 +160,7 @@ var PlaylistPanel = class {
157
160
  };
158
161
 
159
162
  // src/styles.ts
163
+ import { injectSharedStyles } from "@scarlett-player/core";
160
164
  var STYLE_ID = "sp-playlist-styles";
161
165
  var styles = `
162
166
  .sp-playlist-skip[disabled] {
@@ -258,18 +262,11 @@ var styles = `
258
262
  }
259
263
  `;
260
264
  function injectStyles() {
261
- if (typeof document === "undefined" || document.getElementById(STYLE_ID)) {
262
- return null;
263
- }
264
- const el = document.createElement("style");
265
- el.id = STYLE_ID;
266
- el.textContent = styles;
267
- document.head.appendChild(el);
268
- return el;
265
+ return injectSharedStyles(STYLE_ID, styles);
269
266
  }
270
267
 
271
268
  // src/version.ts
272
- var PKG_VERSION = true ? "1.8.1" : "0.0.0-dev";
269
+ var PKG_VERSION = true ? "1.10.0" : "0.0.0-dev";
273
270
 
274
271
  // src/index.ts
275
272
  var DEFAULT_CONFIG = {
@@ -296,6 +293,9 @@ function shuffleArray(array) {
296
293
  function createPlaylistPlugin(config) {
297
294
  const mergedConfig = { ...DEFAULT_CONFIG, ...config };
298
295
  let api = null;
296
+ let releaseStyles = null;
297
+ let releaseControls = null;
298
+ let lifecycle = 0;
299
299
  let tracks = mergedConfig.tracks || [];
300
300
  let currentIndex = mergedConfig.initialIndex ?? -1;
301
301
  if (currentIndex < -1 || currentIndex >= tracks.length) {
@@ -390,18 +390,24 @@ function createPlaylistPlugin(config) {
390
390
  api?.logger.warn("Failed to persist playlist", e);
391
391
  }
392
392
  };
393
+ const isTrack = (t) => typeof t === "object" && t !== null && typeof t.src === "string";
393
394
  const loadPersistedPlaylist = () => {
394
395
  if (!mergedConfig.persist) return;
396
+ if (mergedConfig.tracks !== void 0) return;
395
397
  try {
396
398
  const data = localStorage.getItem(mergedConfig.persistKey);
397
- if (data) {
398
- const parsed = JSON.parse(data);
399
- tracks = parsed.tracks || [];
400
- currentIndex = parsed.currentIndex ?? -1;
401
- shuffle = parsed.shuffle ?? false;
402
- repeat = parsed.repeat ?? "none";
403
- shuffleOrder = parsed.shuffleOrder || [];
404
- }
399
+ if (!data) return;
400
+ const parsed = JSON.parse(data);
401
+ const restored = Array.isArray(parsed.tracks) ? parsed.tracks.filter(isTrack) : [];
402
+ if (restored.length === 0) return;
403
+ tracks = restored;
404
+ const index = Number.isInteger(parsed.currentIndex) ? parsed.currentIndex : -1;
405
+ currentIndex = index >= 0 ? Math.min(index, tracks.length - 1) : -1;
406
+ shuffle = parsed.shuffle === true;
407
+ repeat = parsed.repeat === "one" || parsed.repeat === "all" ? parsed.repeat : "none";
408
+ shuffleOrder = Array.isArray(parsed.shuffleOrder) ? parsed.shuffleOrder.filter(
409
+ (n) => typeof n === "number" && n >= 0 && n < tracks.length
410
+ ) : [];
405
411
  } catch (e) {
406
412
  api?.logger.warn("Failed to load persisted playlist", e);
407
413
  }
@@ -411,20 +417,20 @@ function createPlaylistPlugin(config) {
411
417
  api?.emit("playlist:change", { track, index: currentIndex });
412
418
  persistPlaylist();
413
419
  };
414
- const setCurrentTrack = (index) => {
420
+ const setCurrentTrack = (index, options = {}) => {
415
421
  if (index < 0 || index >= tracks.length) {
416
422
  api?.logger.warn("Invalid track index", { index });
417
423
  return;
418
424
  }
419
425
  const track = tracks[index];
420
426
  currentIndex = index;
421
- api?.logger.info("Track changed", { index, title: track.title, src: track.src });
427
+ api?.logger.info("Track changed", { index, title: track.title, src: sanitizeUrl(track.src) });
422
428
  api?.setState("title", track.title || "");
423
429
  api?.setState("poster", track.artwork || "");
424
430
  api?.setState("mediaType", track.type || "audio");
425
431
  emitChange();
426
432
  if (mergedConfig.autoLoad !== false && track.src) {
427
- api?.emit("media:load-request", { src: track.src, autoplay: true });
433
+ api?.emit("media:load-request", { src: track.src, autoplay: options.autoplay ?? true });
428
434
  }
429
435
  };
430
436
  const plugin = {
@@ -434,10 +440,11 @@ function createPlaylistPlugin(config) {
434
440
  type: "feature",
435
441
  description: "Playlist management with shuffle, repeat, and gapless playback",
436
442
  async init(pluginApi) {
443
+ const generation = ++lifecycle;
437
444
  api = pluginApi;
438
445
  api.logger.info("Playlist plugin initialized");
439
446
  loadPersistedPlaylist();
440
- if (shuffle && tracks.length > 0) {
447
+ if (shuffle && tracks.length > 0 && shuffleOrder.length !== tracks.length) {
441
448
  generateShuffleOrder();
442
449
  }
443
450
  let advanceTimeout = null;
@@ -445,11 +452,13 @@ function createPlaylistPlugin(config) {
445
452
  if (!mergedConfig.autoAdvance) return;
446
453
  const nextIdx = getNextIndex();
447
454
  if (nextIdx >= 0) {
455
+ const wasPlaying = !api?.getState("paused");
448
456
  const advance = () => {
449
457
  api?.logger.debug("Auto-advancing to next track", { nextIdx });
450
- setCurrentTrack(nextIdx);
458
+ setCurrentTrack(nextIdx, { autoplay: wasPlaying });
451
459
  };
452
460
  if (mergedConfig.advanceDelay) {
461
+ if (advanceTimeout) clearTimeout(advanceTimeout);
453
462
  advanceTimeout = setTimeout(advance, mergedConfig.advanceDelay);
454
463
  } else {
455
464
  advance();
@@ -459,20 +468,31 @@ function createPlaylistPlugin(config) {
459
468
  api?.emit("playlist:ended", void 0);
460
469
  }
461
470
  });
462
- injectStyles();
463
- void import("@scarlett-player/ui").then(({ registerControl }) => {
471
+ releaseStyles = injectStyles();
472
+ const owner = api.container;
473
+ void import("@scarlett-player/ui").then(({ registerControl, unregisterControl }) => {
474
+ if (generation !== lifecycle) return;
464
475
  const self = plugin;
465
476
  registerControl(
466
477
  "playlist-previous",
467
- () => new PlaylistSkipButton(self, "previous")
478
+ () => new PlaylistSkipButton(self, "previous"),
479
+ { owner }
468
480
  );
469
- registerControl("playlist-next", () => new PlaylistSkipButton(self, "next"));
481
+ registerControl("playlist-next", () => new PlaylistSkipButton(self, "next"), {
482
+ owner
483
+ });
470
484
  registerControl(
471
485
  "playlist",
472
486
  () => new PlaylistPanel(self, {
473
487
  onSelect: (index) => self.play(index)
474
- })
488
+ }),
489
+ { owner }
475
490
  );
491
+ releaseControls = () => {
492
+ unregisterControl("playlist-previous", { owner });
493
+ unregisterControl("playlist-next", { owner });
494
+ unregisterControl("playlist", { owner });
495
+ };
476
496
  }).catch(() => {
477
497
  api?.logger.debug("@scarlett-player/ui not present, playlist controls not registered");
478
498
  });
@@ -503,8 +523,13 @@ function createPlaylistPlugin(config) {
503
523
  });
504
524
  },
505
525
  async destroy() {
526
+ lifecycle++;
506
527
  api?.logger.info("Playlist plugin destroying");
507
528
  persistPlaylist();
529
+ releaseStyles?.();
530
+ releaseStyles = null;
531
+ releaseControls?.();
532
+ releaseControls = null;
508
533
  api = null;
509
534
  },
510
535
  add(trackOrTracks) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scarlett-player/playlist",
3
- "version": "1.8.1",
3
+ "version": "1.10.0",
4
4
  "description": "Playlist Plugin for Scarlett Player - Queue management, shuffle, repeat, and gapless playback",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -22,16 +22,16 @@
22
22
  "dist"
23
23
  ],
24
24
  "peerDependencies": {
25
- "@scarlett-player/core": "^1.8.0",
26
- "@scarlett-player/ui": "^1.8.0"
25
+ "@scarlett-player/core": "^1.9.0",
26
+ "@scarlett-player/ui": "^1.9.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "tsup": "^8.0.0",
30
30
  "typescript": "^5.3.0",
31
31
  "vitest": "^1.6.0",
32
32
  "jsdom": "^24.0.0",
33
- "@scarlett-player/core": "1.8.1",
34
- "@scarlett-player/ui": "1.8.1"
33
+ "@scarlett-player/core": "1.10.0",
34
+ "@scarlett-player/ui": "1.10.0"
35
35
  },
36
36
  "keywords": [
37
37
  "video",
@@ -65,6 +65,6 @@
65
65
  "dev": "tsup --watch",
66
66
  "test": "vitest --run",
67
67
  "test:watch": "vitest",
68
- "typecheck": "tsc --noEmit"
68
+ "typecheck": "tsc --noEmit -p tsconfig.typecheck.json"
69
69
  }
70
70
  }