@vanduo-oss/framework 1.3.1 → 1.3.2

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.
@@ -1,4 +1,4 @@
1
- /*! Vanduo v1.3.1 | Built: 2026-03-20T21:48:40.922Z | git:7e73bb8 | development */
1
+ /*! Vanduo v1.3.2 | Built: 2026-04-06T19:04:41.601Z | git:8e08b38 | development */
2
2
  var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -132,7 +132,7 @@ module.exports = __toCommonJS(index_exports);
132
132
  // js/vanduo.js
133
133
  (function() {
134
134
  "use strict";
135
- const VANDUO_VERSION = true ? "1.3.1" : "0.0.0-dev";
135
+ const VANDUO_VERSION = true ? "1.3.2" : "0.0.0-dev";
136
136
  const Vanduo2 = {
137
137
  version: VANDUO_VERSION,
138
138
  components: {},
@@ -8565,6 +8565,667 @@ module.exports = __toCommonJS(index_exports);
8565
8565
  window.VanduoSpotlight = Spotlight;
8566
8566
  })();
8567
8567
 
8568
+ // js/components/music-player.js
8569
+ (function() {
8570
+ "use strict";
8571
+ function shuffleArray(arr) {
8572
+ const shuffled = arr.slice();
8573
+ for (let i = shuffled.length - 1; i > 0; i--) {
8574
+ const j = Math.floor(Math.random() * (i + 1));
8575
+ const tmp = shuffled[i];
8576
+ shuffled[i] = shuffled[j];
8577
+ shuffled[j] = tmp;
8578
+ }
8579
+ return shuffled;
8580
+ }
8581
+ function formatTime(seconds) {
8582
+ if (!isFinite(seconds) || seconds < 0) return "0:00";
8583
+ const m = Math.floor(seconds / 60);
8584
+ const s = Math.floor(seconds % 60);
8585
+ return m + ":" + (s < 10 ? "0" : "") + s;
8586
+ }
8587
+ function updateRangeFill(input) {
8588
+ const min = parseFloat(input.min) || 0;
8589
+ const max = parseFloat(input.max) || 1;
8590
+ const val = parseFloat(input.value) || 0;
8591
+ const pct = (val - min) / (max - min) * 100;
8592
+ input.style.setProperty("--fill", pct + "%");
8593
+ input.style.backgroundImage = "linear-gradient(to right, var(--music-player-track-fill, currentColor) 0%, var(--music-player-track-fill, currentColor) " + pct + "%, var(--music-player-track-bg, #ccc) " + pct + "%, var(--music-player-track-bg, #ccc) 100%)";
8594
+ }
8595
+ function icon(name) {
8596
+ const el = document.createElement("i");
8597
+ el.className = "ph ph-" + name;
8598
+ el.setAttribute("aria-hidden", "true");
8599
+ return el;
8600
+ }
8601
+ const MusicPlayer = {
8602
+ /** @type {Map<HTMLElement, Object>} */
8603
+ instances: /* @__PURE__ */ new Map(),
8604
+ /**
8605
+ * Default options.
8606
+ */
8607
+ defaults: {
8608
+ tracks: [],
8609
+ volume: 0.5,
8610
+ shuffle: false,
8611
+ showProgress: false,
8612
+ showPlaylist: false,
8613
+ autoAdvance: true
8614
+ },
8615
+ /**
8616
+ * Auto-initialize all .vd-music-player / [data-music-player] elements.
8617
+ * Options can be provided via data-music-player-options (JSON string).
8618
+ */
8619
+ init: function() {
8620
+ document.querySelectorAll(".vd-music-player, [data-music-player]").forEach((el) => {
8621
+ if (this.instances.has(el)) return;
8622
+ let opts = {};
8623
+ const attr = el.getAttribute("data-music-player-options");
8624
+ if (attr) {
8625
+ try {
8626
+ opts = JSON.parse(attr);
8627
+ } catch (_) {
8628
+ }
8629
+ }
8630
+ this.initPlayer(el, opts);
8631
+ });
8632
+ },
8633
+ /**
8634
+ * Initialize a single player element.
8635
+ * @param {HTMLElement} container
8636
+ * @param {Object} [options]
8637
+ */
8638
+ initPlayer: function(container, options) {
8639
+ const opts = Object.assign({}, this.defaults, options || {});
8640
+ const rawTracks = Array.isArray(opts.tracks) ? opts.tracks : [];
8641
+ const tracks = rawTracks.filter((t) => t && typeof t.url === "string" && t.url.trim());
8642
+ const trackList = opts.shuffle ? shuffleArray(tracks) : tracks.slice();
8643
+ const state = {
8644
+ tracks: trackList,
8645
+ originalTracks: tracks.slice(),
8646
+ currentIndex: 0,
8647
+ isPlaying: false,
8648
+ volume: Math.max(0, Math.min(1, opts.volume)),
8649
+ shuffle: opts.shuffle,
8650
+ showProgress: opts.showProgress,
8651
+ showPlaylist: opts.showPlaylist,
8652
+ autoAdvance: opts.autoAdvance,
8653
+ audio: null
8654
+ };
8655
+ const audio = new Audio();
8656
+ audio.volume = state.volume;
8657
+ audio.preload = "metadata";
8658
+ state.audio = audio;
8659
+ this._buildDOM(container, state);
8660
+ const refs = {
8661
+ btnPlay: container.querySelector(".vd-music-player-btn-play"),
8662
+ btnPrev: container.querySelector(".vd-music-player-btn-prev"),
8663
+ btnNext: container.querySelector(".vd-music-player-btn-next"),
8664
+ btnShuffle: container.querySelector(".vd-music-player-btn-shuffle"),
8665
+ btnPlaylist: container.querySelector(".vd-music-player-btn-playlist"),
8666
+ trackName: container.querySelector(".vd-music-player-track-name"),
8667
+ volumeSlider: container.querySelector(".vd-music-player-volume-slider"),
8668
+ volumeIcon: container.querySelector(".vd-music-player-volume-icon"),
8669
+ progressBar: container.querySelector(".vd-music-player-progress-bar"),
8670
+ timeElapsed: container.querySelector(".vd-music-player-time-elapsed"),
8671
+ timeDuration: container.querySelector(".vd-music-player-time-duration"),
8672
+ playlistPanel: container.querySelector(".vd-music-player-playlist")
8673
+ };
8674
+ const renderPlayIcon = () => {
8675
+ const btn = refs.btnPlay;
8676
+ if (!btn) return;
8677
+ btn.innerHTML = "";
8678
+ btn.appendChild(icon(state.isPlaying ? "pause" : "play"));
8679
+ btn.setAttribute("aria-label", state.isPlaying ? "Pause" : "Play");
8680
+ btn.classList.toggle("is-active", state.isPlaying);
8681
+ };
8682
+ const renderTrackName = () => {
8683
+ const el = refs.trackName;
8684
+ if (!el) return;
8685
+ const track = state.tracks[state.currentIndex];
8686
+ if (track) {
8687
+ el.textContent = track.name || "Unknown Track";
8688
+ el.classList.remove("is-idle");
8689
+ } else {
8690
+ el.textContent = "No tracks loaded";
8691
+ el.classList.add("is-idle");
8692
+ }
8693
+ };
8694
+ const renderVolumeIcon = () => {
8695
+ const el = refs.volumeIcon;
8696
+ if (!el) return;
8697
+ el.innerHTML = "";
8698
+ const v = state.volume;
8699
+ const name = v === 0 ? "speaker-none" : v < 0.5 ? "speaker-low" : "speaker-high";
8700
+ el.appendChild(icon(name));
8701
+ };
8702
+ const renderShuffleBtn = () => {
8703
+ const btn = refs.btnShuffle;
8704
+ if (!btn) return;
8705
+ btn.classList.toggle("is-active", state.shuffle);
8706
+ btn.setAttribute("aria-pressed", state.shuffle ? "true" : "false");
8707
+ };
8708
+ const renderPlaylistItems = () => {
8709
+ const panel = refs.playlistPanel;
8710
+ if (!panel) return;
8711
+ panel.innerHTML = "";
8712
+ state.tracks.forEach((track, i) => {
8713
+ const item = document.createElement("button");
8714
+ item.className = "vd-music-player-playlist-item" + (i === state.currentIndex ? " is-active" : "");
8715
+ item.type = "button";
8716
+ item.setAttribute("data-index", String(i));
8717
+ item.setAttribute("aria-current", i === state.currentIndex ? "true" : "false");
8718
+ const num = document.createElement("span");
8719
+ num.className = "vd-music-player-playlist-num";
8720
+ num.textContent = String(i + 1);
8721
+ const name = document.createElement("span");
8722
+ name.className = "vd-music-player-playlist-name";
8723
+ name.textContent = track.name || "Track " + (i + 1);
8724
+ item.appendChild(num);
8725
+ item.appendChild(name);
8726
+ panel.appendChild(item);
8727
+ });
8728
+ };
8729
+ const renderProgress = () => {
8730
+ const bar = refs.progressBar;
8731
+ if (!bar || !audio.duration) return;
8732
+ const pct = audio.currentTime / audio.duration * 100;
8733
+ bar.value = String(pct);
8734
+ updateRangeFill(bar);
8735
+ if (refs.timeElapsed) refs.timeElapsed.textContent = formatTime(audio.currentTime);
8736
+ if (refs.timeDuration) refs.timeDuration.textContent = formatTime(audio.duration);
8737
+ };
8738
+ const loadTrack = (index, autoPlay) => {
8739
+ const track = state.tracks[index];
8740
+ if (!track) return;
8741
+ state.currentIndex = index;
8742
+ audio.src = track.url;
8743
+ renderTrackName();
8744
+ renderPlaylistItems();
8745
+ if (refs.progressBar) {
8746
+ refs.progressBar.value = "0";
8747
+ updateRangeFill(refs.progressBar);
8748
+ }
8749
+ if (refs.timeElapsed) refs.timeElapsed.textContent = "0:00";
8750
+ if (refs.timeDuration) refs.timeDuration.textContent = "0:00";
8751
+ container.dispatchEvent(
8752
+ new CustomEvent("musicplayer:trackchange", {
8753
+ bubbles: true,
8754
+ detail: { index, name: track.name, url: track.url }
8755
+ })
8756
+ );
8757
+ if (autoPlay) {
8758
+ audio.play().catch(() => {
8759
+ });
8760
+ }
8761
+ };
8762
+ const cleanupFunctions = [];
8763
+ const onPlay = () => {
8764
+ state.isPlaying = true;
8765
+ renderPlayIcon();
8766
+ container.dispatchEvent(new CustomEvent("musicplayer:play", { bubbles: true }));
8767
+ };
8768
+ const onPause = () => {
8769
+ state.isPlaying = false;
8770
+ renderPlayIcon();
8771
+ container.dispatchEvent(new CustomEvent("musicplayer:pause", { bubbles: true }));
8772
+ };
8773
+ const onEnded = () => {
8774
+ if (state.autoAdvance && state.tracks.length > 1) {
8775
+ const next = (state.currentIndex + 1) % state.tracks.length;
8776
+ loadTrack(next, true);
8777
+ } else {
8778
+ state.isPlaying = false;
8779
+ renderPlayIcon();
8780
+ container.dispatchEvent(new CustomEvent("musicplayer:ended", { bubbles: true }));
8781
+ }
8782
+ };
8783
+ const onTimeUpdate = () => {
8784
+ if (state.showProgress) renderProgress();
8785
+ };
8786
+ const onLoadedMetadata = () => {
8787
+ if (refs.timeDuration) refs.timeDuration.textContent = formatTime(audio.duration);
8788
+ if (refs.progressBar) {
8789
+ refs.progressBar.max = "100";
8790
+ updateRangeFill(refs.progressBar);
8791
+ }
8792
+ };
8793
+ audio.addEventListener("play", onPlay);
8794
+ audio.addEventListener("pause", onPause);
8795
+ audio.addEventListener("ended", onEnded);
8796
+ audio.addEventListener("timeupdate", onTimeUpdate);
8797
+ audio.addEventListener("loadedmetadata", onLoadedMetadata);
8798
+ cleanupFunctions.push(() => {
8799
+ audio.removeEventListener("play", onPlay);
8800
+ audio.removeEventListener("pause", onPause);
8801
+ audio.removeEventListener("ended", onEnded);
8802
+ audio.removeEventListener("timeupdate", onTimeUpdate);
8803
+ audio.removeEventListener("loadedmetadata", onLoadedMetadata);
8804
+ audio.pause();
8805
+ audio.src = "";
8806
+ });
8807
+ if (refs.btnPlay) {
8808
+ const handler = () => {
8809
+ if (!audio.src && state.tracks.length) loadTrack(state.currentIndex, false);
8810
+ if (state.isPlaying) {
8811
+ audio.pause();
8812
+ } else {
8813
+ audio.play().catch(() => {
8814
+ });
8815
+ }
8816
+ };
8817
+ refs.btnPlay.addEventListener("click", handler);
8818
+ cleanupFunctions.push(() => refs.btnPlay.removeEventListener("click", handler));
8819
+ const keyHandler = (e) => {
8820
+ if (e.key === " " || e.key === "Enter") {
8821
+ e.preventDefault();
8822
+ handler();
8823
+ }
8824
+ };
8825
+ refs.btnPlay.addEventListener("keydown", keyHandler);
8826
+ cleanupFunctions.push(() => refs.btnPlay.removeEventListener("keydown", keyHandler));
8827
+ }
8828
+ if (refs.btnPrev) {
8829
+ const handler = () => {
8830
+ if (!state.tracks.length) return;
8831
+ if (audio.currentTime > 3) {
8832
+ audio.currentTime = 0;
8833
+ } else {
8834
+ const prev = state.currentIndex === 0 ? state.tracks.length - 1 : state.currentIndex - 1;
8835
+ loadTrack(prev, state.isPlaying);
8836
+ }
8837
+ };
8838
+ refs.btnPrev.addEventListener("click", handler);
8839
+ cleanupFunctions.push(() => refs.btnPrev.removeEventListener("click", handler));
8840
+ }
8841
+ if (refs.btnNext) {
8842
+ const handler = () => {
8843
+ if (!state.tracks.length) return;
8844
+ const next = (state.currentIndex + 1) % state.tracks.length;
8845
+ loadTrack(next, state.isPlaying);
8846
+ };
8847
+ refs.btnNext.addEventListener("click", handler);
8848
+ cleanupFunctions.push(() => refs.btnNext.removeEventListener("click", handler));
8849
+ }
8850
+ if (refs.btnShuffle) {
8851
+ const handler = () => {
8852
+ state.shuffle = !state.shuffle;
8853
+ if (state.shuffle) {
8854
+ const current = state.tracks[state.currentIndex];
8855
+ state.tracks = shuffleArray(state.tracks);
8856
+ const newIdx = state.tracks.findIndex((t) => t === current);
8857
+ if (newIdx > 0) {
8858
+ state.tracks.splice(newIdx, 1);
8859
+ state.tracks.unshift(current);
8860
+ }
8861
+ state.currentIndex = 0;
8862
+ } else {
8863
+ const current = state.tracks[state.currentIndex];
8864
+ state.tracks = state.originalTracks.slice();
8865
+ state.currentIndex = state.tracks.findIndex((t) => t === current);
8866
+ if (state.currentIndex < 0) state.currentIndex = 0;
8867
+ }
8868
+ renderShuffleBtn();
8869
+ renderPlaylistItems();
8870
+ };
8871
+ refs.btnShuffle.addEventListener("click", handler);
8872
+ cleanupFunctions.push(() => refs.btnShuffle.removeEventListener("click", handler));
8873
+ }
8874
+ if (refs.btnPlaylist) {
8875
+ const handler = () => {
8876
+ const panel = refs.playlistPanel;
8877
+ if (!panel) return;
8878
+ const isOpen = panel.classList.toggle("is-open");
8879
+ refs.btnPlaylist.classList.toggle("is-active", isOpen);
8880
+ refs.btnPlaylist.setAttribute("aria-expanded", isOpen ? "true" : "false");
8881
+ };
8882
+ refs.btnPlaylist.addEventListener("click", handler);
8883
+ cleanupFunctions.push(() => refs.btnPlaylist.removeEventListener("click", handler));
8884
+ }
8885
+ if (refs.volumeSlider) {
8886
+ const handler = (e) => {
8887
+ const v = parseFloat(e.target.value);
8888
+ state.volume = v;
8889
+ audio.volume = v;
8890
+ renderVolumeIcon();
8891
+ updateRangeFill(refs.volumeSlider);
8892
+ container.dispatchEvent(
8893
+ new CustomEvent("musicplayer:volumechange", { bubbles: true, detail: { volume: v } })
8894
+ );
8895
+ };
8896
+ refs.volumeSlider.addEventListener("input", handler);
8897
+ cleanupFunctions.push(() => refs.volumeSlider.removeEventListener("input", handler));
8898
+ updateRangeFill(refs.volumeSlider);
8899
+ }
8900
+ if (refs.progressBar) {
8901
+ const handler = (e) => {
8902
+ if (!audio.duration) return;
8903
+ const pct = parseFloat(e.target.value);
8904
+ audio.currentTime = pct / 100 * audio.duration;
8905
+ updateRangeFill(refs.progressBar);
8906
+ };
8907
+ refs.progressBar.addEventListener("input", handler);
8908
+ cleanupFunctions.push(() => refs.progressBar.removeEventListener("input", handler));
8909
+ }
8910
+ if (refs.playlistPanel) {
8911
+ const panelHandler = (e) => {
8912
+ const item = e.target.closest(".vd-music-player-playlist-item");
8913
+ if (!item) return;
8914
+ const idx = parseInt(item.getAttribute("data-index"), 10);
8915
+ if (!isNaN(idx)) loadTrack(idx, true);
8916
+ };
8917
+ refs.playlistPanel.addEventListener("click", panelHandler);
8918
+ cleanupFunctions.push(
8919
+ () => refs.playlistPanel.removeEventListener("click", panelHandler)
8920
+ );
8921
+ }
8922
+ renderPlayIcon();
8923
+ renderTrackName();
8924
+ renderVolumeIcon();
8925
+ if (opts.showPlaylist) renderPlaylistItems();
8926
+ this.instances.set(container, { state, audio, refs, cleanup: cleanupFunctions });
8927
+ container.setAttribute("data-music-player-initialized", "true");
8928
+ },
8929
+ /* ─── DOM builder ─────────────────────────────────────── */
8930
+ /**
8931
+ * Build the inner DOM structure inside container.
8932
+ * Pre-existing inner content is replaced only if it has no
8933
+ * recognised child elements (allows server-rendered markup).
8934
+ * @param {HTMLElement} container
8935
+ * @param {Object} state
8936
+ */
8937
+ _buildDOM: function(container, state) {
8938
+ if (container.querySelector(".vd-music-player-controls")) return;
8939
+ container.setAttribute("role", "region");
8940
+ container.setAttribute("aria-label", "Music Player");
8941
+ if (state.showProgress) container.classList.add("has-progress");
8942
+ if (state.showPlaylist) container.classList.add("has-playlist");
8943
+ const info = document.createElement("div");
8944
+ info.className = "vd-music-player-info";
8945
+ const iconWrap = document.createElement("span");
8946
+ iconWrap.className = "vd-music-player-icon";
8947
+ iconWrap.setAttribute("aria-hidden", "true");
8948
+ iconWrap.appendChild(icon("music-note"));
8949
+ const trackName = document.createElement("span");
8950
+ trackName.className = "vd-music-player-track-name";
8951
+ trackName.setAttribute("aria-live", "polite");
8952
+ trackName.setAttribute("aria-atomic", "true");
8953
+ info.appendChild(iconWrap);
8954
+ info.appendChild(trackName);
8955
+ container.appendChild(info);
8956
+ const controls = document.createElement("div");
8957
+ controls.className = "vd-music-player-controls";
8958
+ controls.setAttribute("role", "group");
8959
+ controls.setAttribute("aria-label", "Playback controls");
8960
+ const btnPrev = document.createElement("button");
8961
+ btnPrev.type = "button";
8962
+ btnPrev.className = "vd-music-player-btn vd-music-player-btn-prev";
8963
+ btnPrev.setAttribute("aria-label", "Previous track");
8964
+ btnPrev.appendChild(icon("skip-back"));
8965
+ const btnPlay = document.createElement("button");
8966
+ btnPlay.type = "button";
8967
+ btnPlay.className = "vd-music-player-btn vd-music-player-btn-play";
8968
+ btnPlay.setAttribute("aria-label", "Play");
8969
+ btnPlay.appendChild(icon("play"));
8970
+ const btnNext = document.createElement("button");
8971
+ btnNext.type = "button";
8972
+ btnNext.className = "vd-music-player-btn vd-music-player-btn-next";
8973
+ btnNext.setAttribute("aria-label", "Next track");
8974
+ btnNext.appendChild(icon("skip-forward"));
8975
+ controls.appendChild(btnPrev);
8976
+ controls.appendChild(btnPlay);
8977
+ controls.appendChild(btnNext);
8978
+ if (state.showPlaylist || state.shuffle !== void 0) {
8979
+ const btnShuffle = document.createElement("button");
8980
+ btnShuffle.type = "button";
8981
+ btnShuffle.className = "vd-music-player-btn vd-music-player-btn-shuffle";
8982
+ btnShuffle.setAttribute("aria-label", "Shuffle");
8983
+ btnShuffle.setAttribute("aria-pressed", state.shuffle ? "true" : "false");
8984
+ btnShuffle.appendChild(icon("shuffle"));
8985
+ controls.appendChild(btnShuffle);
8986
+ }
8987
+ const spacer = document.createElement("span");
8988
+ spacer.className = "vd-music-player-spacer";
8989
+ spacer.setAttribute("aria-hidden", "true");
8990
+ controls.appendChild(spacer);
8991
+ const volumeWrap = document.createElement("div");
8992
+ volumeWrap.className = "vd-music-player-volume";
8993
+ const volumeIcon = document.createElement("span");
8994
+ volumeIcon.className = "vd-music-player-volume-icon";
8995
+ volumeIcon.setAttribute("aria-hidden", "true");
8996
+ const volumeSlider = document.createElement("input");
8997
+ volumeSlider.type = "range";
8998
+ volumeSlider.className = "vd-music-player-volume-slider";
8999
+ volumeSlider.min = "0";
9000
+ volumeSlider.max = "1";
9001
+ volumeSlider.step = "0.01";
9002
+ volumeSlider.value = String(state.volume);
9003
+ volumeSlider.setAttribute("aria-label", "Volume");
9004
+ volumeWrap.appendChild(volumeIcon);
9005
+ volumeWrap.appendChild(volumeSlider);
9006
+ controls.appendChild(volumeWrap);
9007
+ if (state.showPlaylist) {
9008
+ const btnPlaylist = document.createElement("button");
9009
+ btnPlaylist.type = "button";
9010
+ btnPlaylist.className = "vd-music-player-btn vd-music-player-btn-playlist";
9011
+ btnPlaylist.setAttribute("aria-label", "Show playlist");
9012
+ btnPlaylist.setAttribute("aria-expanded", "false");
9013
+ btnPlaylist.appendChild(icon("playlist"));
9014
+ controls.appendChild(btnPlaylist);
9015
+ }
9016
+ container.appendChild(controls);
9017
+ if (state.showProgress) {
9018
+ const progressRow = document.createElement("div");
9019
+ progressRow.className = "vd-music-player-progress";
9020
+ const timeElapsed = document.createElement("span");
9021
+ timeElapsed.className = "vd-music-player-time vd-music-player-time-elapsed";
9022
+ timeElapsed.textContent = "0:00";
9023
+ timeElapsed.setAttribute("aria-hidden", "true");
9024
+ const progressBar = document.createElement("input");
9025
+ progressBar.type = "range";
9026
+ progressBar.className = "vd-music-player-progress-bar";
9027
+ progressBar.min = "0";
9028
+ progressBar.max = "100";
9029
+ progressBar.step = "0.1";
9030
+ progressBar.value = "0";
9031
+ progressBar.setAttribute("aria-label", "Seek");
9032
+ const timeDuration = document.createElement("span");
9033
+ timeDuration.className = "vd-music-player-time vd-music-player-time-duration";
9034
+ timeDuration.textContent = "0:00";
9035
+ timeDuration.setAttribute("aria-hidden", "true");
9036
+ progressRow.appendChild(timeElapsed);
9037
+ progressRow.appendChild(progressBar);
9038
+ progressRow.appendChild(timeDuration);
9039
+ container.appendChild(progressRow);
9040
+ }
9041
+ if (state.showPlaylist) {
9042
+ const playlist = document.createElement("div");
9043
+ playlist.className = "vd-music-player-playlist";
9044
+ playlist.setAttribute("aria-label", "Playlist");
9045
+ container.appendChild(playlist);
9046
+ }
9047
+ },
9048
+ /* ─── Public API ──────────────────────────────────────── */
9049
+ /**
9050
+ * @param {HTMLElement} container
9051
+ */
9052
+ play: function(container) {
9053
+ const inst = this.instances.get(container);
9054
+ if (!inst) return;
9055
+ if (!inst.audio.src && inst.state.tracks.length) {
9056
+ inst.audio.src = inst.state.tracks[inst.state.currentIndex].url;
9057
+ }
9058
+ inst.audio.play().catch(() => {
9059
+ });
9060
+ },
9061
+ /**
9062
+ * @param {HTMLElement} container
9063
+ */
9064
+ pause: function(container) {
9065
+ const inst = this.instances.get(container);
9066
+ if (inst) inst.audio.pause();
9067
+ },
9068
+ /**
9069
+ * @param {HTMLElement} container
9070
+ */
9071
+ toggle: function(container) {
9072
+ const inst = this.instances.get(container);
9073
+ if (!inst) return;
9074
+ if (inst.state.isPlaying) {
9075
+ this.pause(container);
9076
+ } else {
9077
+ this.play(container);
9078
+ }
9079
+ },
9080
+ /**
9081
+ * @param {HTMLElement} container
9082
+ */
9083
+ next: function(container) {
9084
+ const inst = this.instances.get(container);
9085
+ if (!inst || !inst.state.tracks.length) return;
9086
+ const next = (inst.state.currentIndex + 1) % inst.state.tracks.length;
9087
+ this._loadTrack(inst, next, inst.state.isPlaying);
9088
+ },
9089
+ /**
9090
+ * @param {HTMLElement} container
9091
+ */
9092
+ previous: function(container) {
9093
+ const inst = this.instances.get(container);
9094
+ if (!inst || !inst.state.tracks.length) return;
9095
+ const len = inst.state.tracks.length;
9096
+ const prev = (inst.state.currentIndex - 1 + len) % len;
9097
+ this._loadTrack(inst, prev, inst.state.isPlaying);
9098
+ },
9099
+ /**
9100
+ * @param {HTMLElement} container
9101
+ * @param {number} value - 0 to 1
9102
+ */
9103
+ setVolume: function(container, value) {
9104
+ const inst = this.instances.get(container);
9105
+ if (!inst) return;
9106
+ const v = Math.max(0, Math.min(1, value));
9107
+ inst.state.volume = v;
9108
+ inst.audio.volume = v;
9109
+ if (inst.refs.volumeSlider) {
9110
+ inst.refs.volumeSlider.value = String(v);
9111
+ updateRangeFill(inst.refs.volumeSlider);
9112
+ }
9113
+ container.dispatchEvent(
9114
+ new CustomEvent("musicplayer:volumechange", { bubbles: true, detail: { volume: v } })
9115
+ );
9116
+ },
9117
+ /**
9118
+ * @param {HTMLElement} container
9119
+ * @param {number} index - Track index
9120
+ */
9121
+ setTrack: function(container, index) {
9122
+ const inst = this.instances.get(container);
9123
+ if (!inst) return;
9124
+ this._loadTrack(inst, index, inst.state.isPlaying);
9125
+ },
9126
+ /**
9127
+ * Shuffle or un-shuffle the track list.
9128
+ * @param {HTMLElement} container
9129
+ */
9130
+ shuffle: function(container) {
9131
+ const inst = this.instances.get(container);
9132
+ if (!inst || !inst.refs.btnShuffle) return;
9133
+ inst.refs.btnShuffle.click();
9134
+ },
9135
+ /**
9136
+ * Return a shallow copy of the current player state.
9137
+ * @param {HTMLElement} container
9138
+ * @returns {Object|null}
9139
+ */
9140
+ getState: function(container) {
9141
+ const inst = this.instances.get(container);
9142
+ if (!inst) return null;
9143
+ const s = inst.state;
9144
+ return {
9145
+ isPlaying: s.isPlaying,
9146
+ currentIndex: s.currentIndex,
9147
+ currentTrack: s.tracks[s.currentIndex] || null,
9148
+ volume: s.volume,
9149
+ shuffle: s.shuffle,
9150
+ tracks: s.tracks.slice()
9151
+ };
9152
+ },
9153
+ /**
9154
+ * Stop playback, clean up listeners, remove instance.
9155
+ * @param {HTMLElement} container
9156
+ */
9157
+ destroy: function(container) {
9158
+ const inst = this.instances.get(container);
9159
+ if (!inst) return;
9160
+ inst.cleanup.forEach((fn) => fn());
9161
+ this.instances.delete(container);
9162
+ container.removeAttribute("data-music-player-initialized");
9163
+ },
9164
+ /**
9165
+ * Destroy all instances.
9166
+ */
9167
+ destroyAll: function() {
9168
+ this.instances.forEach((_, container) => this.destroy(container));
9169
+ },
9170
+ /* ─── Internal helpers ────────────────────────────────── */
9171
+ /**
9172
+ * Load track by index on an already-initialised instance object.
9173
+ * @param {Object} inst
9174
+ * @param {number} index
9175
+ * @param {boolean} autoPlay
9176
+ */
9177
+ _loadTrack: function(inst, index, autoPlay) {
9178
+ const track = inst.state.tracks[index];
9179
+ if (!track) return;
9180
+ const container = this._containerOf(inst);
9181
+ inst.state.currentIndex = index;
9182
+ inst.audio.src = track.url;
9183
+ if (inst.refs.trackName) {
9184
+ inst.refs.trackName.textContent = track.name || "Unknown Track";
9185
+ inst.refs.trackName.classList.remove("is-idle");
9186
+ }
9187
+ if (inst.refs.playlistPanel) {
9188
+ inst.refs.playlistPanel.querySelectorAll(".vd-music-player-playlist-item").forEach((item, i) => {
9189
+ const active = i === index;
9190
+ item.classList.toggle("is-active", active);
9191
+ item.setAttribute("aria-current", active ? "true" : "false");
9192
+ });
9193
+ }
9194
+ if (inst.refs.progressBar) {
9195
+ inst.refs.progressBar.value = "0";
9196
+ updateRangeFill(inst.refs.progressBar);
9197
+ }
9198
+ if (inst.refs.timeElapsed) inst.refs.timeElapsed.textContent = "0:00";
9199
+ if (inst.refs.timeDuration) inst.refs.timeDuration.textContent = "0:00";
9200
+ if (container) {
9201
+ container.dispatchEvent(
9202
+ new CustomEvent("musicplayer:trackchange", {
9203
+ bubbles: true,
9204
+ detail: { index, name: track.name, url: track.url }
9205
+ })
9206
+ );
9207
+ }
9208
+ if (autoPlay) inst.audio.play().catch(() => {
9209
+ });
9210
+ },
9211
+ /**
9212
+ * Reverse-lookup the container element for a given instance object.
9213
+ * @param {Object} inst
9214
+ * @returns {HTMLElement|null}
9215
+ */
9216
+ _containerOf: function(inst) {
9217
+ for (const [container, i] of this.instances) {
9218
+ if (i === inst) return container;
9219
+ }
9220
+ return null;
9221
+ }
9222
+ };
9223
+ if (typeof window.Vanduo !== "undefined") {
9224
+ window.Vanduo.register("musicPlayer", MusicPlayer);
9225
+ }
9226
+ window.VanduoMusicPlayer = MusicPlayer;
9227
+ })();
9228
+
8568
9229
  // js/index.js
8569
9230
  var Vanduo = window.Vanduo;
8570
9231
  var index_default = Vanduo;