@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.
- package/README.md +13 -14
- package/css/components/music-player.css +578 -0
- package/css/vanduo.css +1 -0
- package/dist/build-info.json +3 -3
- package/dist/vanduo.cjs.js +663 -2
- package/dist/vanduo.cjs.js.map +3 -3
- package/dist/vanduo.cjs.min.js +5 -5
- package/dist/vanduo.cjs.min.js.map +4 -4
- package/dist/vanduo.css +508 -1
- package/dist/vanduo.css.map +1 -1
- package/dist/vanduo.esm.js +663 -2
- package/dist/vanduo.esm.js.map +3 -3
- package/dist/vanduo.esm.min.js +5 -5
- package/dist/vanduo.esm.min.js.map +4 -4
- package/dist/vanduo.js +663 -2
- package/dist/vanduo.js.map +3 -3
- package/dist/vanduo.min.css +2 -2
- package/dist/vanduo.min.css.map +1 -1
- package/dist/vanduo.min.js +5 -5
- package/dist/vanduo.min.js.map +4 -4
- package/js/components/music-player.js +848 -0
- package/js/index.js +1 -0
- package/package.json +1 -1
package/dist/vanduo.cjs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! Vanduo v1.3.
|
|
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.
|
|
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;
|