@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.cjs.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
|
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.3" : "0.0.0-dev";
|
|
136
136
|
const Vanduo2 = {
|
|
137
137
|
version: VANDUO_VERSION,
|
|
138
138
|
components: {},
|
|
@@ -3831,6 +3831,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
3831
3831
|
loadPreferences: function() {
|
|
3832
3832
|
this.state.theme = this.getStorageValue(this.STORAGE_KEYS.THEME, this.DEFAULTS.THEME);
|
|
3833
3833
|
this.state.primary = this.getStorageValue(this.STORAGE_KEYS.PRIMARY, this.getDefaultPrimary(this.state.theme));
|
|
3834
|
+
this._normalizeDefaultPrimaryIfStaleWithStoredTheme();
|
|
3834
3835
|
this.state.neutral = this.getStorageValue(this.STORAGE_KEYS.NEUTRAL, this.DEFAULTS.NEUTRAL);
|
|
3835
3836
|
this.state.radius = this.getStorageValue(this.STORAGE_KEYS.RADIUS, this.DEFAULTS.RADIUS);
|
|
3836
3837
|
this.state.font = this.getStorageValue(this.STORAGE_KEYS.FONT, this.DEFAULTS.FONT);
|
|
@@ -3916,12 +3917,10 @@ module.exports = __toCommonJS(index_exports);
|
|
|
3916
3917
|
mode = this.DEFAULTS.THEME;
|
|
3917
3918
|
}
|
|
3918
3919
|
this._isApplying = true;
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
if (newDefault !== this.state.primary) {
|
|
3924
|
-
this.applyPrimary(newDefault);
|
|
3920
|
+
if (this.isUsingDefaultPrimary()) {
|
|
3921
|
+
const expected = this.getDefaultPrimary(mode);
|
|
3922
|
+
if (this.state.primary !== expected) {
|
|
3923
|
+
this.applyPrimary(expected);
|
|
3925
3924
|
}
|
|
3926
3925
|
}
|
|
3927
3926
|
this.state.theme = mode;
|
|
@@ -4163,6 +4162,20 @@ module.exports = __toCommonJS(index_exports);
|
|
|
4163
4162
|
isUsingDefaultPrimary: function() {
|
|
4164
4163
|
return this.state.primary === this.DEFAULTS.PRIMARY_LIGHT || this.state.primary === this.DEFAULTS.PRIMARY_DARK;
|
|
4165
4164
|
},
|
|
4165
|
+
/**
|
|
4166
|
+
* When primary is still one of the auto-default palette keys (black/amber) but
|
|
4167
|
+
* localStorage was written under a different theme (or OS changed in system mode),
|
|
4168
|
+
* align in-memory state before applyAllPreferences runs — avoids amber+light / black+dark drift.
|
|
4169
|
+
*/
|
|
4170
|
+
_normalizeDefaultPrimaryIfStaleWithStoredTheme: function() {
|
|
4171
|
+
if (!this.isUsingDefaultPrimary()) {
|
|
4172
|
+
return;
|
|
4173
|
+
}
|
|
4174
|
+
const expected = this.getDefaultPrimary(this.state.theme);
|
|
4175
|
+
if (this.state.primary !== expected) {
|
|
4176
|
+
this.state.primary = expected;
|
|
4177
|
+
}
|
|
4178
|
+
},
|
|
4166
4179
|
bindEvents: function() {
|
|
4167
4180
|
if (this.elements.trigger) {
|
|
4168
4181
|
this.addListener(this.elements.trigger, "click", (e) => {
|
|
@@ -4401,6 +4414,9 @@ module.exports = __toCommonJS(index_exports);
|
|
|
4401
4414
|
this._onMediaChange = (_e) => {
|
|
4402
4415
|
if (this.state.preference === "system") {
|
|
4403
4416
|
this.applyTheme();
|
|
4417
|
+
if (window.ThemeCustomizer && typeof window.ThemeCustomizer.applyTheme === "function" && !window.ThemeCustomizer._isApplying) {
|
|
4418
|
+
window.ThemeCustomizer.applyTheme("system");
|
|
4419
|
+
}
|
|
4404
4420
|
}
|
|
4405
4421
|
};
|
|
4406
4422
|
this._mediaQuery.addEventListener("change", this._onMediaChange);
|
|
@@ -5632,6 +5648,8 @@ module.exports = __toCommonJS(index_exports);
|
|
|
5632
5648
|
touchState: null,
|
|
5633
5649
|
// Feedback element
|
|
5634
5650
|
feedbackElement: null,
|
|
5651
|
+
// Shared selector used by init and touch reorder
|
|
5652
|
+
containerSelector: ".vd-draggable-container, .vd-draggable-container-vertical",
|
|
5635
5653
|
/**
|
|
5636
5654
|
* Initialize draggable components
|
|
5637
5655
|
*/
|
|
@@ -5643,7 +5661,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
5643
5661
|
}
|
|
5644
5662
|
this.initDraggable(element);
|
|
5645
5663
|
});
|
|
5646
|
-
const containers = document.querySelectorAll(
|
|
5664
|
+
const containers = document.querySelectorAll(this.containerSelector);
|
|
5647
5665
|
containers.forEach((container) => {
|
|
5648
5666
|
if (!this.instances.has(container)) {
|
|
5649
5667
|
this.initContainer(container);
|
|
@@ -5887,10 +5905,16 @@ module.exports = __toCommonJS(index_exports);
|
|
|
5887
5905
|
*/
|
|
5888
5906
|
handleTouchStart: function(e, element) {
|
|
5889
5907
|
const touch = e.touches[0];
|
|
5908
|
+
const rect = element.getBoundingClientRect();
|
|
5890
5909
|
this.touchState = {
|
|
5891
5910
|
element,
|
|
5892
5911
|
startX: touch.clientX,
|
|
5893
5912
|
startY: touch.clientY,
|
|
5913
|
+
lastX: touch.clientX,
|
|
5914
|
+
lastY: touch.clientY,
|
|
5915
|
+
// Keep preview anchored to the original grab point.
|
|
5916
|
+
offsetX: touch.clientX - rect.left,
|
|
5917
|
+
offsetY: touch.clientY - rect.top,
|
|
5894
5918
|
startTime: Date.now(),
|
|
5895
5919
|
isDragging: false
|
|
5896
5920
|
};
|
|
@@ -5903,6 +5927,8 @@ module.exports = __toCommonJS(index_exports);
|
|
|
5903
5927
|
handleTouchMove: function(e, element) {
|
|
5904
5928
|
if (!this.touchState) return;
|
|
5905
5929
|
const touch = e.touches[0];
|
|
5930
|
+
this.touchState.lastX = touch.clientX;
|
|
5931
|
+
this.touchState.lastY = touch.clientY;
|
|
5906
5932
|
const deltaX = touch.clientX - this.touchState.startX;
|
|
5907
5933
|
const deltaY = touch.clientY - this.touchState.startY;
|
|
5908
5934
|
if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) {
|
|
@@ -5915,7 +5941,10 @@ module.exports = __toCommonJS(index_exports);
|
|
|
5915
5941
|
element,
|
|
5916
5942
|
initialPosition: { x: this.touchState.startX, y: this.touchState.startY },
|
|
5917
5943
|
initialBounds: element.getBoundingClientRect(),
|
|
5918
|
-
data: this.getData(element)
|
|
5944
|
+
data: this.getData(element),
|
|
5945
|
+
// Preserve where inside the element the drag started for accurate ghost positioning.
|
|
5946
|
+
offsetX: this.touchState.offsetX,
|
|
5947
|
+
offsetY: this.touchState.offsetY
|
|
5919
5948
|
};
|
|
5920
5949
|
element.dispatchEvent(new CustomEvent("draggable:start", {
|
|
5921
5950
|
bubbles: true,
|
|
@@ -5937,7 +5966,8 @@ module.exports = __toCommonJS(index_exports);
|
|
|
5937
5966
|
delta: { x: deltaX, y: deltaY }
|
|
5938
5967
|
}
|
|
5939
5968
|
}));
|
|
5940
|
-
|
|
5969
|
+
this.updateTouchDropZone(touch.clientX, touch.clientY);
|
|
5970
|
+
const container = element.closest(this.containerSelector);
|
|
5941
5971
|
if (container && container.contains(element)) {
|
|
5942
5972
|
this.handleReorder(container, element, touch.clientX, touch.clientY);
|
|
5943
5973
|
}
|
|
@@ -5952,6 +5982,17 @@ module.exports = __toCommonJS(index_exports);
|
|
|
5952
5982
|
handleTouchEnd: function(e, element) {
|
|
5953
5983
|
if (this.touchState && this.touchState.isDragging) {
|
|
5954
5984
|
if (e.cancelable) e.preventDefault();
|
|
5985
|
+
const endTouch = e.changedTouches?.[0];
|
|
5986
|
+
const endPosition = {
|
|
5987
|
+
x: endTouch?.clientX ?? this.touchState.lastX ?? this.touchState.startX,
|
|
5988
|
+
y: endTouch?.clientY ?? this.touchState.lastY ?? this.touchState.startY
|
|
5989
|
+
};
|
|
5990
|
+
const dropZone = this.resolveDropZoneAtPoint(endPosition.x, endPosition.y) || this.touchState.overZone;
|
|
5991
|
+
if (dropZone) {
|
|
5992
|
+
this.dispatchDrop(dropZone, endPosition);
|
|
5993
|
+
} else if (this.touchState.overZone) {
|
|
5994
|
+
this.touchState.overZone.classList.remove("is-drag-over");
|
|
5995
|
+
}
|
|
5955
5996
|
element.classList.remove("is-dragging");
|
|
5956
5997
|
element.classList.add("is-dropped");
|
|
5957
5998
|
element.setAttribute("aria-grabbed", "false");
|
|
@@ -5959,7 +6000,6 @@ module.exports = __toCommonJS(index_exports);
|
|
|
5959
6000
|
if (this.feedbackElement) {
|
|
5960
6001
|
this.feedbackElement.classList.add("hidden");
|
|
5961
6002
|
}
|
|
5962
|
-
const endTouch = e.changedTouches[0];
|
|
5963
6003
|
const data = this.currentDrag?.data || this.getData(element);
|
|
5964
6004
|
const startX = this.touchState?.startX || 0;
|
|
5965
6005
|
const startY = this.touchState?.startY || 0;
|
|
@@ -5968,10 +6008,10 @@ module.exports = __toCommonJS(index_exports);
|
|
|
5968
6008
|
detail: {
|
|
5969
6009
|
element,
|
|
5970
6010
|
data,
|
|
5971
|
-
position:
|
|
6011
|
+
position: endPosition,
|
|
5972
6012
|
delta: {
|
|
5973
|
-
x:
|
|
5974
|
-
y:
|
|
6013
|
+
x: endPosition.x - startX,
|
|
6014
|
+
y: endPosition.y - startY
|
|
5975
6015
|
}
|
|
5976
6016
|
}
|
|
5977
6017
|
}));
|
|
@@ -6012,6 +6052,58 @@ module.exports = __toCommonJS(index_exports);
|
|
|
6012
6052
|
*/
|
|
6013
6053
|
handleDrop: function(e, zone) {
|
|
6014
6054
|
e.preventDefault();
|
|
6055
|
+
this.dispatchDrop(zone, { x: e.clientX, y: e.clientY });
|
|
6056
|
+
},
|
|
6057
|
+
/**
|
|
6058
|
+
* Resolve a drop zone from viewport coordinates
|
|
6059
|
+
* @param {number} x
|
|
6060
|
+
* @param {number} y
|
|
6061
|
+
* @returns {HTMLElement|null}
|
|
6062
|
+
*/
|
|
6063
|
+
resolveDropZoneAtPoint: function(x, y) {
|
|
6064
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
|
|
6065
|
+
if (typeof document.elementsFromPoint === "function") {
|
|
6066
|
+
const stacked = document.elementsFromPoint(x, y);
|
|
6067
|
+
for (const element of stacked) {
|
|
6068
|
+
const zone = element.closest(".vd-drop-zone");
|
|
6069
|
+
if (zone) return zone;
|
|
6070
|
+
}
|
|
6071
|
+
}
|
|
6072
|
+
const target = document.elementFromPoint(x, y);
|
|
6073
|
+
const targetZone = target ? target.closest(".vd-drop-zone") : null;
|
|
6074
|
+
if (targetZone) return targetZone;
|
|
6075
|
+
const zones = document.querySelectorAll(".vd-drop-zone");
|
|
6076
|
+
for (const zone of zones) {
|
|
6077
|
+
const rect = zone.getBoundingClientRect();
|
|
6078
|
+
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
|
|
6079
|
+
return zone;
|
|
6080
|
+
}
|
|
6081
|
+
}
|
|
6082
|
+
return null;
|
|
6083
|
+
},
|
|
6084
|
+
/**
|
|
6085
|
+
* Track and update active drop-zone hover state on touch devices
|
|
6086
|
+
* @param {number} x
|
|
6087
|
+
* @param {number} y
|
|
6088
|
+
*/
|
|
6089
|
+
updateTouchDropZone: function(x, y) {
|
|
6090
|
+
if (!this.touchState) return;
|
|
6091
|
+
const nextZone = this.resolveDropZoneAtPoint(x, y);
|
|
6092
|
+
const prevZone = this.touchState.overZone || null;
|
|
6093
|
+
if (prevZone && prevZone !== nextZone) {
|
|
6094
|
+
prevZone.classList.remove("is-drag-over");
|
|
6095
|
+
}
|
|
6096
|
+
if (nextZone && nextZone !== prevZone) {
|
|
6097
|
+
nextZone.classList.add("is-drag-over");
|
|
6098
|
+
}
|
|
6099
|
+
this.touchState.overZone = nextZone || null;
|
|
6100
|
+
},
|
|
6101
|
+
/**
|
|
6102
|
+
* Dispatch a normalized drop event for mouse and touch flows
|
|
6103
|
+
* @param {HTMLElement} zone
|
|
6104
|
+
* @param {{x:number, y:number}} position
|
|
6105
|
+
*/
|
|
6106
|
+
dispatchDrop: function(zone, position) {
|
|
6015
6107
|
zone.classList.remove("is-drag-over");
|
|
6016
6108
|
zone.dispatchEvent(new CustomEvent("draggable:drop", {
|
|
6017
6109
|
bubbles: true,
|
|
@@ -6019,7 +6111,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
6019
6111
|
zone,
|
|
6020
6112
|
element: this.currentDrag?.element,
|
|
6021
6113
|
data: this.currentDrag?.data,
|
|
6022
|
-
position
|
|
6114
|
+
position
|
|
6023
6115
|
}
|
|
6024
6116
|
}));
|
|
6025
6117
|
},
|
|
@@ -6120,9 +6212,11 @@ module.exports = __toCommonJS(index_exports);
|
|
|
6120
6212
|
this.feedbackElement.innerHTML = "";
|
|
6121
6213
|
const clone = this.currentDrag.element.cloneNode(true);
|
|
6122
6214
|
this.feedbackElement.appendChild(clone);
|
|
6215
|
+
const offsetX = this.currentDrag.offsetX ?? 20;
|
|
6216
|
+
const offsetY = this.currentDrag.offsetY ?? 20;
|
|
6123
6217
|
Object.assign(this.feedbackElement.style, {
|
|
6124
|
-
left: x -
|
|
6125
|
-
top: y -
|
|
6218
|
+
left: x - offsetX + "px",
|
|
6219
|
+
top: y - offsetY + "px",
|
|
6126
6220
|
width: rect.width + "px",
|
|
6127
6221
|
height: rect.height + "px"
|
|
6128
6222
|
});
|
|
@@ -8565,6 +8659,667 @@ module.exports = __toCommonJS(index_exports);
|
|
|
8565
8659
|
window.VanduoSpotlight = Spotlight;
|
|
8566
8660
|
})();
|
|
8567
8661
|
|
|
8662
|
+
// js/components/music-player.js
|
|
8663
|
+
(function() {
|
|
8664
|
+
"use strict";
|
|
8665
|
+
function shuffleArray(arr) {
|
|
8666
|
+
const shuffled = arr.slice();
|
|
8667
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
8668
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
8669
|
+
const tmp = shuffled[i];
|
|
8670
|
+
shuffled[i] = shuffled[j];
|
|
8671
|
+
shuffled[j] = tmp;
|
|
8672
|
+
}
|
|
8673
|
+
return shuffled;
|
|
8674
|
+
}
|
|
8675
|
+
function formatTime(seconds) {
|
|
8676
|
+
if (!isFinite(seconds) || seconds < 0) return "0:00";
|
|
8677
|
+
const m = Math.floor(seconds / 60);
|
|
8678
|
+
const s = Math.floor(seconds % 60);
|
|
8679
|
+
return m + ":" + (s < 10 ? "0" : "") + s;
|
|
8680
|
+
}
|
|
8681
|
+
function updateRangeFill(input) {
|
|
8682
|
+
const min = parseFloat(input.min) || 0;
|
|
8683
|
+
const max = parseFloat(input.max) || 1;
|
|
8684
|
+
const val = parseFloat(input.value) || 0;
|
|
8685
|
+
const pct = (val - min) / (max - min) * 100;
|
|
8686
|
+
input.style.setProperty("--fill", pct + "%");
|
|
8687
|
+
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%)";
|
|
8688
|
+
}
|
|
8689
|
+
function icon(name) {
|
|
8690
|
+
const el = document.createElement("i");
|
|
8691
|
+
el.className = "ph ph-" + name;
|
|
8692
|
+
el.setAttribute("aria-hidden", "true");
|
|
8693
|
+
return el;
|
|
8694
|
+
}
|
|
8695
|
+
const MusicPlayer = {
|
|
8696
|
+
/** @type {Map<HTMLElement, Object>} */
|
|
8697
|
+
instances: /* @__PURE__ */ new Map(),
|
|
8698
|
+
/**
|
|
8699
|
+
* Default options.
|
|
8700
|
+
*/
|
|
8701
|
+
defaults: {
|
|
8702
|
+
tracks: [],
|
|
8703
|
+
volume: 0.5,
|
|
8704
|
+
shuffle: false,
|
|
8705
|
+
showProgress: false,
|
|
8706
|
+
showPlaylist: false,
|
|
8707
|
+
autoAdvance: true
|
|
8708
|
+
},
|
|
8709
|
+
/**
|
|
8710
|
+
* Auto-initialize all .vd-music-player / [data-music-player] elements.
|
|
8711
|
+
* Options can be provided via data-music-player-options (JSON string).
|
|
8712
|
+
*/
|
|
8713
|
+
init: function() {
|
|
8714
|
+
document.querySelectorAll(".vd-music-player, [data-music-player]").forEach((el) => {
|
|
8715
|
+
if (this.instances.has(el)) return;
|
|
8716
|
+
let opts = {};
|
|
8717
|
+
const attr = el.getAttribute("data-music-player-options");
|
|
8718
|
+
if (attr) {
|
|
8719
|
+
try {
|
|
8720
|
+
opts = JSON.parse(attr);
|
|
8721
|
+
} catch (_) {
|
|
8722
|
+
}
|
|
8723
|
+
}
|
|
8724
|
+
this.initPlayer(el, opts);
|
|
8725
|
+
});
|
|
8726
|
+
},
|
|
8727
|
+
/**
|
|
8728
|
+
* Initialize a single player element.
|
|
8729
|
+
* @param {HTMLElement} container
|
|
8730
|
+
* @param {Object} [options]
|
|
8731
|
+
*/
|
|
8732
|
+
initPlayer: function(container, options) {
|
|
8733
|
+
const opts = Object.assign({}, this.defaults, options || {});
|
|
8734
|
+
const rawTracks = Array.isArray(opts.tracks) ? opts.tracks : [];
|
|
8735
|
+
const tracks = rawTracks.filter((t) => t && typeof t.url === "string" && t.url.trim());
|
|
8736
|
+
const trackList = opts.shuffle ? shuffleArray(tracks) : tracks.slice();
|
|
8737
|
+
const state = {
|
|
8738
|
+
tracks: trackList,
|
|
8739
|
+
originalTracks: tracks.slice(),
|
|
8740
|
+
currentIndex: 0,
|
|
8741
|
+
isPlaying: false,
|
|
8742
|
+
volume: Math.max(0, Math.min(1, opts.volume)),
|
|
8743
|
+
shuffle: opts.shuffle,
|
|
8744
|
+
showProgress: opts.showProgress,
|
|
8745
|
+
showPlaylist: opts.showPlaylist,
|
|
8746
|
+
autoAdvance: opts.autoAdvance,
|
|
8747
|
+
audio: null
|
|
8748
|
+
};
|
|
8749
|
+
const audio = new Audio();
|
|
8750
|
+
audio.volume = state.volume;
|
|
8751
|
+
audio.preload = "metadata";
|
|
8752
|
+
state.audio = audio;
|
|
8753
|
+
this._buildDOM(container, state);
|
|
8754
|
+
const refs = {
|
|
8755
|
+
btnPlay: container.querySelector(".vd-music-player-btn-play"),
|
|
8756
|
+
btnPrev: container.querySelector(".vd-music-player-btn-prev"),
|
|
8757
|
+
btnNext: container.querySelector(".vd-music-player-btn-next"),
|
|
8758
|
+
btnShuffle: container.querySelector(".vd-music-player-btn-shuffle"),
|
|
8759
|
+
btnPlaylist: container.querySelector(".vd-music-player-btn-playlist"),
|
|
8760
|
+
trackName: container.querySelector(".vd-music-player-track-name"),
|
|
8761
|
+
volumeSlider: container.querySelector(".vd-music-player-volume-slider"),
|
|
8762
|
+
volumeIcon: container.querySelector(".vd-music-player-volume-icon"),
|
|
8763
|
+
progressBar: container.querySelector(".vd-music-player-progress-bar"),
|
|
8764
|
+
timeElapsed: container.querySelector(".vd-music-player-time-elapsed"),
|
|
8765
|
+
timeDuration: container.querySelector(".vd-music-player-time-duration"),
|
|
8766
|
+
playlistPanel: container.querySelector(".vd-music-player-playlist")
|
|
8767
|
+
};
|
|
8768
|
+
const renderPlayIcon = () => {
|
|
8769
|
+
const btn = refs.btnPlay;
|
|
8770
|
+
if (!btn) return;
|
|
8771
|
+
btn.innerHTML = "";
|
|
8772
|
+
btn.appendChild(icon(state.isPlaying ? "pause" : "play"));
|
|
8773
|
+
btn.setAttribute("aria-label", state.isPlaying ? "Pause" : "Play");
|
|
8774
|
+
btn.classList.toggle("is-active", state.isPlaying);
|
|
8775
|
+
};
|
|
8776
|
+
const renderTrackName = () => {
|
|
8777
|
+
const el = refs.trackName;
|
|
8778
|
+
if (!el) return;
|
|
8779
|
+
const track = state.tracks[state.currentIndex];
|
|
8780
|
+
if (track) {
|
|
8781
|
+
el.textContent = track.name || "Unknown Track";
|
|
8782
|
+
el.classList.remove("is-idle");
|
|
8783
|
+
} else {
|
|
8784
|
+
el.textContent = "No tracks loaded";
|
|
8785
|
+
el.classList.add("is-idle");
|
|
8786
|
+
}
|
|
8787
|
+
};
|
|
8788
|
+
const renderVolumeIcon = () => {
|
|
8789
|
+
const el = refs.volumeIcon;
|
|
8790
|
+
if (!el) return;
|
|
8791
|
+
el.innerHTML = "";
|
|
8792
|
+
const v = state.volume;
|
|
8793
|
+
const name = v === 0 ? "speaker-none" : v < 0.5 ? "speaker-low" : "speaker-high";
|
|
8794
|
+
el.appendChild(icon(name));
|
|
8795
|
+
};
|
|
8796
|
+
const renderShuffleBtn = () => {
|
|
8797
|
+
const btn = refs.btnShuffle;
|
|
8798
|
+
if (!btn) return;
|
|
8799
|
+
btn.classList.toggle("is-active", state.shuffle);
|
|
8800
|
+
btn.setAttribute("aria-pressed", state.shuffle ? "true" : "false");
|
|
8801
|
+
};
|
|
8802
|
+
const renderPlaylistItems = () => {
|
|
8803
|
+
const panel = refs.playlistPanel;
|
|
8804
|
+
if (!panel) return;
|
|
8805
|
+
panel.innerHTML = "";
|
|
8806
|
+
state.tracks.forEach((track, i) => {
|
|
8807
|
+
const item = document.createElement("button");
|
|
8808
|
+
item.className = "vd-music-player-playlist-item" + (i === state.currentIndex ? " is-active" : "");
|
|
8809
|
+
item.type = "button";
|
|
8810
|
+
item.setAttribute("data-index", String(i));
|
|
8811
|
+
item.setAttribute("aria-current", i === state.currentIndex ? "true" : "false");
|
|
8812
|
+
const num = document.createElement("span");
|
|
8813
|
+
num.className = "vd-music-player-playlist-num";
|
|
8814
|
+
num.textContent = String(i + 1);
|
|
8815
|
+
const name = document.createElement("span");
|
|
8816
|
+
name.className = "vd-music-player-playlist-name";
|
|
8817
|
+
name.textContent = track.name || "Track " + (i + 1);
|
|
8818
|
+
item.appendChild(num);
|
|
8819
|
+
item.appendChild(name);
|
|
8820
|
+
panel.appendChild(item);
|
|
8821
|
+
});
|
|
8822
|
+
};
|
|
8823
|
+
const renderProgress = () => {
|
|
8824
|
+
const bar = refs.progressBar;
|
|
8825
|
+
if (!bar || !audio.duration) return;
|
|
8826
|
+
const pct = audio.currentTime / audio.duration * 100;
|
|
8827
|
+
bar.value = String(pct);
|
|
8828
|
+
updateRangeFill(bar);
|
|
8829
|
+
if (refs.timeElapsed) refs.timeElapsed.textContent = formatTime(audio.currentTime);
|
|
8830
|
+
if (refs.timeDuration) refs.timeDuration.textContent = formatTime(audio.duration);
|
|
8831
|
+
};
|
|
8832
|
+
const loadTrack = (index, autoPlay) => {
|
|
8833
|
+
const track = state.tracks[index];
|
|
8834
|
+
if (!track) return;
|
|
8835
|
+
state.currentIndex = index;
|
|
8836
|
+
audio.src = track.url;
|
|
8837
|
+
renderTrackName();
|
|
8838
|
+
renderPlaylistItems();
|
|
8839
|
+
if (refs.progressBar) {
|
|
8840
|
+
refs.progressBar.value = "0";
|
|
8841
|
+
updateRangeFill(refs.progressBar);
|
|
8842
|
+
}
|
|
8843
|
+
if (refs.timeElapsed) refs.timeElapsed.textContent = "0:00";
|
|
8844
|
+
if (refs.timeDuration) refs.timeDuration.textContent = "0:00";
|
|
8845
|
+
container.dispatchEvent(
|
|
8846
|
+
new CustomEvent("musicplayer:trackchange", {
|
|
8847
|
+
bubbles: true,
|
|
8848
|
+
detail: { index, name: track.name, url: track.url }
|
|
8849
|
+
})
|
|
8850
|
+
);
|
|
8851
|
+
if (autoPlay) {
|
|
8852
|
+
audio.play().catch(() => {
|
|
8853
|
+
});
|
|
8854
|
+
}
|
|
8855
|
+
};
|
|
8856
|
+
const cleanupFunctions = [];
|
|
8857
|
+
const onPlay = () => {
|
|
8858
|
+
state.isPlaying = true;
|
|
8859
|
+
renderPlayIcon();
|
|
8860
|
+
container.dispatchEvent(new CustomEvent("musicplayer:play", { bubbles: true }));
|
|
8861
|
+
};
|
|
8862
|
+
const onPause = () => {
|
|
8863
|
+
state.isPlaying = false;
|
|
8864
|
+
renderPlayIcon();
|
|
8865
|
+
container.dispatchEvent(new CustomEvent("musicplayer:pause", { bubbles: true }));
|
|
8866
|
+
};
|
|
8867
|
+
const onEnded = () => {
|
|
8868
|
+
if (state.autoAdvance && state.tracks.length > 1) {
|
|
8869
|
+
const next = (state.currentIndex + 1) % state.tracks.length;
|
|
8870
|
+
loadTrack(next, true);
|
|
8871
|
+
} else {
|
|
8872
|
+
state.isPlaying = false;
|
|
8873
|
+
renderPlayIcon();
|
|
8874
|
+
container.dispatchEvent(new CustomEvent("musicplayer:ended", { bubbles: true }));
|
|
8875
|
+
}
|
|
8876
|
+
};
|
|
8877
|
+
const onTimeUpdate = () => {
|
|
8878
|
+
if (state.showProgress) renderProgress();
|
|
8879
|
+
};
|
|
8880
|
+
const onLoadedMetadata = () => {
|
|
8881
|
+
if (refs.timeDuration) refs.timeDuration.textContent = formatTime(audio.duration);
|
|
8882
|
+
if (refs.progressBar) {
|
|
8883
|
+
refs.progressBar.max = "100";
|
|
8884
|
+
updateRangeFill(refs.progressBar);
|
|
8885
|
+
}
|
|
8886
|
+
};
|
|
8887
|
+
audio.addEventListener("play", onPlay);
|
|
8888
|
+
audio.addEventListener("pause", onPause);
|
|
8889
|
+
audio.addEventListener("ended", onEnded);
|
|
8890
|
+
audio.addEventListener("timeupdate", onTimeUpdate);
|
|
8891
|
+
audio.addEventListener("loadedmetadata", onLoadedMetadata);
|
|
8892
|
+
cleanupFunctions.push(() => {
|
|
8893
|
+
audio.removeEventListener("play", onPlay);
|
|
8894
|
+
audio.removeEventListener("pause", onPause);
|
|
8895
|
+
audio.removeEventListener("ended", onEnded);
|
|
8896
|
+
audio.removeEventListener("timeupdate", onTimeUpdate);
|
|
8897
|
+
audio.removeEventListener("loadedmetadata", onLoadedMetadata);
|
|
8898
|
+
audio.pause();
|
|
8899
|
+
audio.src = "";
|
|
8900
|
+
});
|
|
8901
|
+
if (refs.btnPlay) {
|
|
8902
|
+
const handler = () => {
|
|
8903
|
+
if (!audio.src && state.tracks.length) loadTrack(state.currentIndex, false);
|
|
8904
|
+
if (state.isPlaying) {
|
|
8905
|
+
audio.pause();
|
|
8906
|
+
} else {
|
|
8907
|
+
audio.play().catch(() => {
|
|
8908
|
+
});
|
|
8909
|
+
}
|
|
8910
|
+
};
|
|
8911
|
+
refs.btnPlay.addEventListener("click", handler);
|
|
8912
|
+
cleanupFunctions.push(() => refs.btnPlay.removeEventListener("click", handler));
|
|
8913
|
+
const keyHandler = (e) => {
|
|
8914
|
+
if (e.key === " " || e.key === "Enter") {
|
|
8915
|
+
e.preventDefault();
|
|
8916
|
+
handler();
|
|
8917
|
+
}
|
|
8918
|
+
};
|
|
8919
|
+
refs.btnPlay.addEventListener("keydown", keyHandler);
|
|
8920
|
+
cleanupFunctions.push(() => refs.btnPlay.removeEventListener("keydown", keyHandler));
|
|
8921
|
+
}
|
|
8922
|
+
if (refs.btnPrev) {
|
|
8923
|
+
const handler = () => {
|
|
8924
|
+
if (!state.tracks.length) return;
|
|
8925
|
+
if (audio.currentTime > 3) {
|
|
8926
|
+
audio.currentTime = 0;
|
|
8927
|
+
} else {
|
|
8928
|
+
const prev = state.currentIndex === 0 ? state.tracks.length - 1 : state.currentIndex - 1;
|
|
8929
|
+
loadTrack(prev, state.isPlaying);
|
|
8930
|
+
}
|
|
8931
|
+
};
|
|
8932
|
+
refs.btnPrev.addEventListener("click", handler);
|
|
8933
|
+
cleanupFunctions.push(() => refs.btnPrev.removeEventListener("click", handler));
|
|
8934
|
+
}
|
|
8935
|
+
if (refs.btnNext) {
|
|
8936
|
+
const handler = () => {
|
|
8937
|
+
if (!state.tracks.length) return;
|
|
8938
|
+
const next = (state.currentIndex + 1) % state.tracks.length;
|
|
8939
|
+
loadTrack(next, state.isPlaying);
|
|
8940
|
+
};
|
|
8941
|
+
refs.btnNext.addEventListener("click", handler);
|
|
8942
|
+
cleanupFunctions.push(() => refs.btnNext.removeEventListener("click", handler));
|
|
8943
|
+
}
|
|
8944
|
+
if (refs.btnShuffle) {
|
|
8945
|
+
const handler = () => {
|
|
8946
|
+
state.shuffle = !state.shuffle;
|
|
8947
|
+
if (state.shuffle) {
|
|
8948
|
+
const current = state.tracks[state.currentIndex];
|
|
8949
|
+
state.tracks = shuffleArray(state.tracks);
|
|
8950
|
+
const newIdx = state.tracks.findIndex((t) => t === current);
|
|
8951
|
+
if (newIdx > 0) {
|
|
8952
|
+
state.tracks.splice(newIdx, 1);
|
|
8953
|
+
state.tracks.unshift(current);
|
|
8954
|
+
}
|
|
8955
|
+
state.currentIndex = 0;
|
|
8956
|
+
} else {
|
|
8957
|
+
const current = state.tracks[state.currentIndex];
|
|
8958
|
+
state.tracks = state.originalTracks.slice();
|
|
8959
|
+
state.currentIndex = state.tracks.findIndex((t) => t === current);
|
|
8960
|
+
if (state.currentIndex < 0) state.currentIndex = 0;
|
|
8961
|
+
}
|
|
8962
|
+
renderShuffleBtn();
|
|
8963
|
+
renderPlaylistItems();
|
|
8964
|
+
};
|
|
8965
|
+
refs.btnShuffle.addEventListener("click", handler);
|
|
8966
|
+
cleanupFunctions.push(() => refs.btnShuffle.removeEventListener("click", handler));
|
|
8967
|
+
}
|
|
8968
|
+
if (refs.btnPlaylist) {
|
|
8969
|
+
const handler = () => {
|
|
8970
|
+
const panel = refs.playlistPanel;
|
|
8971
|
+
if (!panel) return;
|
|
8972
|
+
const isOpen = panel.classList.toggle("is-open");
|
|
8973
|
+
refs.btnPlaylist.classList.toggle("is-active", isOpen);
|
|
8974
|
+
refs.btnPlaylist.setAttribute("aria-expanded", isOpen ? "true" : "false");
|
|
8975
|
+
};
|
|
8976
|
+
refs.btnPlaylist.addEventListener("click", handler);
|
|
8977
|
+
cleanupFunctions.push(() => refs.btnPlaylist.removeEventListener("click", handler));
|
|
8978
|
+
}
|
|
8979
|
+
if (refs.volumeSlider) {
|
|
8980
|
+
const handler = (e) => {
|
|
8981
|
+
const v = parseFloat(e.target.value);
|
|
8982
|
+
state.volume = v;
|
|
8983
|
+
audio.volume = v;
|
|
8984
|
+
renderVolumeIcon();
|
|
8985
|
+
updateRangeFill(refs.volumeSlider);
|
|
8986
|
+
container.dispatchEvent(
|
|
8987
|
+
new CustomEvent("musicplayer:volumechange", { bubbles: true, detail: { volume: v } })
|
|
8988
|
+
);
|
|
8989
|
+
};
|
|
8990
|
+
refs.volumeSlider.addEventListener("input", handler);
|
|
8991
|
+
cleanupFunctions.push(() => refs.volumeSlider.removeEventListener("input", handler));
|
|
8992
|
+
updateRangeFill(refs.volumeSlider);
|
|
8993
|
+
}
|
|
8994
|
+
if (refs.progressBar) {
|
|
8995
|
+
const handler = (e) => {
|
|
8996
|
+
if (!audio.duration) return;
|
|
8997
|
+
const pct = parseFloat(e.target.value);
|
|
8998
|
+
audio.currentTime = pct / 100 * audio.duration;
|
|
8999
|
+
updateRangeFill(refs.progressBar);
|
|
9000
|
+
};
|
|
9001
|
+
refs.progressBar.addEventListener("input", handler);
|
|
9002
|
+
cleanupFunctions.push(() => refs.progressBar.removeEventListener("input", handler));
|
|
9003
|
+
}
|
|
9004
|
+
if (refs.playlistPanel) {
|
|
9005
|
+
const panelHandler = (e) => {
|
|
9006
|
+
const item = e.target.closest(".vd-music-player-playlist-item");
|
|
9007
|
+
if (!item) return;
|
|
9008
|
+
const idx = parseInt(item.getAttribute("data-index"), 10);
|
|
9009
|
+
if (!isNaN(idx)) loadTrack(idx, true);
|
|
9010
|
+
};
|
|
9011
|
+
refs.playlistPanel.addEventListener("click", panelHandler);
|
|
9012
|
+
cleanupFunctions.push(
|
|
9013
|
+
() => refs.playlistPanel.removeEventListener("click", panelHandler)
|
|
9014
|
+
);
|
|
9015
|
+
}
|
|
9016
|
+
renderPlayIcon();
|
|
9017
|
+
renderTrackName();
|
|
9018
|
+
renderVolumeIcon();
|
|
9019
|
+
if (opts.showPlaylist) renderPlaylistItems();
|
|
9020
|
+
this.instances.set(container, { state, audio, refs, cleanup: cleanupFunctions });
|
|
9021
|
+
container.setAttribute("data-music-player-initialized", "true");
|
|
9022
|
+
},
|
|
9023
|
+
/* ─── DOM builder ─────────────────────────────────────── */
|
|
9024
|
+
/**
|
|
9025
|
+
* Build the inner DOM structure inside container.
|
|
9026
|
+
* Pre-existing inner content is replaced only if it has no
|
|
9027
|
+
* recognised child elements (allows server-rendered markup).
|
|
9028
|
+
* @param {HTMLElement} container
|
|
9029
|
+
* @param {Object} state
|
|
9030
|
+
*/
|
|
9031
|
+
_buildDOM: function(container, state) {
|
|
9032
|
+
if (container.querySelector(".vd-music-player-controls")) return;
|
|
9033
|
+
container.setAttribute("role", "region");
|
|
9034
|
+
container.setAttribute("aria-label", "Music Player");
|
|
9035
|
+
if (state.showProgress) container.classList.add("has-progress");
|
|
9036
|
+
if (state.showPlaylist) container.classList.add("has-playlist");
|
|
9037
|
+
const info = document.createElement("div");
|
|
9038
|
+
info.className = "vd-music-player-info";
|
|
9039
|
+
const iconWrap = document.createElement("span");
|
|
9040
|
+
iconWrap.className = "vd-music-player-icon";
|
|
9041
|
+
iconWrap.setAttribute("aria-hidden", "true");
|
|
9042
|
+
iconWrap.appendChild(icon("music-note"));
|
|
9043
|
+
const trackName = document.createElement("span");
|
|
9044
|
+
trackName.className = "vd-music-player-track-name";
|
|
9045
|
+
trackName.setAttribute("aria-live", "polite");
|
|
9046
|
+
trackName.setAttribute("aria-atomic", "true");
|
|
9047
|
+
info.appendChild(iconWrap);
|
|
9048
|
+
info.appendChild(trackName);
|
|
9049
|
+
container.appendChild(info);
|
|
9050
|
+
const controls = document.createElement("div");
|
|
9051
|
+
controls.className = "vd-music-player-controls";
|
|
9052
|
+
controls.setAttribute("role", "group");
|
|
9053
|
+
controls.setAttribute("aria-label", "Playback controls");
|
|
9054
|
+
const btnPrev = document.createElement("button");
|
|
9055
|
+
btnPrev.type = "button";
|
|
9056
|
+
btnPrev.className = "vd-music-player-btn vd-music-player-btn-prev";
|
|
9057
|
+
btnPrev.setAttribute("aria-label", "Previous track");
|
|
9058
|
+
btnPrev.appendChild(icon("skip-back"));
|
|
9059
|
+
const btnPlay = document.createElement("button");
|
|
9060
|
+
btnPlay.type = "button";
|
|
9061
|
+
btnPlay.className = "vd-music-player-btn vd-music-player-btn-play";
|
|
9062
|
+
btnPlay.setAttribute("aria-label", "Play");
|
|
9063
|
+
btnPlay.appendChild(icon("play"));
|
|
9064
|
+
const btnNext = document.createElement("button");
|
|
9065
|
+
btnNext.type = "button";
|
|
9066
|
+
btnNext.className = "vd-music-player-btn vd-music-player-btn-next";
|
|
9067
|
+
btnNext.setAttribute("aria-label", "Next track");
|
|
9068
|
+
btnNext.appendChild(icon("skip-forward"));
|
|
9069
|
+
controls.appendChild(btnPrev);
|
|
9070
|
+
controls.appendChild(btnPlay);
|
|
9071
|
+
controls.appendChild(btnNext);
|
|
9072
|
+
if (state.showPlaylist || state.shuffle !== void 0) {
|
|
9073
|
+
const btnShuffle = document.createElement("button");
|
|
9074
|
+
btnShuffle.type = "button";
|
|
9075
|
+
btnShuffle.className = "vd-music-player-btn vd-music-player-btn-shuffle";
|
|
9076
|
+
btnShuffle.setAttribute("aria-label", "Shuffle");
|
|
9077
|
+
btnShuffle.setAttribute("aria-pressed", state.shuffle ? "true" : "false");
|
|
9078
|
+
btnShuffle.appendChild(icon("shuffle"));
|
|
9079
|
+
controls.appendChild(btnShuffle);
|
|
9080
|
+
}
|
|
9081
|
+
const spacer = document.createElement("span");
|
|
9082
|
+
spacer.className = "vd-music-player-spacer";
|
|
9083
|
+
spacer.setAttribute("aria-hidden", "true");
|
|
9084
|
+
controls.appendChild(spacer);
|
|
9085
|
+
const volumeWrap = document.createElement("div");
|
|
9086
|
+
volumeWrap.className = "vd-music-player-volume";
|
|
9087
|
+
const volumeIcon = document.createElement("span");
|
|
9088
|
+
volumeIcon.className = "vd-music-player-volume-icon";
|
|
9089
|
+
volumeIcon.setAttribute("aria-hidden", "true");
|
|
9090
|
+
const volumeSlider = document.createElement("input");
|
|
9091
|
+
volumeSlider.type = "range";
|
|
9092
|
+
volumeSlider.className = "vd-music-player-volume-slider";
|
|
9093
|
+
volumeSlider.min = "0";
|
|
9094
|
+
volumeSlider.max = "1";
|
|
9095
|
+
volumeSlider.step = "0.01";
|
|
9096
|
+
volumeSlider.value = String(state.volume);
|
|
9097
|
+
volumeSlider.setAttribute("aria-label", "Volume");
|
|
9098
|
+
volumeWrap.appendChild(volumeIcon);
|
|
9099
|
+
volumeWrap.appendChild(volumeSlider);
|
|
9100
|
+
controls.appendChild(volumeWrap);
|
|
9101
|
+
if (state.showPlaylist) {
|
|
9102
|
+
const btnPlaylist = document.createElement("button");
|
|
9103
|
+
btnPlaylist.type = "button";
|
|
9104
|
+
btnPlaylist.className = "vd-music-player-btn vd-music-player-btn-playlist";
|
|
9105
|
+
btnPlaylist.setAttribute("aria-label", "Show playlist");
|
|
9106
|
+
btnPlaylist.setAttribute("aria-expanded", "false");
|
|
9107
|
+
btnPlaylist.appendChild(icon("playlist"));
|
|
9108
|
+
controls.appendChild(btnPlaylist);
|
|
9109
|
+
}
|
|
9110
|
+
container.appendChild(controls);
|
|
9111
|
+
if (state.showProgress) {
|
|
9112
|
+
const progressRow = document.createElement("div");
|
|
9113
|
+
progressRow.className = "vd-music-player-progress";
|
|
9114
|
+
const timeElapsed = document.createElement("span");
|
|
9115
|
+
timeElapsed.className = "vd-music-player-time vd-music-player-time-elapsed";
|
|
9116
|
+
timeElapsed.textContent = "0:00";
|
|
9117
|
+
timeElapsed.setAttribute("aria-hidden", "true");
|
|
9118
|
+
const progressBar = document.createElement("input");
|
|
9119
|
+
progressBar.type = "range";
|
|
9120
|
+
progressBar.className = "vd-music-player-progress-bar";
|
|
9121
|
+
progressBar.min = "0";
|
|
9122
|
+
progressBar.max = "100";
|
|
9123
|
+
progressBar.step = "0.1";
|
|
9124
|
+
progressBar.value = "0";
|
|
9125
|
+
progressBar.setAttribute("aria-label", "Seek");
|
|
9126
|
+
const timeDuration = document.createElement("span");
|
|
9127
|
+
timeDuration.className = "vd-music-player-time vd-music-player-time-duration";
|
|
9128
|
+
timeDuration.textContent = "0:00";
|
|
9129
|
+
timeDuration.setAttribute("aria-hidden", "true");
|
|
9130
|
+
progressRow.appendChild(timeElapsed);
|
|
9131
|
+
progressRow.appendChild(progressBar);
|
|
9132
|
+
progressRow.appendChild(timeDuration);
|
|
9133
|
+
container.appendChild(progressRow);
|
|
9134
|
+
}
|
|
9135
|
+
if (state.showPlaylist) {
|
|
9136
|
+
const playlist = document.createElement("div");
|
|
9137
|
+
playlist.className = "vd-music-player-playlist";
|
|
9138
|
+
playlist.setAttribute("aria-label", "Playlist");
|
|
9139
|
+
container.appendChild(playlist);
|
|
9140
|
+
}
|
|
9141
|
+
},
|
|
9142
|
+
/* ─── Public API ──────────────────────────────────────── */
|
|
9143
|
+
/**
|
|
9144
|
+
* @param {HTMLElement} container
|
|
9145
|
+
*/
|
|
9146
|
+
play: function(container) {
|
|
9147
|
+
const inst = this.instances.get(container);
|
|
9148
|
+
if (!inst) return;
|
|
9149
|
+
if (!inst.audio.src && inst.state.tracks.length) {
|
|
9150
|
+
inst.audio.src = inst.state.tracks[inst.state.currentIndex].url;
|
|
9151
|
+
}
|
|
9152
|
+
inst.audio.play().catch(() => {
|
|
9153
|
+
});
|
|
9154
|
+
},
|
|
9155
|
+
/**
|
|
9156
|
+
* @param {HTMLElement} container
|
|
9157
|
+
*/
|
|
9158
|
+
pause: function(container) {
|
|
9159
|
+
const inst = this.instances.get(container);
|
|
9160
|
+
if (inst) inst.audio.pause();
|
|
9161
|
+
},
|
|
9162
|
+
/**
|
|
9163
|
+
* @param {HTMLElement} container
|
|
9164
|
+
*/
|
|
9165
|
+
toggle: function(container) {
|
|
9166
|
+
const inst = this.instances.get(container);
|
|
9167
|
+
if (!inst) return;
|
|
9168
|
+
if (inst.state.isPlaying) {
|
|
9169
|
+
this.pause(container);
|
|
9170
|
+
} else {
|
|
9171
|
+
this.play(container);
|
|
9172
|
+
}
|
|
9173
|
+
},
|
|
9174
|
+
/**
|
|
9175
|
+
* @param {HTMLElement} container
|
|
9176
|
+
*/
|
|
9177
|
+
next: function(container) {
|
|
9178
|
+
const inst = this.instances.get(container);
|
|
9179
|
+
if (!inst || !inst.state.tracks.length) return;
|
|
9180
|
+
const next = (inst.state.currentIndex + 1) % inst.state.tracks.length;
|
|
9181
|
+
this._loadTrack(inst, next, inst.state.isPlaying);
|
|
9182
|
+
},
|
|
9183
|
+
/**
|
|
9184
|
+
* @param {HTMLElement} container
|
|
9185
|
+
*/
|
|
9186
|
+
previous: function(container) {
|
|
9187
|
+
const inst = this.instances.get(container);
|
|
9188
|
+
if (!inst || !inst.state.tracks.length) return;
|
|
9189
|
+
const len = inst.state.tracks.length;
|
|
9190
|
+
const prev = (inst.state.currentIndex - 1 + len) % len;
|
|
9191
|
+
this._loadTrack(inst, prev, inst.state.isPlaying);
|
|
9192
|
+
},
|
|
9193
|
+
/**
|
|
9194
|
+
* @param {HTMLElement} container
|
|
9195
|
+
* @param {number} value - 0 to 1
|
|
9196
|
+
*/
|
|
9197
|
+
setVolume: function(container, value) {
|
|
9198
|
+
const inst = this.instances.get(container);
|
|
9199
|
+
if (!inst) return;
|
|
9200
|
+
const v = Math.max(0, Math.min(1, value));
|
|
9201
|
+
inst.state.volume = v;
|
|
9202
|
+
inst.audio.volume = v;
|
|
9203
|
+
if (inst.refs.volumeSlider) {
|
|
9204
|
+
inst.refs.volumeSlider.value = String(v);
|
|
9205
|
+
updateRangeFill(inst.refs.volumeSlider);
|
|
9206
|
+
}
|
|
9207
|
+
container.dispatchEvent(
|
|
9208
|
+
new CustomEvent("musicplayer:volumechange", { bubbles: true, detail: { volume: v } })
|
|
9209
|
+
);
|
|
9210
|
+
},
|
|
9211
|
+
/**
|
|
9212
|
+
* @param {HTMLElement} container
|
|
9213
|
+
* @param {number} index - Track index
|
|
9214
|
+
*/
|
|
9215
|
+
setTrack: function(container, index) {
|
|
9216
|
+
const inst = this.instances.get(container);
|
|
9217
|
+
if (!inst) return;
|
|
9218
|
+
this._loadTrack(inst, index, inst.state.isPlaying);
|
|
9219
|
+
},
|
|
9220
|
+
/**
|
|
9221
|
+
* Shuffle or un-shuffle the track list.
|
|
9222
|
+
* @param {HTMLElement} container
|
|
9223
|
+
*/
|
|
9224
|
+
shuffle: function(container) {
|
|
9225
|
+
const inst = this.instances.get(container);
|
|
9226
|
+
if (!inst || !inst.refs.btnShuffle) return;
|
|
9227
|
+
inst.refs.btnShuffle.click();
|
|
9228
|
+
},
|
|
9229
|
+
/**
|
|
9230
|
+
* Return a shallow copy of the current player state.
|
|
9231
|
+
* @param {HTMLElement} container
|
|
9232
|
+
* @returns {Object|null}
|
|
9233
|
+
*/
|
|
9234
|
+
getState: function(container) {
|
|
9235
|
+
const inst = this.instances.get(container);
|
|
9236
|
+
if (!inst) return null;
|
|
9237
|
+
const s = inst.state;
|
|
9238
|
+
return {
|
|
9239
|
+
isPlaying: s.isPlaying,
|
|
9240
|
+
currentIndex: s.currentIndex,
|
|
9241
|
+
currentTrack: s.tracks[s.currentIndex] || null,
|
|
9242
|
+
volume: s.volume,
|
|
9243
|
+
shuffle: s.shuffle,
|
|
9244
|
+
tracks: s.tracks.slice()
|
|
9245
|
+
};
|
|
9246
|
+
},
|
|
9247
|
+
/**
|
|
9248
|
+
* Stop playback, clean up listeners, remove instance.
|
|
9249
|
+
* @param {HTMLElement} container
|
|
9250
|
+
*/
|
|
9251
|
+
destroy: function(container) {
|
|
9252
|
+
const inst = this.instances.get(container);
|
|
9253
|
+
if (!inst) return;
|
|
9254
|
+
inst.cleanup.forEach((fn) => fn());
|
|
9255
|
+
this.instances.delete(container);
|
|
9256
|
+
container.removeAttribute("data-music-player-initialized");
|
|
9257
|
+
},
|
|
9258
|
+
/**
|
|
9259
|
+
* Destroy all instances.
|
|
9260
|
+
*/
|
|
9261
|
+
destroyAll: function() {
|
|
9262
|
+
this.instances.forEach((_, container) => this.destroy(container));
|
|
9263
|
+
},
|
|
9264
|
+
/* ─── Internal helpers ────────────────────────────────── */
|
|
9265
|
+
/**
|
|
9266
|
+
* Load track by index on an already-initialised instance object.
|
|
9267
|
+
* @param {Object} inst
|
|
9268
|
+
* @param {number} index
|
|
9269
|
+
* @param {boolean} autoPlay
|
|
9270
|
+
*/
|
|
9271
|
+
_loadTrack: function(inst, index, autoPlay) {
|
|
9272
|
+
const track = inst.state.tracks[index];
|
|
9273
|
+
if (!track) return;
|
|
9274
|
+
const container = this._containerOf(inst);
|
|
9275
|
+
inst.state.currentIndex = index;
|
|
9276
|
+
inst.audio.src = track.url;
|
|
9277
|
+
if (inst.refs.trackName) {
|
|
9278
|
+
inst.refs.trackName.textContent = track.name || "Unknown Track";
|
|
9279
|
+
inst.refs.trackName.classList.remove("is-idle");
|
|
9280
|
+
}
|
|
9281
|
+
if (inst.refs.playlistPanel) {
|
|
9282
|
+
inst.refs.playlistPanel.querySelectorAll(".vd-music-player-playlist-item").forEach((item, i) => {
|
|
9283
|
+
const active = i === index;
|
|
9284
|
+
item.classList.toggle("is-active", active);
|
|
9285
|
+
item.setAttribute("aria-current", active ? "true" : "false");
|
|
9286
|
+
});
|
|
9287
|
+
}
|
|
9288
|
+
if (inst.refs.progressBar) {
|
|
9289
|
+
inst.refs.progressBar.value = "0";
|
|
9290
|
+
updateRangeFill(inst.refs.progressBar);
|
|
9291
|
+
}
|
|
9292
|
+
if (inst.refs.timeElapsed) inst.refs.timeElapsed.textContent = "0:00";
|
|
9293
|
+
if (inst.refs.timeDuration) inst.refs.timeDuration.textContent = "0:00";
|
|
9294
|
+
if (container) {
|
|
9295
|
+
container.dispatchEvent(
|
|
9296
|
+
new CustomEvent("musicplayer:trackchange", {
|
|
9297
|
+
bubbles: true,
|
|
9298
|
+
detail: { index, name: track.name, url: track.url }
|
|
9299
|
+
})
|
|
9300
|
+
);
|
|
9301
|
+
}
|
|
9302
|
+
if (autoPlay) inst.audio.play().catch(() => {
|
|
9303
|
+
});
|
|
9304
|
+
},
|
|
9305
|
+
/**
|
|
9306
|
+
* Reverse-lookup the container element for a given instance object.
|
|
9307
|
+
* @param {Object} inst
|
|
9308
|
+
* @returns {HTMLElement|null}
|
|
9309
|
+
*/
|
|
9310
|
+
_containerOf: function(inst) {
|
|
9311
|
+
for (const [container, i] of this.instances) {
|
|
9312
|
+
if (i === inst) return container;
|
|
9313
|
+
}
|
|
9314
|
+
return null;
|
|
9315
|
+
}
|
|
9316
|
+
};
|
|
9317
|
+
if (typeof window.Vanduo !== "undefined") {
|
|
9318
|
+
window.Vanduo.register("musicPlayer", MusicPlayer);
|
|
9319
|
+
}
|
|
9320
|
+
window.VanduoMusicPlayer = MusicPlayer;
|
|
9321
|
+
})();
|
|
9322
|
+
|
|
8568
9323
|
// js/index.js
|
|
8569
9324
|
var Vanduo = window.Vanduo;
|
|
8570
9325
|
var index_default = Vanduo;
|