@vanduo-oss/framework 1.3.1 → 1.3.3
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 +22 -14
- package/css/components/draggable.css +3 -1
- 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 +773 -18
- 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 +511 -2
- package/dist/vanduo.css.map +1 -1
- package/dist/vanduo.esm.js +773 -18
- 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 +773 -18
- 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/draggable.js +103 -12
- package/js/components/music-player.js +848 -0
- package/js/components/theme-customizer.js +22 -9
- package/js/components/theme-switcher.js +4 -0
- package/js/index.js +1 -0
- package/package.json +1 -1
package/dist/vanduo.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! Vanduo v1.3.
|
|
1
|
+
/*! Vanduo v1.3.3 | Built: 2026-04-10T21:45:12.664Z | git:281f4f6 | development */
|
|
2
2
|
(() => {
|
|
3
3
|
// js/utils/lifecycle.js
|
|
4
4
|
(function() {
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
// js/vanduo.js
|
|
108
108
|
(function() {
|
|
109
109
|
"use strict";
|
|
110
|
-
const VANDUO_VERSION = true ? "1.3.
|
|
110
|
+
const VANDUO_VERSION = true ? "1.3.3" : "0.0.0-dev";
|
|
111
111
|
const Vanduo2 = {
|
|
112
112
|
version: VANDUO_VERSION,
|
|
113
113
|
components: {},
|
|
@@ -3806,6 +3806,7 @@
|
|
|
3806
3806
|
loadPreferences: function() {
|
|
3807
3807
|
this.state.theme = this.getStorageValue(this.STORAGE_KEYS.THEME, this.DEFAULTS.THEME);
|
|
3808
3808
|
this.state.primary = this.getStorageValue(this.STORAGE_KEYS.PRIMARY, this.getDefaultPrimary(this.state.theme));
|
|
3809
|
+
this._normalizeDefaultPrimaryIfStaleWithStoredTheme();
|
|
3809
3810
|
this.state.neutral = this.getStorageValue(this.STORAGE_KEYS.NEUTRAL, this.DEFAULTS.NEUTRAL);
|
|
3810
3811
|
this.state.radius = this.getStorageValue(this.STORAGE_KEYS.RADIUS, this.DEFAULTS.RADIUS);
|
|
3811
3812
|
this.state.font = this.getStorageValue(this.STORAGE_KEYS.FONT, this.DEFAULTS.FONT);
|
|
@@ -3891,12 +3892,10 @@
|
|
|
3891
3892
|
mode = this.DEFAULTS.THEME;
|
|
3892
3893
|
}
|
|
3893
3894
|
this._isApplying = true;
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
if (newDefault !== this.state.primary) {
|
|
3899
|
-
this.applyPrimary(newDefault);
|
|
3895
|
+
if (this.isUsingDefaultPrimary()) {
|
|
3896
|
+
const expected = this.getDefaultPrimary(mode);
|
|
3897
|
+
if (this.state.primary !== expected) {
|
|
3898
|
+
this.applyPrimary(expected);
|
|
3900
3899
|
}
|
|
3901
3900
|
}
|
|
3902
3901
|
this.state.theme = mode;
|
|
@@ -4138,6 +4137,20 @@
|
|
|
4138
4137
|
isUsingDefaultPrimary: function() {
|
|
4139
4138
|
return this.state.primary === this.DEFAULTS.PRIMARY_LIGHT || this.state.primary === this.DEFAULTS.PRIMARY_DARK;
|
|
4140
4139
|
},
|
|
4140
|
+
/**
|
|
4141
|
+
* When primary is still one of the auto-default palette keys (black/amber) but
|
|
4142
|
+
* localStorage was written under a different theme (or OS changed in system mode),
|
|
4143
|
+
* align in-memory state before applyAllPreferences runs — avoids amber+light / black+dark drift.
|
|
4144
|
+
*/
|
|
4145
|
+
_normalizeDefaultPrimaryIfStaleWithStoredTheme: function() {
|
|
4146
|
+
if (!this.isUsingDefaultPrimary()) {
|
|
4147
|
+
return;
|
|
4148
|
+
}
|
|
4149
|
+
const expected = this.getDefaultPrimary(this.state.theme);
|
|
4150
|
+
if (this.state.primary !== expected) {
|
|
4151
|
+
this.state.primary = expected;
|
|
4152
|
+
}
|
|
4153
|
+
},
|
|
4141
4154
|
bindEvents: function() {
|
|
4142
4155
|
if (this.elements.trigger) {
|
|
4143
4156
|
this.addListener(this.elements.trigger, "click", (e) => {
|
|
@@ -4376,6 +4389,9 @@
|
|
|
4376
4389
|
this._onMediaChange = (_e) => {
|
|
4377
4390
|
if (this.state.preference === "system") {
|
|
4378
4391
|
this.applyTheme();
|
|
4392
|
+
if (window.ThemeCustomizer && typeof window.ThemeCustomizer.applyTheme === "function" && !window.ThemeCustomizer._isApplying) {
|
|
4393
|
+
window.ThemeCustomizer.applyTheme("system");
|
|
4394
|
+
}
|
|
4379
4395
|
}
|
|
4380
4396
|
};
|
|
4381
4397
|
this._mediaQuery.addEventListener("change", this._onMediaChange);
|
|
@@ -5607,6 +5623,8 @@
|
|
|
5607
5623
|
touchState: null,
|
|
5608
5624
|
// Feedback element
|
|
5609
5625
|
feedbackElement: null,
|
|
5626
|
+
// Shared selector used by init and touch reorder
|
|
5627
|
+
containerSelector: ".vd-draggable-container, .vd-draggable-container-vertical",
|
|
5610
5628
|
/**
|
|
5611
5629
|
* Initialize draggable components
|
|
5612
5630
|
*/
|
|
@@ -5618,7 +5636,7 @@
|
|
|
5618
5636
|
}
|
|
5619
5637
|
this.initDraggable(element);
|
|
5620
5638
|
});
|
|
5621
|
-
const containers = document.querySelectorAll(
|
|
5639
|
+
const containers = document.querySelectorAll(this.containerSelector);
|
|
5622
5640
|
containers.forEach((container) => {
|
|
5623
5641
|
if (!this.instances.has(container)) {
|
|
5624
5642
|
this.initContainer(container);
|
|
@@ -5862,10 +5880,16 @@
|
|
|
5862
5880
|
*/
|
|
5863
5881
|
handleTouchStart: function(e, element) {
|
|
5864
5882
|
const touch = e.touches[0];
|
|
5883
|
+
const rect = element.getBoundingClientRect();
|
|
5865
5884
|
this.touchState = {
|
|
5866
5885
|
element,
|
|
5867
5886
|
startX: touch.clientX,
|
|
5868
5887
|
startY: touch.clientY,
|
|
5888
|
+
lastX: touch.clientX,
|
|
5889
|
+
lastY: touch.clientY,
|
|
5890
|
+
// Keep preview anchored to the original grab point.
|
|
5891
|
+
offsetX: touch.clientX - rect.left,
|
|
5892
|
+
offsetY: touch.clientY - rect.top,
|
|
5869
5893
|
startTime: Date.now(),
|
|
5870
5894
|
isDragging: false
|
|
5871
5895
|
};
|
|
@@ -5878,6 +5902,8 @@
|
|
|
5878
5902
|
handleTouchMove: function(e, element) {
|
|
5879
5903
|
if (!this.touchState) return;
|
|
5880
5904
|
const touch = e.touches[0];
|
|
5905
|
+
this.touchState.lastX = touch.clientX;
|
|
5906
|
+
this.touchState.lastY = touch.clientY;
|
|
5881
5907
|
const deltaX = touch.clientX - this.touchState.startX;
|
|
5882
5908
|
const deltaY = touch.clientY - this.touchState.startY;
|
|
5883
5909
|
if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) {
|
|
@@ -5890,7 +5916,10 @@
|
|
|
5890
5916
|
element,
|
|
5891
5917
|
initialPosition: { x: this.touchState.startX, y: this.touchState.startY },
|
|
5892
5918
|
initialBounds: element.getBoundingClientRect(),
|
|
5893
|
-
data: this.getData(element)
|
|
5919
|
+
data: this.getData(element),
|
|
5920
|
+
// Preserve where inside the element the drag started for accurate ghost positioning.
|
|
5921
|
+
offsetX: this.touchState.offsetX,
|
|
5922
|
+
offsetY: this.touchState.offsetY
|
|
5894
5923
|
};
|
|
5895
5924
|
element.dispatchEvent(new CustomEvent("draggable:start", {
|
|
5896
5925
|
bubbles: true,
|
|
@@ -5912,7 +5941,8 @@
|
|
|
5912
5941
|
delta: { x: deltaX, y: deltaY }
|
|
5913
5942
|
}
|
|
5914
5943
|
}));
|
|
5915
|
-
|
|
5944
|
+
this.updateTouchDropZone(touch.clientX, touch.clientY);
|
|
5945
|
+
const container = element.closest(this.containerSelector);
|
|
5916
5946
|
if (container && container.contains(element)) {
|
|
5917
5947
|
this.handleReorder(container, element, touch.clientX, touch.clientY);
|
|
5918
5948
|
}
|
|
@@ -5927,6 +5957,17 @@
|
|
|
5927
5957
|
handleTouchEnd: function(e, element) {
|
|
5928
5958
|
if (this.touchState && this.touchState.isDragging) {
|
|
5929
5959
|
if (e.cancelable) e.preventDefault();
|
|
5960
|
+
const endTouch = e.changedTouches?.[0];
|
|
5961
|
+
const endPosition = {
|
|
5962
|
+
x: endTouch?.clientX ?? this.touchState.lastX ?? this.touchState.startX,
|
|
5963
|
+
y: endTouch?.clientY ?? this.touchState.lastY ?? this.touchState.startY
|
|
5964
|
+
};
|
|
5965
|
+
const dropZone = this.resolveDropZoneAtPoint(endPosition.x, endPosition.y) || this.touchState.overZone;
|
|
5966
|
+
if (dropZone) {
|
|
5967
|
+
this.dispatchDrop(dropZone, endPosition);
|
|
5968
|
+
} else if (this.touchState.overZone) {
|
|
5969
|
+
this.touchState.overZone.classList.remove("is-drag-over");
|
|
5970
|
+
}
|
|
5930
5971
|
element.classList.remove("is-dragging");
|
|
5931
5972
|
element.classList.add("is-dropped");
|
|
5932
5973
|
element.setAttribute("aria-grabbed", "false");
|
|
@@ -5934,7 +5975,6 @@
|
|
|
5934
5975
|
if (this.feedbackElement) {
|
|
5935
5976
|
this.feedbackElement.classList.add("hidden");
|
|
5936
5977
|
}
|
|
5937
|
-
const endTouch = e.changedTouches[0];
|
|
5938
5978
|
const data = this.currentDrag?.data || this.getData(element);
|
|
5939
5979
|
const startX = this.touchState?.startX || 0;
|
|
5940
5980
|
const startY = this.touchState?.startY || 0;
|
|
@@ -5943,10 +5983,10 @@
|
|
|
5943
5983
|
detail: {
|
|
5944
5984
|
element,
|
|
5945
5985
|
data,
|
|
5946
|
-
position:
|
|
5986
|
+
position: endPosition,
|
|
5947
5987
|
delta: {
|
|
5948
|
-
x:
|
|
5949
|
-
y:
|
|
5988
|
+
x: endPosition.x - startX,
|
|
5989
|
+
y: endPosition.y - startY
|
|
5950
5990
|
}
|
|
5951
5991
|
}
|
|
5952
5992
|
}));
|
|
@@ -5987,6 +6027,58 @@
|
|
|
5987
6027
|
*/
|
|
5988
6028
|
handleDrop: function(e, zone) {
|
|
5989
6029
|
e.preventDefault();
|
|
6030
|
+
this.dispatchDrop(zone, { x: e.clientX, y: e.clientY });
|
|
6031
|
+
},
|
|
6032
|
+
/**
|
|
6033
|
+
* Resolve a drop zone from viewport coordinates
|
|
6034
|
+
* @param {number} x
|
|
6035
|
+
* @param {number} y
|
|
6036
|
+
* @returns {HTMLElement|null}
|
|
6037
|
+
*/
|
|
6038
|
+
resolveDropZoneAtPoint: function(x, y) {
|
|
6039
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
|
|
6040
|
+
if (typeof document.elementsFromPoint === "function") {
|
|
6041
|
+
const stacked = document.elementsFromPoint(x, y);
|
|
6042
|
+
for (const element of stacked) {
|
|
6043
|
+
const zone = element.closest(".vd-drop-zone");
|
|
6044
|
+
if (zone) return zone;
|
|
6045
|
+
}
|
|
6046
|
+
}
|
|
6047
|
+
const target = document.elementFromPoint(x, y);
|
|
6048
|
+
const targetZone = target ? target.closest(".vd-drop-zone") : null;
|
|
6049
|
+
if (targetZone) return targetZone;
|
|
6050
|
+
const zones = document.querySelectorAll(".vd-drop-zone");
|
|
6051
|
+
for (const zone of zones) {
|
|
6052
|
+
const rect = zone.getBoundingClientRect();
|
|
6053
|
+
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
|
|
6054
|
+
return zone;
|
|
6055
|
+
}
|
|
6056
|
+
}
|
|
6057
|
+
return null;
|
|
6058
|
+
},
|
|
6059
|
+
/**
|
|
6060
|
+
* Track and update active drop-zone hover state on touch devices
|
|
6061
|
+
* @param {number} x
|
|
6062
|
+
* @param {number} y
|
|
6063
|
+
*/
|
|
6064
|
+
updateTouchDropZone: function(x, y) {
|
|
6065
|
+
if (!this.touchState) return;
|
|
6066
|
+
const nextZone = this.resolveDropZoneAtPoint(x, y);
|
|
6067
|
+
const prevZone = this.touchState.overZone || null;
|
|
6068
|
+
if (prevZone && prevZone !== nextZone) {
|
|
6069
|
+
prevZone.classList.remove("is-drag-over");
|
|
6070
|
+
}
|
|
6071
|
+
if (nextZone && nextZone !== prevZone) {
|
|
6072
|
+
nextZone.classList.add("is-drag-over");
|
|
6073
|
+
}
|
|
6074
|
+
this.touchState.overZone = nextZone || null;
|
|
6075
|
+
},
|
|
6076
|
+
/**
|
|
6077
|
+
* Dispatch a normalized drop event for mouse and touch flows
|
|
6078
|
+
* @param {HTMLElement} zone
|
|
6079
|
+
* @param {{x:number, y:number}} position
|
|
6080
|
+
*/
|
|
6081
|
+
dispatchDrop: function(zone, position) {
|
|
5990
6082
|
zone.classList.remove("is-drag-over");
|
|
5991
6083
|
zone.dispatchEvent(new CustomEvent("draggable:drop", {
|
|
5992
6084
|
bubbles: true,
|
|
@@ -5994,7 +6086,7 @@
|
|
|
5994
6086
|
zone,
|
|
5995
6087
|
element: this.currentDrag?.element,
|
|
5996
6088
|
data: this.currentDrag?.data,
|
|
5997
|
-
position
|
|
6089
|
+
position
|
|
5998
6090
|
}
|
|
5999
6091
|
}));
|
|
6000
6092
|
},
|
|
@@ -6095,9 +6187,11 @@
|
|
|
6095
6187
|
this.feedbackElement.innerHTML = "";
|
|
6096
6188
|
const clone = this.currentDrag.element.cloneNode(true);
|
|
6097
6189
|
this.feedbackElement.appendChild(clone);
|
|
6190
|
+
const offsetX = this.currentDrag.offsetX ?? 20;
|
|
6191
|
+
const offsetY = this.currentDrag.offsetY ?? 20;
|
|
6098
6192
|
Object.assign(this.feedbackElement.style, {
|
|
6099
|
-
left: x -
|
|
6100
|
-
top: y -
|
|
6193
|
+
left: x - offsetX + "px",
|
|
6194
|
+
top: y - offsetY + "px",
|
|
6101
6195
|
width: rect.width + "px",
|
|
6102
6196
|
height: rect.height + "px"
|
|
6103
6197
|
});
|
|
@@ -8540,6 +8634,667 @@
|
|
|
8540
8634
|
window.VanduoSpotlight = Spotlight;
|
|
8541
8635
|
})();
|
|
8542
8636
|
|
|
8637
|
+
// js/components/music-player.js
|
|
8638
|
+
(function() {
|
|
8639
|
+
"use strict";
|
|
8640
|
+
function shuffleArray(arr) {
|
|
8641
|
+
const shuffled = arr.slice();
|
|
8642
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
8643
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
8644
|
+
const tmp = shuffled[i];
|
|
8645
|
+
shuffled[i] = shuffled[j];
|
|
8646
|
+
shuffled[j] = tmp;
|
|
8647
|
+
}
|
|
8648
|
+
return shuffled;
|
|
8649
|
+
}
|
|
8650
|
+
function formatTime(seconds) {
|
|
8651
|
+
if (!isFinite(seconds) || seconds < 0) return "0:00";
|
|
8652
|
+
const m = Math.floor(seconds / 60);
|
|
8653
|
+
const s = Math.floor(seconds % 60);
|
|
8654
|
+
return m + ":" + (s < 10 ? "0" : "") + s;
|
|
8655
|
+
}
|
|
8656
|
+
function updateRangeFill(input) {
|
|
8657
|
+
const min = parseFloat(input.min) || 0;
|
|
8658
|
+
const max = parseFloat(input.max) || 1;
|
|
8659
|
+
const val = parseFloat(input.value) || 0;
|
|
8660
|
+
const pct = (val - min) / (max - min) * 100;
|
|
8661
|
+
input.style.setProperty("--fill", pct + "%");
|
|
8662
|
+
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%)";
|
|
8663
|
+
}
|
|
8664
|
+
function icon(name) {
|
|
8665
|
+
const el = document.createElement("i");
|
|
8666
|
+
el.className = "ph ph-" + name;
|
|
8667
|
+
el.setAttribute("aria-hidden", "true");
|
|
8668
|
+
return el;
|
|
8669
|
+
}
|
|
8670
|
+
const MusicPlayer = {
|
|
8671
|
+
/** @type {Map<HTMLElement, Object>} */
|
|
8672
|
+
instances: /* @__PURE__ */ new Map(),
|
|
8673
|
+
/**
|
|
8674
|
+
* Default options.
|
|
8675
|
+
*/
|
|
8676
|
+
defaults: {
|
|
8677
|
+
tracks: [],
|
|
8678
|
+
volume: 0.5,
|
|
8679
|
+
shuffle: false,
|
|
8680
|
+
showProgress: false,
|
|
8681
|
+
showPlaylist: false,
|
|
8682
|
+
autoAdvance: true
|
|
8683
|
+
},
|
|
8684
|
+
/**
|
|
8685
|
+
* Auto-initialize all .vd-music-player / [data-music-player] elements.
|
|
8686
|
+
* Options can be provided via data-music-player-options (JSON string).
|
|
8687
|
+
*/
|
|
8688
|
+
init: function() {
|
|
8689
|
+
document.querySelectorAll(".vd-music-player, [data-music-player]").forEach((el) => {
|
|
8690
|
+
if (this.instances.has(el)) return;
|
|
8691
|
+
let opts = {};
|
|
8692
|
+
const attr = el.getAttribute("data-music-player-options");
|
|
8693
|
+
if (attr) {
|
|
8694
|
+
try {
|
|
8695
|
+
opts = JSON.parse(attr);
|
|
8696
|
+
} catch (_) {
|
|
8697
|
+
}
|
|
8698
|
+
}
|
|
8699
|
+
this.initPlayer(el, opts);
|
|
8700
|
+
});
|
|
8701
|
+
},
|
|
8702
|
+
/**
|
|
8703
|
+
* Initialize a single player element.
|
|
8704
|
+
* @param {HTMLElement} container
|
|
8705
|
+
* @param {Object} [options]
|
|
8706
|
+
*/
|
|
8707
|
+
initPlayer: function(container, options) {
|
|
8708
|
+
const opts = Object.assign({}, this.defaults, options || {});
|
|
8709
|
+
const rawTracks = Array.isArray(opts.tracks) ? opts.tracks : [];
|
|
8710
|
+
const tracks = rawTracks.filter((t) => t && typeof t.url === "string" && t.url.trim());
|
|
8711
|
+
const trackList = opts.shuffle ? shuffleArray(tracks) : tracks.slice();
|
|
8712
|
+
const state = {
|
|
8713
|
+
tracks: trackList,
|
|
8714
|
+
originalTracks: tracks.slice(),
|
|
8715
|
+
currentIndex: 0,
|
|
8716
|
+
isPlaying: false,
|
|
8717
|
+
volume: Math.max(0, Math.min(1, opts.volume)),
|
|
8718
|
+
shuffle: opts.shuffle,
|
|
8719
|
+
showProgress: opts.showProgress,
|
|
8720
|
+
showPlaylist: opts.showPlaylist,
|
|
8721
|
+
autoAdvance: opts.autoAdvance,
|
|
8722
|
+
audio: null
|
|
8723
|
+
};
|
|
8724
|
+
const audio = new Audio();
|
|
8725
|
+
audio.volume = state.volume;
|
|
8726
|
+
audio.preload = "metadata";
|
|
8727
|
+
state.audio = audio;
|
|
8728
|
+
this._buildDOM(container, state);
|
|
8729
|
+
const refs = {
|
|
8730
|
+
btnPlay: container.querySelector(".vd-music-player-btn-play"),
|
|
8731
|
+
btnPrev: container.querySelector(".vd-music-player-btn-prev"),
|
|
8732
|
+
btnNext: container.querySelector(".vd-music-player-btn-next"),
|
|
8733
|
+
btnShuffle: container.querySelector(".vd-music-player-btn-shuffle"),
|
|
8734
|
+
btnPlaylist: container.querySelector(".vd-music-player-btn-playlist"),
|
|
8735
|
+
trackName: container.querySelector(".vd-music-player-track-name"),
|
|
8736
|
+
volumeSlider: container.querySelector(".vd-music-player-volume-slider"),
|
|
8737
|
+
volumeIcon: container.querySelector(".vd-music-player-volume-icon"),
|
|
8738
|
+
progressBar: container.querySelector(".vd-music-player-progress-bar"),
|
|
8739
|
+
timeElapsed: container.querySelector(".vd-music-player-time-elapsed"),
|
|
8740
|
+
timeDuration: container.querySelector(".vd-music-player-time-duration"),
|
|
8741
|
+
playlistPanel: container.querySelector(".vd-music-player-playlist")
|
|
8742
|
+
};
|
|
8743
|
+
const renderPlayIcon = () => {
|
|
8744
|
+
const btn = refs.btnPlay;
|
|
8745
|
+
if (!btn) return;
|
|
8746
|
+
btn.innerHTML = "";
|
|
8747
|
+
btn.appendChild(icon(state.isPlaying ? "pause" : "play"));
|
|
8748
|
+
btn.setAttribute("aria-label", state.isPlaying ? "Pause" : "Play");
|
|
8749
|
+
btn.classList.toggle("is-active", state.isPlaying);
|
|
8750
|
+
};
|
|
8751
|
+
const renderTrackName = () => {
|
|
8752
|
+
const el = refs.trackName;
|
|
8753
|
+
if (!el) return;
|
|
8754
|
+
const track = state.tracks[state.currentIndex];
|
|
8755
|
+
if (track) {
|
|
8756
|
+
el.textContent = track.name || "Unknown Track";
|
|
8757
|
+
el.classList.remove("is-idle");
|
|
8758
|
+
} else {
|
|
8759
|
+
el.textContent = "No tracks loaded";
|
|
8760
|
+
el.classList.add("is-idle");
|
|
8761
|
+
}
|
|
8762
|
+
};
|
|
8763
|
+
const renderVolumeIcon = () => {
|
|
8764
|
+
const el = refs.volumeIcon;
|
|
8765
|
+
if (!el) return;
|
|
8766
|
+
el.innerHTML = "";
|
|
8767
|
+
const v = state.volume;
|
|
8768
|
+
const name = v === 0 ? "speaker-none" : v < 0.5 ? "speaker-low" : "speaker-high";
|
|
8769
|
+
el.appendChild(icon(name));
|
|
8770
|
+
};
|
|
8771
|
+
const renderShuffleBtn = () => {
|
|
8772
|
+
const btn = refs.btnShuffle;
|
|
8773
|
+
if (!btn) return;
|
|
8774
|
+
btn.classList.toggle("is-active", state.shuffle);
|
|
8775
|
+
btn.setAttribute("aria-pressed", state.shuffle ? "true" : "false");
|
|
8776
|
+
};
|
|
8777
|
+
const renderPlaylistItems = () => {
|
|
8778
|
+
const panel = refs.playlistPanel;
|
|
8779
|
+
if (!panel) return;
|
|
8780
|
+
panel.innerHTML = "";
|
|
8781
|
+
state.tracks.forEach((track, i) => {
|
|
8782
|
+
const item = document.createElement("button");
|
|
8783
|
+
item.className = "vd-music-player-playlist-item" + (i === state.currentIndex ? " is-active" : "");
|
|
8784
|
+
item.type = "button";
|
|
8785
|
+
item.setAttribute("data-index", String(i));
|
|
8786
|
+
item.setAttribute("aria-current", i === state.currentIndex ? "true" : "false");
|
|
8787
|
+
const num = document.createElement("span");
|
|
8788
|
+
num.className = "vd-music-player-playlist-num";
|
|
8789
|
+
num.textContent = String(i + 1);
|
|
8790
|
+
const name = document.createElement("span");
|
|
8791
|
+
name.className = "vd-music-player-playlist-name";
|
|
8792
|
+
name.textContent = track.name || "Track " + (i + 1);
|
|
8793
|
+
item.appendChild(num);
|
|
8794
|
+
item.appendChild(name);
|
|
8795
|
+
panel.appendChild(item);
|
|
8796
|
+
});
|
|
8797
|
+
};
|
|
8798
|
+
const renderProgress = () => {
|
|
8799
|
+
const bar = refs.progressBar;
|
|
8800
|
+
if (!bar || !audio.duration) return;
|
|
8801
|
+
const pct = audio.currentTime / audio.duration * 100;
|
|
8802
|
+
bar.value = String(pct);
|
|
8803
|
+
updateRangeFill(bar);
|
|
8804
|
+
if (refs.timeElapsed) refs.timeElapsed.textContent = formatTime(audio.currentTime);
|
|
8805
|
+
if (refs.timeDuration) refs.timeDuration.textContent = formatTime(audio.duration);
|
|
8806
|
+
};
|
|
8807
|
+
const loadTrack = (index, autoPlay) => {
|
|
8808
|
+
const track = state.tracks[index];
|
|
8809
|
+
if (!track) return;
|
|
8810
|
+
state.currentIndex = index;
|
|
8811
|
+
audio.src = track.url;
|
|
8812
|
+
renderTrackName();
|
|
8813
|
+
renderPlaylistItems();
|
|
8814
|
+
if (refs.progressBar) {
|
|
8815
|
+
refs.progressBar.value = "0";
|
|
8816
|
+
updateRangeFill(refs.progressBar);
|
|
8817
|
+
}
|
|
8818
|
+
if (refs.timeElapsed) refs.timeElapsed.textContent = "0:00";
|
|
8819
|
+
if (refs.timeDuration) refs.timeDuration.textContent = "0:00";
|
|
8820
|
+
container.dispatchEvent(
|
|
8821
|
+
new CustomEvent("musicplayer:trackchange", {
|
|
8822
|
+
bubbles: true,
|
|
8823
|
+
detail: { index, name: track.name, url: track.url }
|
|
8824
|
+
})
|
|
8825
|
+
);
|
|
8826
|
+
if (autoPlay) {
|
|
8827
|
+
audio.play().catch(() => {
|
|
8828
|
+
});
|
|
8829
|
+
}
|
|
8830
|
+
};
|
|
8831
|
+
const cleanupFunctions = [];
|
|
8832
|
+
const onPlay = () => {
|
|
8833
|
+
state.isPlaying = true;
|
|
8834
|
+
renderPlayIcon();
|
|
8835
|
+
container.dispatchEvent(new CustomEvent("musicplayer:play", { bubbles: true }));
|
|
8836
|
+
};
|
|
8837
|
+
const onPause = () => {
|
|
8838
|
+
state.isPlaying = false;
|
|
8839
|
+
renderPlayIcon();
|
|
8840
|
+
container.dispatchEvent(new CustomEvent("musicplayer:pause", { bubbles: true }));
|
|
8841
|
+
};
|
|
8842
|
+
const onEnded = () => {
|
|
8843
|
+
if (state.autoAdvance && state.tracks.length > 1) {
|
|
8844
|
+
const next = (state.currentIndex + 1) % state.tracks.length;
|
|
8845
|
+
loadTrack(next, true);
|
|
8846
|
+
} else {
|
|
8847
|
+
state.isPlaying = false;
|
|
8848
|
+
renderPlayIcon();
|
|
8849
|
+
container.dispatchEvent(new CustomEvent("musicplayer:ended", { bubbles: true }));
|
|
8850
|
+
}
|
|
8851
|
+
};
|
|
8852
|
+
const onTimeUpdate = () => {
|
|
8853
|
+
if (state.showProgress) renderProgress();
|
|
8854
|
+
};
|
|
8855
|
+
const onLoadedMetadata = () => {
|
|
8856
|
+
if (refs.timeDuration) refs.timeDuration.textContent = formatTime(audio.duration);
|
|
8857
|
+
if (refs.progressBar) {
|
|
8858
|
+
refs.progressBar.max = "100";
|
|
8859
|
+
updateRangeFill(refs.progressBar);
|
|
8860
|
+
}
|
|
8861
|
+
};
|
|
8862
|
+
audio.addEventListener("play", onPlay);
|
|
8863
|
+
audio.addEventListener("pause", onPause);
|
|
8864
|
+
audio.addEventListener("ended", onEnded);
|
|
8865
|
+
audio.addEventListener("timeupdate", onTimeUpdate);
|
|
8866
|
+
audio.addEventListener("loadedmetadata", onLoadedMetadata);
|
|
8867
|
+
cleanupFunctions.push(() => {
|
|
8868
|
+
audio.removeEventListener("play", onPlay);
|
|
8869
|
+
audio.removeEventListener("pause", onPause);
|
|
8870
|
+
audio.removeEventListener("ended", onEnded);
|
|
8871
|
+
audio.removeEventListener("timeupdate", onTimeUpdate);
|
|
8872
|
+
audio.removeEventListener("loadedmetadata", onLoadedMetadata);
|
|
8873
|
+
audio.pause();
|
|
8874
|
+
audio.src = "";
|
|
8875
|
+
});
|
|
8876
|
+
if (refs.btnPlay) {
|
|
8877
|
+
const handler = () => {
|
|
8878
|
+
if (!audio.src && state.tracks.length) loadTrack(state.currentIndex, false);
|
|
8879
|
+
if (state.isPlaying) {
|
|
8880
|
+
audio.pause();
|
|
8881
|
+
} else {
|
|
8882
|
+
audio.play().catch(() => {
|
|
8883
|
+
});
|
|
8884
|
+
}
|
|
8885
|
+
};
|
|
8886
|
+
refs.btnPlay.addEventListener("click", handler);
|
|
8887
|
+
cleanupFunctions.push(() => refs.btnPlay.removeEventListener("click", handler));
|
|
8888
|
+
const keyHandler = (e) => {
|
|
8889
|
+
if (e.key === " " || e.key === "Enter") {
|
|
8890
|
+
e.preventDefault();
|
|
8891
|
+
handler();
|
|
8892
|
+
}
|
|
8893
|
+
};
|
|
8894
|
+
refs.btnPlay.addEventListener("keydown", keyHandler);
|
|
8895
|
+
cleanupFunctions.push(() => refs.btnPlay.removeEventListener("keydown", keyHandler));
|
|
8896
|
+
}
|
|
8897
|
+
if (refs.btnPrev) {
|
|
8898
|
+
const handler = () => {
|
|
8899
|
+
if (!state.tracks.length) return;
|
|
8900
|
+
if (audio.currentTime > 3) {
|
|
8901
|
+
audio.currentTime = 0;
|
|
8902
|
+
} else {
|
|
8903
|
+
const prev = state.currentIndex === 0 ? state.tracks.length - 1 : state.currentIndex - 1;
|
|
8904
|
+
loadTrack(prev, state.isPlaying);
|
|
8905
|
+
}
|
|
8906
|
+
};
|
|
8907
|
+
refs.btnPrev.addEventListener("click", handler);
|
|
8908
|
+
cleanupFunctions.push(() => refs.btnPrev.removeEventListener("click", handler));
|
|
8909
|
+
}
|
|
8910
|
+
if (refs.btnNext) {
|
|
8911
|
+
const handler = () => {
|
|
8912
|
+
if (!state.tracks.length) return;
|
|
8913
|
+
const next = (state.currentIndex + 1) % state.tracks.length;
|
|
8914
|
+
loadTrack(next, state.isPlaying);
|
|
8915
|
+
};
|
|
8916
|
+
refs.btnNext.addEventListener("click", handler);
|
|
8917
|
+
cleanupFunctions.push(() => refs.btnNext.removeEventListener("click", handler));
|
|
8918
|
+
}
|
|
8919
|
+
if (refs.btnShuffle) {
|
|
8920
|
+
const handler = () => {
|
|
8921
|
+
state.shuffle = !state.shuffle;
|
|
8922
|
+
if (state.shuffle) {
|
|
8923
|
+
const current = state.tracks[state.currentIndex];
|
|
8924
|
+
state.tracks = shuffleArray(state.tracks);
|
|
8925
|
+
const newIdx = state.tracks.findIndex((t) => t === current);
|
|
8926
|
+
if (newIdx > 0) {
|
|
8927
|
+
state.tracks.splice(newIdx, 1);
|
|
8928
|
+
state.tracks.unshift(current);
|
|
8929
|
+
}
|
|
8930
|
+
state.currentIndex = 0;
|
|
8931
|
+
} else {
|
|
8932
|
+
const current = state.tracks[state.currentIndex];
|
|
8933
|
+
state.tracks = state.originalTracks.slice();
|
|
8934
|
+
state.currentIndex = state.tracks.findIndex((t) => t === current);
|
|
8935
|
+
if (state.currentIndex < 0) state.currentIndex = 0;
|
|
8936
|
+
}
|
|
8937
|
+
renderShuffleBtn();
|
|
8938
|
+
renderPlaylistItems();
|
|
8939
|
+
};
|
|
8940
|
+
refs.btnShuffle.addEventListener("click", handler);
|
|
8941
|
+
cleanupFunctions.push(() => refs.btnShuffle.removeEventListener("click", handler));
|
|
8942
|
+
}
|
|
8943
|
+
if (refs.btnPlaylist) {
|
|
8944
|
+
const handler = () => {
|
|
8945
|
+
const panel = refs.playlistPanel;
|
|
8946
|
+
if (!panel) return;
|
|
8947
|
+
const isOpen = panel.classList.toggle("is-open");
|
|
8948
|
+
refs.btnPlaylist.classList.toggle("is-active", isOpen);
|
|
8949
|
+
refs.btnPlaylist.setAttribute("aria-expanded", isOpen ? "true" : "false");
|
|
8950
|
+
};
|
|
8951
|
+
refs.btnPlaylist.addEventListener("click", handler);
|
|
8952
|
+
cleanupFunctions.push(() => refs.btnPlaylist.removeEventListener("click", handler));
|
|
8953
|
+
}
|
|
8954
|
+
if (refs.volumeSlider) {
|
|
8955
|
+
const handler = (e) => {
|
|
8956
|
+
const v = parseFloat(e.target.value);
|
|
8957
|
+
state.volume = v;
|
|
8958
|
+
audio.volume = v;
|
|
8959
|
+
renderVolumeIcon();
|
|
8960
|
+
updateRangeFill(refs.volumeSlider);
|
|
8961
|
+
container.dispatchEvent(
|
|
8962
|
+
new CustomEvent("musicplayer:volumechange", { bubbles: true, detail: { volume: v } })
|
|
8963
|
+
);
|
|
8964
|
+
};
|
|
8965
|
+
refs.volumeSlider.addEventListener("input", handler);
|
|
8966
|
+
cleanupFunctions.push(() => refs.volumeSlider.removeEventListener("input", handler));
|
|
8967
|
+
updateRangeFill(refs.volumeSlider);
|
|
8968
|
+
}
|
|
8969
|
+
if (refs.progressBar) {
|
|
8970
|
+
const handler = (e) => {
|
|
8971
|
+
if (!audio.duration) return;
|
|
8972
|
+
const pct = parseFloat(e.target.value);
|
|
8973
|
+
audio.currentTime = pct / 100 * audio.duration;
|
|
8974
|
+
updateRangeFill(refs.progressBar);
|
|
8975
|
+
};
|
|
8976
|
+
refs.progressBar.addEventListener("input", handler);
|
|
8977
|
+
cleanupFunctions.push(() => refs.progressBar.removeEventListener("input", handler));
|
|
8978
|
+
}
|
|
8979
|
+
if (refs.playlistPanel) {
|
|
8980
|
+
const panelHandler = (e) => {
|
|
8981
|
+
const item = e.target.closest(".vd-music-player-playlist-item");
|
|
8982
|
+
if (!item) return;
|
|
8983
|
+
const idx = parseInt(item.getAttribute("data-index"), 10);
|
|
8984
|
+
if (!isNaN(idx)) loadTrack(idx, true);
|
|
8985
|
+
};
|
|
8986
|
+
refs.playlistPanel.addEventListener("click", panelHandler);
|
|
8987
|
+
cleanupFunctions.push(
|
|
8988
|
+
() => refs.playlistPanel.removeEventListener("click", panelHandler)
|
|
8989
|
+
);
|
|
8990
|
+
}
|
|
8991
|
+
renderPlayIcon();
|
|
8992
|
+
renderTrackName();
|
|
8993
|
+
renderVolumeIcon();
|
|
8994
|
+
if (opts.showPlaylist) renderPlaylistItems();
|
|
8995
|
+
this.instances.set(container, { state, audio, refs, cleanup: cleanupFunctions });
|
|
8996
|
+
container.setAttribute("data-music-player-initialized", "true");
|
|
8997
|
+
},
|
|
8998
|
+
/* ─── DOM builder ─────────────────────────────────────── */
|
|
8999
|
+
/**
|
|
9000
|
+
* Build the inner DOM structure inside container.
|
|
9001
|
+
* Pre-existing inner content is replaced only if it has no
|
|
9002
|
+
* recognised child elements (allows server-rendered markup).
|
|
9003
|
+
* @param {HTMLElement} container
|
|
9004
|
+
* @param {Object} state
|
|
9005
|
+
*/
|
|
9006
|
+
_buildDOM: function(container, state) {
|
|
9007
|
+
if (container.querySelector(".vd-music-player-controls")) return;
|
|
9008
|
+
container.setAttribute("role", "region");
|
|
9009
|
+
container.setAttribute("aria-label", "Music Player");
|
|
9010
|
+
if (state.showProgress) container.classList.add("has-progress");
|
|
9011
|
+
if (state.showPlaylist) container.classList.add("has-playlist");
|
|
9012
|
+
const info = document.createElement("div");
|
|
9013
|
+
info.className = "vd-music-player-info";
|
|
9014
|
+
const iconWrap = document.createElement("span");
|
|
9015
|
+
iconWrap.className = "vd-music-player-icon";
|
|
9016
|
+
iconWrap.setAttribute("aria-hidden", "true");
|
|
9017
|
+
iconWrap.appendChild(icon("music-note"));
|
|
9018
|
+
const trackName = document.createElement("span");
|
|
9019
|
+
trackName.className = "vd-music-player-track-name";
|
|
9020
|
+
trackName.setAttribute("aria-live", "polite");
|
|
9021
|
+
trackName.setAttribute("aria-atomic", "true");
|
|
9022
|
+
info.appendChild(iconWrap);
|
|
9023
|
+
info.appendChild(trackName);
|
|
9024
|
+
container.appendChild(info);
|
|
9025
|
+
const controls = document.createElement("div");
|
|
9026
|
+
controls.className = "vd-music-player-controls";
|
|
9027
|
+
controls.setAttribute("role", "group");
|
|
9028
|
+
controls.setAttribute("aria-label", "Playback controls");
|
|
9029
|
+
const btnPrev = document.createElement("button");
|
|
9030
|
+
btnPrev.type = "button";
|
|
9031
|
+
btnPrev.className = "vd-music-player-btn vd-music-player-btn-prev";
|
|
9032
|
+
btnPrev.setAttribute("aria-label", "Previous track");
|
|
9033
|
+
btnPrev.appendChild(icon("skip-back"));
|
|
9034
|
+
const btnPlay = document.createElement("button");
|
|
9035
|
+
btnPlay.type = "button";
|
|
9036
|
+
btnPlay.className = "vd-music-player-btn vd-music-player-btn-play";
|
|
9037
|
+
btnPlay.setAttribute("aria-label", "Play");
|
|
9038
|
+
btnPlay.appendChild(icon("play"));
|
|
9039
|
+
const btnNext = document.createElement("button");
|
|
9040
|
+
btnNext.type = "button";
|
|
9041
|
+
btnNext.className = "vd-music-player-btn vd-music-player-btn-next";
|
|
9042
|
+
btnNext.setAttribute("aria-label", "Next track");
|
|
9043
|
+
btnNext.appendChild(icon("skip-forward"));
|
|
9044
|
+
controls.appendChild(btnPrev);
|
|
9045
|
+
controls.appendChild(btnPlay);
|
|
9046
|
+
controls.appendChild(btnNext);
|
|
9047
|
+
if (state.showPlaylist || state.shuffle !== void 0) {
|
|
9048
|
+
const btnShuffle = document.createElement("button");
|
|
9049
|
+
btnShuffle.type = "button";
|
|
9050
|
+
btnShuffle.className = "vd-music-player-btn vd-music-player-btn-shuffle";
|
|
9051
|
+
btnShuffle.setAttribute("aria-label", "Shuffle");
|
|
9052
|
+
btnShuffle.setAttribute("aria-pressed", state.shuffle ? "true" : "false");
|
|
9053
|
+
btnShuffle.appendChild(icon("shuffle"));
|
|
9054
|
+
controls.appendChild(btnShuffle);
|
|
9055
|
+
}
|
|
9056
|
+
const spacer = document.createElement("span");
|
|
9057
|
+
spacer.className = "vd-music-player-spacer";
|
|
9058
|
+
spacer.setAttribute("aria-hidden", "true");
|
|
9059
|
+
controls.appendChild(spacer);
|
|
9060
|
+
const volumeWrap = document.createElement("div");
|
|
9061
|
+
volumeWrap.className = "vd-music-player-volume";
|
|
9062
|
+
const volumeIcon = document.createElement("span");
|
|
9063
|
+
volumeIcon.className = "vd-music-player-volume-icon";
|
|
9064
|
+
volumeIcon.setAttribute("aria-hidden", "true");
|
|
9065
|
+
const volumeSlider = document.createElement("input");
|
|
9066
|
+
volumeSlider.type = "range";
|
|
9067
|
+
volumeSlider.className = "vd-music-player-volume-slider";
|
|
9068
|
+
volumeSlider.min = "0";
|
|
9069
|
+
volumeSlider.max = "1";
|
|
9070
|
+
volumeSlider.step = "0.01";
|
|
9071
|
+
volumeSlider.value = String(state.volume);
|
|
9072
|
+
volumeSlider.setAttribute("aria-label", "Volume");
|
|
9073
|
+
volumeWrap.appendChild(volumeIcon);
|
|
9074
|
+
volumeWrap.appendChild(volumeSlider);
|
|
9075
|
+
controls.appendChild(volumeWrap);
|
|
9076
|
+
if (state.showPlaylist) {
|
|
9077
|
+
const btnPlaylist = document.createElement("button");
|
|
9078
|
+
btnPlaylist.type = "button";
|
|
9079
|
+
btnPlaylist.className = "vd-music-player-btn vd-music-player-btn-playlist";
|
|
9080
|
+
btnPlaylist.setAttribute("aria-label", "Show playlist");
|
|
9081
|
+
btnPlaylist.setAttribute("aria-expanded", "false");
|
|
9082
|
+
btnPlaylist.appendChild(icon("playlist"));
|
|
9083
|
+
controls.appendChild(btnPlaylist);
|
|
9084
|
+
}
|
|
9085
|
+
container.appendChild(controls);
|
|
9086
|
+
if (state.showProgress) {
|
|
9087
|
+
const progressRow = document.createElement("div");
|
|
9088
|
+
progressRow.className = "vd-music-player-progress";
|
|
9089
|
+
const timeElapsed = document.createElement("span");
|
|
9090
|
+
timeElapsed.className = "vd-music-player-time vd-music-player-time-elapsed";
|
|
9091
|
+
timeElapsed.textContent = "0:00";
|
|
9092
|
+
timeElapsed.setAttribute("aria-hidden", "true");
|
|
9093
|
+
const progressBar = document.createElement("input");
|
|
9094
|
+
progressBar.type = "range";
|
|
9095
|
+
progressBar.className = "vd-music-player-progress-bar";
|
|
9096
|
+
progressBar.min = "0";
|
|
9097
|
+
progressBar.max = "100";
|
|
9098
|
+
progressBar.step = "0.1";
|
|
9099
|
+
progressBar.value = "0";
|
|
9100
|
+
progressBar.setAttribute("aria-label", "Seek");
|
|
9101
|
+
const timeDuration = document.createElement("span");
|
|
9102
|
+
timeDuration.className = "vd-music-player-time vd-music-player-time-duration";
|
|
9103
|
+
timeDuration.textContent = "0:00";
|
|
9104
|
+
timeDuration.setAttribute("aria-hidden", "true");
|
|
9105
|
+
progressRow.appendChild(timeElapsed);
|
|
9106
|
+
progressRow.appendChild(progressBar);
|
|
9107
|
+
progressRow.appendChild(timeDuration);
|
|
9108
|
+
container.appendChild(progressRow);
|
|
9109
|
+
}
|
|
9110
|
+
if (state.showPlaylist) {
|
|
9111
|
+
const playlist = document.createElement("div");
|
|
9112
|
+
playlist.className = "vd-music-player-playlist";
|
|
9113
|
+
playlist.setAttribute("aria-label", "Playlist");
|
|
9114
|
+
container.appendChild(playlist);
|
|
9115
|
+
}
|
|
9116
|
+
},
|
|
9117
|
+
/* ─── Public API ──────────────────────────────────────── */
|
|
9118
|
+
/**
|
|
9119
|
+
* @param {HTMLElement} container
|
|
9120
|
+
*/
|
|
9121
|
+
play: function(container) {
|
|
9122
|
+
const inst = this.instances.get(container);
|
|
9123
|
+
if (!inst) return;
|
|
9124
|
+
if (!inst.audio.src && inst.state.tracks.length) {
|
|
9125
|
+
inst.audio.src = inst.state.tracks[inst.state.currentIndex].url;
|
|
9126
|
+
}
|
|
9127
|
+
inst.audio.play().catch(() => {
|
|
9128
|
+
});
|
|
9129
|
+
},
|
|
9130
|
+
/**
|
|
9131
|
+
* @param {HTMLElement} container
|
|
9132
|
+
*/
|
|
9133
|
+
pause: function(container) {
|
|
9134
|
+
const inst = this.instances.get(container);
|
|
9135
|
+
if (inst) inst.audio.pause();
|
|
9136
|
+
},
|
|
9137
|
+
/**
|
|
9138
|
+
* @param {HTMLElement} container
|
|
9139
|
+
*/
|
|
9140
|
+
toggle: function(container) {
|
|
9141
|
+
const inst = this.instances.get(container);
|
|
9142
|
+
if (!inst) return;
|
|
9143
|
+
if (inst.state.isPlaying) {
|
|
9144
|
+
this.pause(container);
|
|
9145
|
+
} else {
|
|
9146
|
+
this.play(container);
|
|
9147
|
+
}
|
|
9148
|
+
},
|
|
9149
|
+
/**
|
|
9150
|
+
* @param {HTMLElement} container
|
|
9151
|
+
*/
|
|
9152
|
+
next: function(container) {
|
|
9153
|
+
const inst = this.instances.get(container);
|
|
9154
|
+
if (!inst || !inst.state.tracks.length) return;
|
|
9155
|
+
const next = (inst.state.currentIndex + 1) % inst.state.tracks.length;
|
|
9156
|
+
this._loadTrack(inst, next, inst.state.isPlaying);
|
|
9157
|
+
},
|
|
9158
|
+
/**
|
|
9159
|
+
* @param {HTMLElement} container
|
|
9160
|
+
*/
|
|
9161
|
+
previous: function(container) {
|
|
9162
|
+
const inst = this.instances.get(container);
|
|
9163
|
+
if (!inst || !inst.state.tracks.length) return;
|
|
9164
|
+
const len = inst.state.tracks.length;
|
|
9165
|
+
const prev = (inst.state.currentIndex - 1 + len) % len;
|
|
9166
|
+
this._loadTrack(inst, prev, inst.state.isPlaying);
|
|
9167
|
+
},
|
|
9168
|
+
/**
|
|
9169
|
+
* @param {HTMLElement} container
|
|
9170
|
+
* @param {number} value - 0 to 1
|
|
9171
|
+
*/
|
|
9172
|
+
setVolume: function(container, value) {
|
|
9173
|
+
const inst = this.instances.get(container);
|
|
9174
|
+
if (!inst) return;
|
|
9175
|
+
const v = Math.max(0, Math.min(1, value));
|
|
9176
|
+
inst.state.volume = v;
|
|
9177
|
+
inst.audio.volume = v;
|
|
9178
|
+
if (inst.refs.volumeSlider) {
|
|
9179
|
+
inst.refs.volumeSlider.value = String(v);
|
|
9180
|
+
updateRangeFill(inst.refs.volumeSlider);
|
|
9181
|
+
}
|
|
9182
|
+
container.dispatchEvent(
|
|
9183
|
+
new CustomEvent("musicplayer:volumechange", { bubbles: true, detail: { volume: v } })
|
|
9184
|
+
);
|
|
9185
|
+
},
|
|
9186
|
+
/**
|
|
9187
|
+
* @param {HTMLElement} container
|
|
9188
|
+
* @param {number} index - Track index
|
|
9189
|
+
*/
|
|
9190
|
+
setTrack: function(container, index) {
|
|
9191
|
+
const inst = this.instances.get(container);
|
|
9192
|
+
if (!inst) return;
|
|
9193
|
+
this._loadTrack(inst, index, inst.state.isPlaying);
|
|
9194
|
+
},
|
|
9195
|
+
/**
|
|
9196
|
+
* Shuffle or un-shuffle the track list.
|
|
9197
|
+
* @param {HTMLElement} container
|
|
9198
|
+
*/
|
|
9199
|
+
shuffle: function(container) {
|
|
9200
|
+
const inst = this.instances.get(container);
|
|
9201
|
+
if (!inst || !inst.refs.btnShuffle) return;
|
|
9202
|
+
inst.refs.btnShuffle.click();
|
|
9203
|
+
},
|
|
9204
|
+
/**
|
|
9205
|
+
* Return a shallow copy of the current player state.
|
|
9206
|
+
* @param {HTMLElement} container
|
|
9207
|
+
* @returns {Object|null}
|
|
9208
|
+
*/
|
|
9209
|
+
getState: function(container) {
|
|
9210
|
+
const inst = this.instances.get(container);
|
|
9211
|
+
if (!inst) return null;
|
|
9212
|
+
const s = inst.state;
|
|
9213
|
+
return {
|
|
9214
|
+
isPlaying: s.isPlaying,
|
|
9215
|
+
currentIndex: s.currentIndex,
|
|
9216
|
+
currentTrack: s.tracks[s.currentIndex] || null,
|
|
9217
|
+
volume: s.volume,
|
|
9218
|
+
shuffle: s.shuffle,
|
|
9219
|
+
tracks: s.tracks.slice()
|
|
9220
|
+
};
|
|
9221
|
+
},
|
|
9222
|
+
/**
|
|
9223
|
+
* Stop playback, clean up listeners, remove instance.
|
|
9224
|
+
* @param {HTMLElement} container
|
|
9225
|
+
*/
|
|
9226
|
+
destroy: function(container) {
|
|
9227
|
+
const inst = this.instances.get(container);
|
|
9228
|
+
if (!inst) return;
|
|
9229
|
+
inst.cleanup.forEach((fn) => fn());
|
|
9230
|
+
this.instances.delete(container);
|
|
9231
|
+
container.removeAttribute("data-music-player-initialized");
|
|
9232
|
+
},
|
|
9233
|
+
/**
|
|
9234
|
+
* Destroy all instances.
|
|
9235
|
+
*/
|
|
9236
|
+
destroyAll: function() {
|
|
9237
|
+
this.instances.forEach((_, container) => this.destroy(container));
|
|
9238
|
+
},
|
|
9239
|
+
/* ─── Internal helpers ────────────────────────────────── */
|
|
9240
|
+
/**
|
|
9241
|
+
* Load track by index on an already-initialised instance object.
|
|
9242
|
+
* @param {Object} inst
|
|
9243
|
+
* @param {number} index
|
|
9244
|
+
* @param {boolean} autoPlay
|
|
9245
|
+
*/
|
|
9246
|
+
_loadTrack: function(inst, index, autoPlay) {
|
|
9247
|
+
const track = inst.state.tracks[index];
|
|
9248
|
+
if (!track) return;
|
|
9249
|
+
const container = this._containerOf(inst);
|
|
9250
|
+
inst.state.currentIndex = index;
|
|
9251
|
+
inst.audio.src = track.url;
|
|
9252
|
+
if (inst.refs.trackName) {
|
|
9253
|
+
inst.refs.trackName.textContent = track.name || "Unknown Track";
|
|
9254
|
+
inst.refs.trackName.classList.remove("is-idle");
|
|
9255
|
+
}
|
|
9256
|
+
if (inst.refs.playlistPanel) {
|
|
9257
|
+
inst.refs.playlistPanel.querySelectorAll(".vd-music-player-playlist-item").forEach((item, i) => {
|
|
9258
|
+
const active = i === index;
|
|
9259
|
+
item.classList.toggle("is-active", active);
|
|
9260
|
+
item.setAttribute("aria-current", active ? "true" : "false");
|
|
9261
|
+
});
|
|
9262
|
+
}
|
|
9263
|
+
if (inst.refs.progressBar) {
|
|
9264
|
+
inst.refs.progressBar.value = "0";
|
|
9265
|
+
updateRangeFill(inst.refs.progressBar);
|
|
9266
|
+
}
|
|
9267
|
+
if (inst.refs.timeElapsed) inst.refs.timeElapsed.textContent = "0:00";
|
|
9268
|
+
if (inst.refs.timeDuration) inst.refs.timeDuration.textContent = "0:00";
|
|
9269
|
+
if (container) {
|
|
9270
|
+
container.dispatchEvent(
|
|
9271
|
+
new CustomEvent("musicplayer:trackchange", {
|
|
9272
|
+
bubbles: true,
|
|
9273
|
+
detail: { index, name: track.name, url: track.url }
|
|
9274
|
+
})
|
|
9275
|
+
);
|
|
9276
|
+
}
|
|
9277
|
+
if (autoPlay) inst.audio.play().catch(() => {
|
|
9278
|
+
});
|
|
9279
|
+
},
|
|
9280
|
+
/**
|
|
9281
|
+
* Reverse-lookup the container element for a given instance object.
|
|
9282
|
+
* @param {Object} inst
|
|
9283
|
+
* @returns {HTMLElement|null}
|
|
9284
|
+
*/
|
|
9285
|
+
_containerOf: function(inst) {
|
|
9286
|
+
for (const [container, i] of this.instances) {
|
|
9287
|
+
if (i === inst) return container;
|
|
9288
|
+
}
|
|
9289
|
+
return null;
|
|
9290
|
+
}
|
|
9291
|
+
};
|
|
9292
|
+
if (typeof window.Vanduo !== "undefined") {
|
|
9293
|
+
window.Vanduo.register("musicPlayer", MusicPlayer);
|
|
9294
|
+
}
|
|
9295
|
+
window.VanduoMusicPlayer = MusicPlayer;
|
|
9296
|
+
})();
|
|
9297
|
+
|
|
8543
9298
|
// js/index.js
|
|
8544
9299
|
var Vanduo = window.Vanduo;
|
|
8545
9300
|
var index_default = Vanduo;
|