smooth-player 1.0.4 → 2.1.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 +116 -12
- package/assets/icons/logo-wave-preview.svg +28 -0
- package/assets/icons/logo.svg +8 -0
- package/assets/icons/stop.svg +3 -0
- package/assets/icons/upload.svg +5 -0
- package/assets/icons/visualizer.svg +7 -0
- package/dist/SmoothPlayer.d.ts +16 -1
- package/dist/SmoothPlayer.js +296 -13
- package/dist/i18n/en.generated.d.ts +50 -0
- package/dist/i18n/en.generated.js +51 -0
- package/dist/i18n/strings.d.ts +51 -0
- package/dist/i18n/strings.js +2 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/smooth-player.css +582 -154
- package/dist/types.d.ts +31 -0
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +567 -14
- package/dist/visualizers.d.ts +2 -0
- package/dist/visualizers.js +150 -15
- package/dist-cjs/SmoothPlayer.js +296 -13
- package/dist-cjs/i18n/en.generated.js +54 -0
- package/dist-cjs/i18n/strings.js +5 -0
- package/dist-cjs/index.js +4 -2
- package/dist-cjs/ui.js +568 -15
- package/dist-cjs/visualizers.js +150 -15
- package/package.json +4 -3
- package/styles/common/_base.scss +223 -95
- package/styles/themes/_nocturne.scss +408 -62
- package/styles/themes/_aurora.scss +0 -70
- package/styles/themes/_ocean.scss +0 -13
package/dist/SmoothPlayer.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TypedEventEmitter } from "./events.js";
|
|
2
|
+
import { strings } from "./i18n/strings.js";
|
|
2
3
|
const DEFAULT_ANALYZER = {
|
|
3
4
|
fftSize: 2048,
|
|
4
5
|
smoothingTimeConstant: 0.8,
|
|
@@ -6,8 +7,19 @@ const DEFAULT_ANALYZER = {
|
|
|
6
7
|
maxDecibels: -10,
|
|
7
8
|
};
|
|
8
9
|
const DEFAULT_ACCENT_COLOR = "#0ed2a4";
|
|
9
|
-
const
|
|
10
|
-
const
|
|
10
|
+
const DEFAULT_BACKGROUND_COLOR = "#0b1220";
|
|
11
|
+
const DEFAULT_PLAYLIST_ID = strings.playlist.defaultId;
|
|
12
|
+
const DEFAULT_PLAYLIST_TITLE = strings.playlist.defaultTitle;
|
|
13
|
+
const DEFAULT_SPECTRUM_STYLE = {
|
|
14
|
+
dualLayer: false,
|
|
15
|
+
inverted: false,
|
|
16
|
+
barWidth: "medium",
|
|
17
|
+
};
|
|
18
|
+
const DEFAULT_WAVEFORM_STYLE = {
|
|
19
|
+
doubleLine: false,
|
|
20
|
+
fill: false,
|
|
21
|
+
thickLine: false,
|
|
22
|
+
};
|
|
11
23
|
export class SmoothPlayer {
|
|
12
24
|
constructor(options = {}) {
|
|
13
25
|
this.events = new TypedEventEmitter();
|
|
@@ -29,7 +41,10 @@ export class SmoothPlayer {
|
|
|
29
41
|
this.audio.preload = this.audio.preload || "metadata";
|
|
30
42
|
this.audio.volume = this.clamp(options.initialVolume ?? 1);
|
|
31
43
|
this.visualizerMode = options.visualizer ?? "spectrum";
|
|
44
|
+
this.spectrumStyle = { ...DEFAULT_SPECTRUM_STYLE, ...options.spectrumStyle };
|
|
45
|
+
this.waveformStyle = { ...DEFAULT_WAVEFORM_STYLE, ...options.waveformStyle };
|
|
32
46
|
this.accentColor = options.accentColor ?? DEFAULT_ACCENT_COLOR;
|
|
47
|
+
this.backgroundColor = options.backgroundColor ?? DEFAULT_BACKGROUND_COLOR;
|
|
33
48
|
this.shuffleEnabled = options.initialShuffle ?? false;
|
|
34
49
|
this.debugEnabled = options.debug ?? false;
|
|
35
50
|
this.durationFallbackEnabled = options.durationFallback ?? true;
|
|
@@ -63,6 +78,30 @@ export class SmoothPlayer {
|
|
|
63
78
|
applyAccentColor(target) {
|
|
64
79
|
target.style.setProperty("--smooth-player-accent", this.accentColor);
|
|
65
80
|
}
|
|
81
|
+
setBackgroundColor(color) {
|
|
82
|
+
this.backgroundColor = color;
|
|
83
|
+
}
|
|
84
|
+
getBackgroundColor() {
|
|
85
|
+
return this.backgroundColor;
|
|
86
|
+
}
|
|
87
|
+
applyBackgroundColor(target) {
|
|
88
|
+
const normalized = this.backgroundColor.trim().toLowerCase();
|
|
89
|
+
if (normalized === DEFAULT_BACKGROUND_COLOR) {
|
|
90
|
+
target.style.removeProperty("--smooth-player-background");
|
|
91
|
+
target.style.removeProperty("--smooth-player-surface");
|
|
92
|
+
target.style.removeProperty("--smooth-player-panel");
|
|
93
|
+
target.style.removeProperty("--smooth-player-muted");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
target.style.setProperty("--smooth-player-background", this.backgroundColor);
|
|
97
|
+
target.style.setProperty("--smooth-player-surface", this.buildSurfaceGradient(this.backgroundColor));
|
|
98
|
+
target.style.setProperty("--smooth-player-panel", this.buildPanelGradient(this.backgroundColor));
|
|
99
|
+
target.style.setProperty("--smooth-player-muted", `color-mix(in srgb, ${this.backgroundColor} 30%, #d2deef 70%)`);
|
|
100
|
+
}
|
|
101
|
+
applyTheme(target) {
|
|
102
|
+
this.applyAccentColor(target);
|
|
103
|
+
this.applyBackgroundColor(target);
|
|
104
|
+
}
|
|
66
105
|
setShuffle(enabled) {
|
|
67
106
|
this.shuffleEnabled = enabled;
|
|
68
107
|
}
|
|
@@ -152,8 +191,8 @@ export class SmoothPlayer {
|
|
|
152
191
|
titleClassName: options.titleClassName ?? "smooth-player__playlist-title",
|
|
153
192
|
artistClassName: options.artistClassName ?? "smooth-player__playlist-artist",
|
|
154
193
|
selectedAriaAttr: options.selectedAriaAttr ?? "aria-current",
|
|
155
|
-
getTitle: options.getTitle ?? ((track, index) => track.metadata?.title ??
|
|
156
|
-
getArtist: options.getArtist ?? ((track) => track.metadata?.artist ??
|
|
194
|
+
getTitle: options.getTitle ?? ((track, index) => track.metadata?.title ?? `${strings.track.unknownTitle} ${index + 1}`),
|
|
195
|
+
getArtist: options.getArtist ?? ((track) => track.metadata?.artist ?? strings.track.unknownArtist),
|
|
157
196
|
};
|
|
158
197
|
const onSelect = options.onSelect;
|
|
159
198
|
const render = () => {
|
|
@@ -226,7 +265,7 @@ export class SmoothPlayer {
|
|
|
226
265
|
trigger.className = "smooth-player__playlist-switcher-trigger";
|
|
227
266
|
trigger.setAttribute("aria-haspopup", "listbox");
|
|
228
267
|
trigger.setAttribute("aria-expanded", String(isOpen));
|
|
229
|
-
trigger.textContent = currentPlaylist?.title ?? playlists[0]?.title ??
|
|
268
|
+
trigger.textContent = currentPlaylist?.title ?? playlists[0]?.title ?? strings.playlist.triggerLabel;
|
|
230
269
|
const menu = doc.createElement("div");
|
|
231
270
|
menu.className = "smooth-player__playlist-switcher-menu";
|
|
232
271
|
menu.hidden = !isOpen;
|
|
@@ -295,8 +334,8 @@ export class SmoothPlayer {
|
|
|
295
334
|
return () => off();
|
|
296
335
|
}
|
|
297
336
|
mountTrackInfo(titleElement, artistElement, options = {}) {
|
|
298
|
-
const unknownTitle = options.unknownTitle ??
|
|
299
|
-
const unknownArtist = options.unknownArtist ??
|
|
337
|
+
const unknownTitle = options.unknownTitle ?? strings.track.unknownTitle;
|
|
338
|
+
const unknownArtist = options.unknownArtist ?? strings.track.unknownArtist;
|
|
300
339
|
const render = () => {
|
|
301
340
|
const track = this.getCurrentTrack();
|
|
302
341
|
titleElement.textContent = track?.metadata?.title ?? unknownTitle;
|
|
@@ -308,8 +347,8 @@ export class SmoothPlayer {
|
|
|
308
347
|
}
|
|
309
348
|
mountPlayButton(button, options = {}) {
|
|
310
349
|
const labelElement = options.labelElement ?? null;
|
|
311
|
-
const playLabel = options.playLabel ??
|
|
312
|
-
const pauseLabel = options.pauseLabel ??
|
|
350
|
+
const playLabel = options.playLabel ?? strings.playback.playLabel;
|
|
351
|
+
const pauseLabel = options.pauseLabel ?? strings.playback.pauseLabel;
|
|
313
352
|
const render = () => {
|
|
314
353
|
const isPlaying = !this.audio.paused;
|
|
315
354
|
const label = isPlaying ? pauseLabel : playLabel;
|
|
@@ -344,7 +383,7 @@ export class SmoothPlayer {
|
|
|
344
383
|
};
|
|
345
384
|
}
|
|
346
385
|
mountShuffleToggle(options) {
|
|
347
|
-
const { button, labelElement = null, activeClassName = "smooth-player__toggle-on", enabledLabel =
|
|
386
|
+
const { button, labelElement = null, activeClassName = "smooth-player__toggle-on", enabledLabel = strings.shuffle.enabledLabel, disabledLabel = strings.shuffle.disabledLabel, initialEnabled = false, } = options;
|
|
348
387
|
const render = () => {
|
|
349
388
|
const enabled = this.getShuffle();
|
|
350
389
|
const label = enabled ? enabledLabel : disabledLabel;
|
|
@@ -367,24 +406,40 @@ export class SmoothPlayer {
|
|
|
367
406
|
};
|
|
368
407
|
}
|
|
369
408
|
mountPlaylistPanel(options) {
|
|
370
|
-
const { root, toggleButton, panel, closeButton = null, openClassName = "smooth-player--playlist-open", openLabel =
|
|
409
|
+
const { root, toggleButton, panel, closeButton = null, openClassName = "smooth-player--playlist-open", openLabel = strings.playlist.openLabel, closeLabel = strings.playlist.closeLabel, } = options;
|
|
371
410
|
let isOpen = false;
|
|
372
411
|
const hasPlaylist = () => this.getPlaylists().length > 1 || this.getActiveTracks().length > 1;
|
|
373
412
|
const syncVisibility = () => {
|
|
374
413
|
toggleButton.hidden = !hasPlaylist();
|
|
375
414
|
};
|
|
376
415
|
const setOpen = (open) => {
|
|
416
|
+
const activeElement = (root.ownerDocument ?? document).activeElement;
|
|
417
|
+
const focusedInsidePanel = activeElement instanceof Node && panel.contains(activeElement);
|
|
377
418
|
if (!hasPlaylist()) {
|
|
378
419
|
isOpen = false;
|
|
420
|
+
if (focusedInsidePanel) {
|
|
421
|
+
toggleButton.focus();
|
|
422
|
+
}
|
|
379
423
|
root.classList.remove(openClassName);
|
|
380
424
|
panel.setAttribute("aria-hidden", "true");
|
|
425
|
+
panel.setAttribute("inert", "");
|
|
381
426
|
toggleButton.setAttribute("aria-expanded", "false");
|
|
382
427
|
toggleButton.setAttribute("aria-label", openLabel);
|
|
383
428
|
return;
|
|
384
429
|
}
|
|
385
430
|
isOpen = open;
|
|
431
|
+
if (!open && focusedInsidePanel) {
|
|
432
|
+
toggleButton.focus();
|
|
433
|
+
}
|
|
386
434
|
root.classList.toggle(openClassName, open);
|
|
435
|
+
panel.classList.toggle("is-open", open);
|
|
387
436
|
panel.setAttribute("aria-hidden", String(!open));
|
|
437
|
+
if (open) {
|
|
438
|
+
panel.removeAttribute("inert");
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
panel.setAttribute("inert", "");
|
|
442
|
+
}
|
|
388
443
|
toggleButton.setAttribute("aria-expanded", String(open));
|
|
389
444
|
toggleButton.setAttribute("aria-label", open ? closeLabel : openLabel);
|
|
390
445
|
};
|
|
@@ -577,12 +632,199 @@ export class SmoothPlayer {
|
|
|
577
632
|
offDurationChange();
|
|
578
633
|
};
|
|
579
634
|
}
|
|
635
|
+
mountAudioDrop(target, options = {}) {
|
|
636
|
+
const activeClassName = options.activeClassName ?? "is-drag-over";
|
|
637
|
+
const onDropCallback = options.onDrop;
|
|
638
|
+
let dragDepth = 0;
|
|
639
|
+
let activeObjectUrls = [];
|
|
640
|
+
const clearActiveState = () => {
|
|
641
|
+
dragDepth = 0;
|
|
642
|
+
target.classList.remove(activeClassName);
|
|
643
|
+
};
|
|
644
|
+
const revokeActiveUrls = () => {
|
|
645
|
+
for (const url of activeObjectUrls) {
|
|
646
|
+
URL.revokeObjectURL(url);
|
|
647
|
+
}
|
|
648
|
+
activeObjectUrls = [];
|
|
649
|
+
};
|
|
650
|
+
const isAudioFile = (file) => {
|
|
651
|
+
if (file.type.startsWith("audio/"))
|
|
652
|
+
return true;
|
|
653
|
+
return /\.(mp3|wav|ogg|m4a|aac|flac|opus)$/i.test(file.name);
|
|
654
|
+
};
|
|
655
|
+
const isPlaylistFile = (file) => {
|
|
656
|
+
if (/\.m3u8?$/i.test(file.name))
|
|
657
|
+
return true;
|
|
658
|
+
return /(?:application|audio)\/(?:vnd\.apple\.mpegurl|x-mpegurl)/i.test(file.type);
|
|
659
|
+
};
|
|
660
|
+
const baseName = (value) => {
|
|
661
|
+
const noQuery = value.split("?")[0]?.split("#")[0] ?? value;
|
|
662
|
+
const normalized = noQuery.replace(/\\/g, "/");
|
|
663
|
+
const tail = normalized.split("/").pop() ?? normalized;
|
|
664
|
+
try {
|
|
665
|
+
return decodeURIComponent(tail);
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
return tail;
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
const guessTitle = (value) => baseName(value).replace(/\.[^/.]+$/, "") || strings.track.unknownTitle;
|
|
672
|
+
const buildTrackFromAudioFile = (file, index) => {
|
|
673
|
+
const src = URL.createObjectURL(file);
|
|
674
|
+
activeObjectUrls.push(src);
|
|
675
|
+
const track = {
|
|
676
|
+
id: `dropped-${Date.now()}-${index}`,
|
|
677
|
+
src,
|
|
678
|
+
metadata: {
|
|
679
|
+
title: file.name.replace(/\.[^/.]+$/, ""),
|
|
680
|
+
artist: strings.track.localFileArtist,
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
if (file.type) {
|
|
684
|
+
track.type = file.type;
|
|
685
|
+
}
|
|
686
|
+
return track;
|
|
687
|
+
};
|
|
688
|
+
const parseM3U = (content, localAudioFiles) => {
|
|
689
|
+
const localByName = new Map();
|
|
690
|
+
localAudioFiles.forEach((file) => {
|
|
691
|
+
localByName.set(file.name.toLowerCase(), file);
|
|
692
|
+
});
|
|
693
|
+
const tracks = [];
|
|
694
|
+
const lines = content.split(/\r?\n/);
|
|
695
|
+
let extInfTitle = null;
|
|
696
|
+
lines.forEach((raw, index) => {
|
|
697
|
+
const line = raw.trim().replace(/^\uFEFF/, "");
|
|
698
|
+
if (!line)
|
|
699
|
+
return;
|
|
700
|
+
if (line.startsWith("#EXTINF:")) {
|
|
701
|
+
const commaIndex = line.indexOf(",");
|
|
702
|
+
extInfTitle = commaIndex >= 0 ? line.slice(commaIndex + 1).trim() : null;
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (line.startsWith("#"))
|
|
706
|
+
return;
|
|
707
|
+
const localFile = localByName.get(baseName(line).toLowerCase()) ?? null;
|
|
708
|
+
let src = "";
|
|
709
|
+
let type;
|
|
710
|
+
if (localFile) {
|
|
711
|
+
src = URL.createObjectURL(localFile);
|
|
712
|
+
activeObjectUrls.push(src);
|
|
713
|
+
type = localFile.type || undefined;
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
try {
|
|
717
|
+
src = new URL(line, window.location.href).href;
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
src = "";
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (!src) {
|
|
724
|
+
extInfTitle = null;
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
const track = {
|
|
728
|
+
id: `dropped-m3u-${Date.now()}-${index}`,
|
|
729
|
+
src,
|
|
730
|
+
metadata: {
|
|
731
|
+
title: extInfTitle || guessTitle(line),
|
|
732
|
+
artist: localFile ? strings.track.localFileArtist : strings.track.m3uArtist,
|
|
733
|
+
},
|
|
734
|
+
};
|
|
735
|
+
if (type) {
|
|
736
|
+
track.type = type;
|
|
737
|
+
}
|
|
738
|
+
tracks.push(track);
|
|
739
|
+
extInfTitle = null;
|
|
740
|
+
});
|
|
741
|
+
return tracks;
|
|
742
|
+
};
|
|
743
|
+
const hasFilePayload = (event) => {
|
|
744
|
+
const types = event.dataTransfer?.types;
|
|
745
|
+
if (!types)
|
|
746
|
+
return false;
|
|
747
|
+
return Array.from(types).includes("Files");
|
|
748
|
+
};
|
|
749
|
+
const onDragEnter = (event) => {
|
|
750
|
+
if (!hasFilePayload(event))
|
|
751
|
+
return;
|
|
752
|
+
event.preventDefault();
|
|
753
|
+
dragDepth += 1;
|
|
754
|
+
target.classList.add(activeClassName);
|
|
755
|
+
};
|
|
756
|
+
const onDragOver = (event) => {
|
|
757
|
+
if (!hasFilePayload(event))
|
|
758
|
+
return;
|
|
759
|
+
event.preventDefault();
|
|
760
|
+
if (event.dataTransfer) {
|
|
761
|
+
event.dataTransfer.dropEffect = "copy";
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
const onDragLeave = (event) => {
|
|
765
|
+
if (!hasFilePayload(event))
|
|
766
|
+
return;
|
|
767
|
+
event.preventDefault();
|
|
768
|
+
dragDepth = Math.max(0, dragDepth - 1);
|
|
769
|
+
if (dragDepth === 0) {
|
|
770
|
+
target.classList.remove(activeClassName);
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
const onDropEvent = async (event) => {
|
|
774
|
+
event.preventDefault();
|
|
775
|
+
clearActiveState();
|
|
776
|
+
const files = Array.from(event.dataTransfer?.files ?? []);
|
|
777
|
+
if (!files.length)
|
|
778
|
+
return;
|
|
779
|
+
const playlistFile = files.find(isPlaylistFile) ?? null;
|
|
780
|
+
const audioFiles = files.filter(isAudioFile);
|
|
781
|
+
let tracks = [];
|
|
782
|
+
let kind = "audio";
|
|
783
|
+
let sourceFile = null;
|
|
784
|
+
revokeActiveUrls();
|
|
785
|
+
if (playlistFile) {
|
|
786
|
+
kind = "playlist";
|
|
787
|
+
sourceFile = playlistFile;
|
|
788
|
+
const content = await playlistFile.text();
|
|
789
|
+
tracks = parseM3U(content, audioFiles);
|
|
790
|
+
}
|
|
791
|
+
else if (audioFiles.length) {
|
|
792
|
+
sourceFile = audioFiles[0] ?? null;
|
|
793
|
+
tracks = audioFiles.map((file, index) => buildTrackFromAudioFile(file, index));
|
|
794
|
+
}
|
|
795
|
+
const firstTrack = tracks[0];
|
|
796
|
+
if (!sourceFile || !firstTrack) {
|
|
797
|
+
this.events.emit("error", {
|
|
798
|
+
error: new Error(strings.errors.noPlayableTracksDropped),
|
|
799
|
+
});
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
this.setPlaylist(tracks, 0);
|
|
803
|
+
onDropCallback?.({ file: sourceFile, track: firstTrack, tracks, kind });
|
|
804
|
+
await this.play(0);
|
|
805
|
+
};
|
|
806
|
+
const onDrop = (event) => {
|
|
807
|
+
void onDropEvent(event);
|
|
808
|
+
};
|
|
809
|
+
target.addEventListener("dragenter", onDragEnter);
|
|
810
|
+
target.addEventListener("dragover", onDragOver);
|
|
811
|
+
target.addEventListener("dragleave", onDragLeave);
|
|
812
|
+
target.addEventListener("drop", onDrop);
|
|
813
|
+
return () => {
|
|
814
|
+
clearActiveState();
|
|
815
|
+
target.removeEventListener("dragenter", onDragEnter);
|
|
816
|
+
target.removeEventListener("dragover", onDragOver);
|
|
817
|
+
target.removeEventListener("dragleave", onDragLeave);
|
|
818
|
+
target.removeEventListener("drop", onDrop);
|
|
819
|
+
revokeActiveUrls();
|
|
820
|
+
};
|
|
821
|
+
}
|
|
580
822
|
async play(index) {
|
|
581
823
|
if (typeof index === "number") {
|
|
582
824
|
this.loadTrackByIndex(index);
|
|
583
825
|
}
|
|
584
826
|
if (!this.audio.src) {
|
|
585
|
-
throw new Error(
|
|
827
|
+
throw new Error(strings.errors.noTrackLoaded);
|
|
586
828
|
}
|
|
587
829
|
if (this.context.state === "suspended") {
|
|
588
830
|
await this.context.resume();
|
|
@@ -669,7 +911,10 @@ export class SmoothPlayer {
|
|
|
669
911
|
playlistTitle: playlist?.title ?? DEFAULT_PLAYLIST_TITLE,
|
|
670
912
|
playlistCount: this.playlists.length,
|
|
671
913
|
visualizer: this.visualizerMode,
|
|
914
|
+
spectrumStyle: { ...this.spectrumStyle },
|
|
915
|
+
waveformStyle: { ...this.waveformStyle },
|
|
672
916
|
accentColor: this.accentColor,
|
|
917
|
+
backgroundColor: this.backgroundColor,
|
|
673
918
|
shuffle: this.shuffleEnabled,
|
|
674
919
|
};
|
|
675
920
|
}
|
|
@@ -711,6 +956,18 @@ export class SmoothPlayer {
|
|
|
711
956
|
getVisualizer() {
|
|
712
957
|
return this.visualizerMode;
|
|
713
958
|
}
|
|
959
|
+
setSpectrumStyle(options) {
|
|
960
|
+
this.spectrumStyle = { ...this.spectrumStyle, ...options };
|
|
961
|
+
}
|
|
962
|
+
getSpectrumStyle() {
|
|
963
|
+
return { ...this.spectrumStyle };
|
|
964
|
+
}
|
|
965
|
+
setWaveformStyle(options) {
|
|
966
|
+
this.waveformStyle = { ...this.waveformStyle, ...options };
|
|
967
|
+
}
|
|
968
|
+
getWaveformStyle() {
|
|
969
|
+
return { ...this.waveformStyle };
|
|
970
|
+
}
|
|
714
971
|
configureAnalyzer(options = {}) {
|
|
715
972
|
const config = { ...DEFAULT_ANALYZER, ...options };
|
|
716
973
|
this.analyser.fftSize = config.fftSize;
|
|
@@ -718,6 +975,12 @@ export class SmoothPlayer {
|
|
|
718
975
|
this.analyser.minDecibels = config.minDecibels;
|
|
719
976
|
this.analyser.maxDecibels = config.maxDecibels;
|
|
720
977
|
}
|
|
978
|
+
buildSurfaceGradient(baseColor) {
|
|
979
|
+
return `linear-gradient(145deg, color-mix(in srgb, ${baseColor} 74%, #2a3f67 26%), color-mix(in srgb, ${baseColor} 82%, #15233e 18%) 56%, color-mix(in srgb, ${baseColor} 88%, #0a1324 12%))`;
|
|
980
|
+
}
|
|
981
|
+
buildPanelGradient(baseColor) {
|
|
982
|
+
return `linear-gradient(190deg, color-mix(in srgb, ${baseColor} 72%, #2a4069 28%) 0%, color-mix(in srgb, ${baseColor} 82%, #16243f 18%) 58%, color-mix(in srgb, ${baseColor} 88%, #0a1222 12%) 100%)`;
|
|
983
|
+
}
|
|
721
984
|
getActivePlaylist() {
|
|
722
985
|
if (!this.activePlaylistId)
|
|
723
986
|
return null;
|
|
@@ -767,11 +1030,31 @@ export class SmoothPlayer {
|
|
|
767
1030
|
this.events.emit("ended", undefined);
|
|
768
1031
|
});
|
|
769
1032
|
this.audio.addEventListener("error", () => {
|
|
1033
|
+
const src = this.audio.currentSrc || this.audio.src || "";
|
|
1034
|
+
const mediaError = this.audio.error;
|
|
1035
|
+
const isCrossOrigin = this.isCrossOriginSource(src);
|
|
1036
|
+
const isPossiblyCors = isCrossOrigin && mediaError?.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED;
|
|
1037
|
+
const message = isPossiblyCors
|
|
1038
|
+
? strings.errors.corsBlocked
|
|
1039
|
+
: strings.errors.audioPlaybackFailed;
|
|
770
1040
|
this.events.emit("error", {
|
|
771
|
-
error: new Error(
|
|
1041
|
+
error: new Error(message),
|
|
772
1042
|
});
|
|
773
1043
|
});
|
|
774
1044
|
}
|
|
1045
|
+
isCrossOriginSource(src) {
|
|
1046
|
+
if (!src)
|
|
1047
|
+
return false;
|
|
1048
|
+
if (src.startsWith("blob:") || src.startsWith("data:") || src.startsWith("file:"))
|
|
1049
|
+
return false;
|
|
1050
|
+
try {
|
|
1051
|
+
const url = new URL(src, window.location.href);
|
|
1052
|
+
return url.origin !== window.location.origin;
|
|
1053
|
+
}
|
|
1054
|
+
catch {
|
|
1055
|
+
return false;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
775
1058
|
clamp(value) {
|
|
776
1059
|
return Math.min(1, Math.max(0, value));
|
|
777
1060
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export declare const en: {
|
|
2
|
+
readonly playlist: {
|
|
3
|
+
readonly defaultId: "default";
|
|
4
|
+
readonly defaultTitle: "My playlist";
|
|
5
|
+
readonly triggerLabel: "Playlist";
|
|
6
|
+
readonly openLabel: "Open playlist";
|
|
7
|
+
readonly closeLabel: "Close playlist";
|
|
8
|
+
};
|
|
9
|
+
readonly playback: {
|
|
10
|
+
readonly playLabel: "Play";
|
|
11
|
+
readonly pauseLabel: "Pause";
|
|
12
|
+
readonly stopLabel: "Stop";
|
|
13
|
+
};
|
|
14
|
+
readonly shuffle: {
|
|
15
|
+
readonly enabledLabel: "Disable shuffle";
|
|
16
|
+
readonly disabledLabel: "Enable shuffle";
|
|
17
|
+
};
|
|
18
|
+
readonly visualizer: {
|
|
19
|
+
readonly toggleLabel: "Change visualizer";
|
|
20
|
+
readonly panelTitle: "Visualizer options";
|
|
21
|
+
readonly closeLabel: "Close visualizer options";
|
|
22
|
+
readonly modeLabel: "Mode";
|
|
23
|
+
readonly effectLabel: "Spectrum effects";
|
|
24
|
+
readonly effectDualLayer: "Bi-directional";
|
|
25
|
+
readonly effectInverted: "Inverted";
|
|
26
|
+
readonly barWidthLabel: "Bar width";
|
|
27
|
+
readonly barWidthThin: "Thin";
|
|
28
|
+
readonly barWidthMedium: "Medium";
|
|
29
|
+
readonly barWidthLarge: "Large";
|
|
30
|
+
readonly waveformEffectLabel: "Waveform effects";
|
|
31
|
+
readonly waveformEffectDoubleLine: "Double line";
|
|
32
|
+
readonly waveformEffectFill: "Fill";
|
|
33
|
+
readonly waveformEffectThickLine: "Thick line";
|
|
34
|
+
readonly modeSpectrum: "Spectrum";
|
|
35
|
+
readonly modeWaveform: "Waveform";
|
|
36
|
+
readonly modeNone: "Off";
|
|
37
|
+
};
|
|
38
|
+
readonly track: {
|
|
39
|
+
readonly unknownTitle: "Unknown title";
|
|
40
|
+
readonly unknownArtist: "Unknown artist";
|
|
41
|
+
readonly localFileArtist: "Local file";
|
|
42
|
+
readonly m3uArtist: "M3U playlist";
|
|
43
|
+
};
|
|
44
|
+
readonly errors: {
|
|
45
|
+
readonly noTrackLoaded: "No track loaded. Use playlist option, setPlaylist(), or loadTrack().";
|
|
46
|
+
readonly noPlayableTracksDropped: "No playable tracks found in dropped files.";
|
|
47
|
+
readonly audioPlaybackFailed: "Audio playback failed.";
|
|
48
|
+
readonly corsBlocked: "Unable to play this audio source. It may be blocked by CORS.";
|
|
49
|
+
};
|
|
50
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/* Auto-generated from src/i18n/en.json. Do not edit manually. */
|
|
2
|
+
export const en = {
|
|
3
|
+
"playlist": {
|
|
4
|
+
"defaultId": "default",
|
|
5
|
+
"defaultTitle": "My playlist",
|
|
6
|
+
"triggerLabel": "Playlist",
|
|
7
|
+
"openLabel": "Open playlist",
|
|
8
|
+
"closeLabel": "Close playlist"
|
|
9
|
+
},
|
|
10
|
+
"playback": {
|
|
11
|
+
"playLabel": "Play",
|
|
12
|
+
"pauseLabel": "Pause",
|
|
13
|
+
"stopLabel": "Stop"
|
|
14
|
+
},
|
|
15
|
+
"shuffle": {
|
|
16
|
+
"enabledLabel": "Disable shuffle",
|
|
17
|
+
"disabledLabel": "Enable shuffle"
|
|
18
|
+
},
|
|
19
|
+
"visualizer": {
|
|
20
|
+
"toggleLabel": "Change visualizer",
|
|
21
|
+
"panelTitle": "Visualizer options",
|
|
22
|
+
"closeLabel": "Close visualizer options",
|
|
23
|
+
"modeLabel": "Mode",
|
|
24
|
+
"effectLabel": "Spectrum effects",
|
|
25
|
+
"effectDualLayer": "Bi-directional",
|
|
26
|
+
"effectInverted": "Inverted",
|
|
27
|
+
"barWidthLabel": "Bar width",
|
|
28
|
+
"barWidthThin": "Thin",
|
|
29
|
+
"barWidthMedium": "Medium",
|
|
30
|
+
"barWidthLarge": "Large",
|
|
31
|
+
"waveformEffectLabel": "Waveform effects",
|
|
32
|
+
"waveformEffectDoubleLine": "Double line",
|
|
33
|
+
"waveformEffectFill": "Fill",
|
|
34
|
+
"waveformEffectThickLine": "Thick line",
|
|
35
|
+
"modeSpectrum": "Spectrum",
|
|
36
|
+
"modeWaveform": "Waveform",
|
|
37
|
+
"modeNone": "Off"
|
|
38
|
+
},
|
|
39
|
+
"track": {
|
|
40
|
+
"unknownTitle": "Unknown title",
|
|
41
|
+
"unknownArtist": "Unknown artist",
|
|
42
|
+
"localFileArtist": "Local file",
|
|
43
|
+
"m3uArtist": "M3U playlist"
|
|
44
|
+
},
|
|
45
|
+
"errors": {
|
|
46
|
+
"noTrackLoaded": "No track loaded. Use playlist option, setPlaylist(), or loadTrack().",
|
|
47
|
+
"noPlayableTracksDropped": "No playable tracks found in dropped files.",
|
|
48
|
+
"audioPlaybackFailed": "Audio playback failed.",
|
|
49
|
+
"corsBlocked": "Unable to play this audio source. It may be blocked by CORS."
|
|
50
|
+
}
|
|
51
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface SmoothPlayerStrings {
|
|
2
|
+
playlist: {
|
|
3
|
+
defaultId: string;
|
|
4
|
+
defaultTitle: string;
|
|
5
|
+
triggerLabel: string;
|
|
6
|
+
openLabel: string;
|
|
7
|
+
closeLabel: string;
|
|
8
|
+
};
|
|
9
|
+
playback: {
|
|
10
|
+
playLabel: string;
|
|
11
|
+
pauseLabel: string;
|
|
12
|
+
stopLabel: string;
|
|
13
|
+
};
|
|
14
|
+
shuffle: {
|
|
15
|
+
enabledLabel: string;
|
|
16
|
+
disabledLabel: string;
|
|
17
|
+
};
|
|
18
|
+
visualizer: {
|
|
19
|
+
toggleLabel: string;
|
|
20
|
+
panelTitle: string;
|
|
21
|
+
closeLabel: string;
|
|
22
|
+
modeLabel: string;
|
|
23
|
+
effectLabel: string;
|
|
24
|
+
effectDualLayer: string;
|
|
25
|
+
effectInverted: string;
|
|
26
|
+
barWidthLabel: string;
|
|
27
|
+
barWidthThin: string;
|
|
28
|
+
barWidthMedium: string;
|
|
29
|
+
barWidthLarge: string;
|
|
30
|
+
waveformEffectLabel: string;
|
|
31
|
+
waveformEffectDoubleLine: string;
|
|
32
|
+
waveformEffectFill: string;
|
|
33
|
+
waveformEffectThickLine: string;
|
|
34
|
+
modeSpectrum: string;
|
|
35
|
+
modeWaveform: string;
|
|
36
|
+
modeNone: string;
|
|
37
|
+
};
|
|
38
|
+
track: {
|
|
39
|
+
unknownTitle: string;
|
|
40
|
+
unknownArtist: string;
|
|
41
|
+
localFileArtist: string;
|
|
42
|
+
m3uArtist: string;
|
|
43
|
+
};
|
|
44
|
+
errors: {
|
|
45
|
+
noTrackLoaded: string;
|
|
46
|
+
noPlayableTracksDropped: string;
|
|
47
|
+
audioPlaybackFailed: string;
|
|
48
|
+
corsBlocked: string;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export declare const strings: SmoothPlayerStrings;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { SmoothPlayer } from "./SmoothPlayer.js";
|
|
2
|
-
export {
|
|
2
|
+
export { mountPlayerUI } from "./ui.js";
|
|
3
|
+
export { strings } from "./i18n/strings.js";
|
|
4
|
+
export type { SmoothPlayerStrings } from "./i18n/strings.js";
|
|
3
5
|
export { CanvasRadialVisualizer, CanvasSpectrumVisualizer, CanvasWaveformVisualizer, type RadialVisualizerOptions, type SpectrumVisualizerOptions, type WaveformVisualizerOptions, } from "./visualizers.js";
|
|
4
|
-
export type { AnalyzerOptions, AudioPlaylist, AudioTrack, PlaybackState, PlaylistPanelController, PlaylistPanelMountOptions, PlaylistEntry, PlaylistMountOptions, PlaylistSwitcherMountOptions, PlaylistTitleMountOptions, PlayButtonMountOptions, ProgressMountOptions, PlayerEvents, SmoothPlayerOptions, ShuffleToggleMountOptions, DebugPanelMountOptions, TransportControlsMountOptions, StandardPlayerUIController, StandardPlayerUIMountOptions, TrackInfoMountOptions, TrackMetadata, VisualizerMode, } from "./types.js";
|
|
6
|
+
export type { AnalyzerOptions, AudioDropMountOptions, AudioPlaylist, AudioTrack, PlaybackState, PlaylistPanelController, PlaylistPanelMountOptions, PlaylistEntry, PlaylistMountOptions, PlaylistSwitcherMountOptions, PlaylistTitleMountOptions, PlayButtonMountOptions, ProgressMountOptions, PlayerEvents, SmoothPlayerOptions, ShuffleToggleMountOptions, DebugPanelMountOptions, TransportControlsMountOptions, StandardPlayerUIController, StandardPlayerUIMountOptions, SpectrumStyleOptions, TrackInfoMountOptions, TrackMetadata, VisualizerMode, WaveformStyleOptions, } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { SmoothPlayer } from "./SmoothPlayer.js";
|
|
2
|
-
export {
|
|
2
|
+
export { mountPlayerUI } from "./ui.js";
|
|
3
|
+
export { strings } from "./i18n/strings.js";
|
|
3
4
|
export { CanvasRadialVisualizer, CanvasSpectrumVisualizer, CanvasWaveformVisualizer, } from "./visualizers.js";
|