smooth-player 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -12
- package/assets/icons/logo-wave-preview.svg +28 -0
- package/assets/icons/logo.svg +8 -0
- package/assets/icons/stop.svg +3 -0
- package/assets/icons/upload.svg +5 -0
- package/assets/icons/visualizer.svg +7 -0
- package/dist/SmoothPlayer.d.ts +16 -1
- package/dist/SmoothPlayer.js +295 -13
- package/dist/i18n/en.generated.d.ts +50 -0
- package/dist/i18n/en.generated.js +51 -0
- package/dist/i18n/strings.d.ts +51 -0
- package/dist/i18n/strings.js +2 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/smooth-player.css +575 -154
- package/dist/types.d.ts +31 -0
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +567 -14
- package/dist/visualizers.d.ts +2 -0
- package/dist/visualizers.js +150 -15
- package/dist-cjs/SmoothPlayer.js +295 -13
- package/dist-cjs/i18n/en.generated.js +54 -0
- package/dist-cjs/i18n/strings.js +5 -0
- package/dist-cjs/index.js +4 -2
- package/dist-cjs/ui.js +568 -15
- package/dist-cjs/visualizers.js +150 -15
- package/package.json +15 -3
- package/styles/common/_base.scss +216 -95
- package/styles/themes/_nocturne.scss +408 -62
- package/styles/themes/_aurora.scss +0 -70
- package/styles/themes/_ocean.scss +0 -13
package/dist-cjs/ui.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.mountPlayerUI = mountPlayerUI;
|
|
4
4
|
const visualizers_js_1 = require("./visualizers.js");
|
|
5
|
+
const strings_js_1 = require("./i18n/strings.js");
|
|
5
6
|
function requiredElement(scope, selector) {
|
|
6
7
|
const element = scope.querySelector(selector);
|
|
7
8
|
if (!element) {
|
|
@@ -9,9 +10,103 @@ function requiredElement(scope, selector) {
|
|
|
9
10
|
}
|
|
10
11
|
return element;
|
|
11
12
|
}
|
|
12
|
-
function
|
|
13
|
+
function renderPlayerMarkup(root, debugEnabled) {
|
|
14
|
+
root.classList.add("smooth-player");
|
|
15
|
+
root.innerHTML = `
|
|
16
|
+
<div class="smooth-player__main">
|
|
17
|
+
<div class="smooth-player__top">
|
|
18
|
+
<div class="smooth-player__top-title">${strings_js_1.strings.playlist.defaultTitle}</div>
|
|
19
|
+
<button id="shuffle-toggle" type="button" aria-label="${strings_js_1.strings.shuffle.disabledLabel}" aria-pressed="false" hidden>
|
|
20
|
+
<span class="smooth-player__icon-shuffle" aria-hidden="true"></span>
|
|
21
|
+
<span id="shuffle-text" class="smooth-player__sr-only">${strings_js_1.strings.shuffle.disabledLabel}</span>
|
|
22
|
+
</button>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="smooth-player__hero">
|
|
26
|
+
<div id="progress-ring" class="smooth-player__ring">
|
|
27
|
+
<div class="smooth-player__ring-inner">
|
|
28
|
+
<div class="smooth-player__cover">
|
|
29
|
+
<canvas id="radial-visualizer" class="smooth-player__cover-canvas" width="320" height="320"></canvas>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<button id="play" class="smooth-player__hero-play" type="button" aria-label="${strings_js_1.strings.playback.playLabel}" aria-pressed="false">
|
|
34
|
+
<span id="play-icon" class="smooth-player__icon-play" aria-hidden="true"></span>
|
|
35
|
+
<span id="play-text" class="smooth-player__sr-only">${strings_js_1.strings.playback.playLabel}</span>
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="smooth-player__meta">
|
|
40
|
+
<strong id="title">-</strong>
|
|
41
|
+
<div id="artist" class="smooth-player__artist">-</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="smooth-player__progress-wrap">
|
|
45
|
+
<input id="progress" class="smooth-player__progress" type="range" min="0" max="0" step="0.01" value="0" aria-label="Track position" />
|
|
46
|
+
<div class="smooth-player__progress-row">
|
|
47
|
+
<span id="time-current">00:00</span>
|
|
48
|
+
<span id="time-duration">00:00</span>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div class="smooth-player__transport">
|
|
53
|
+
<button id="prev" class="secondary" type="button" aria-label="Previous track">
|
|
54
|
+
<img class="smooth-player__icon" src="/assets/icons/prev.svg" alt="" />
|
|
55
|
+
<span class="smooth-player__sr-only">Previous track</span>
|
|
56
|
+
</button>
|
|
57
|
+
<button id="playlist-toggle" class="secondary smooth-player__transport-playlist" type="button" aria-label="${strings_js_1.strings.playlist.openLabel}" aria-expanded="false" hidden>
|
|
58
|
+
<img class="smooth-player__icon" src="/assets/icons/menu.svg" alt="" />
|
|
59
|
+
<span class="smooth-player__sr-only">${strings_js_1.strings.playlist.openLabel}</span>
|
|
60
|
+
</button>
|
|
61
|
+
<button id="next" class="secondary" type="button" aria-label="Next track">
|
|
62
|
+
<img class="smooth-player__icon" src="/assets/icons/next.svg" alt="" />
|
|
63
|
+
<span class="smooth-player__sr-only">Next track</span>
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<aside id="playlist-panel" class="smooth-player__playlist" aria-hidden="true">
|
|
69
|
+
<div class="smooth-player__playlist-head">
|
|
70
|
+
<h2>Playlist</h2>
|
|
71
|
+
<button id="playlist-close" class="smooth-player__playlist-close secondary" type="button" aria-label="${strings_js_1.strings.playlist.closeLabel}">
|
|
72
|
+
<span aria-hidden="true">×</span>
|
|
73
|
+
<span class="smooth-player__sr-only">${strings_js_1.strings.playlist.closeLabel}</span>
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
<ul id="playlist-list" class="smooth-player__playlist-list" role="listbox" aria-label="Track list"></ul>
|
|
77
|
+
</aside>
|
|
78
|
+
`;
|
|
79
|
+
if (!debugEnabled)
|
|
80
|
+
return;
|
|
81
|
+
const debugPanel = root.ownerDocument?.createElement("section") ?? document.createElement("section");
|
|
82
|
+
debugPanel.id = "debug-panel";
|
|
83
|
+
debugPanel.className = "smooth-player__debug";
|
|
84
|
+
debugPanel.setAttribute("aria-live", "polite");
|
|
85
|
+
debugPanel.hidden = true;
|
|
86
|
+
debugPanel.innerHTML = `
|
|
87
|
+
<h3>Audio Debug</h3>
|
|
88
|
+
<div class="smooth-player__debug-grid">
|
|
89
|
+
<div>src: <code id="dbg-src">-</code></div>
|
|
90
|
+
<div>currentTime: <code id="dbg-current-time">-</code></div>
|
|
91
|
+
<div>duration: <code id="dbg-duration">-</code></div>
|
|
92
|
+
<div>readyState: <code id="dbg-ready-state">-</code></div>
|
|
93
|
+
<div>networkState: <code id="dbg-network-state">-</code></div>
|
|
94
|
+
<div>paused: <code id="dbg-paused">-</code></div>
|
|
95
|
+
</div>
|
|
96
|
+
<pre id="dbg-events" class="smooth-player__debug-events"></pre>
|
|
97
|
+
`;
|
|
98
|
+
root.append(debugPanel);
|
|
99
|
+
}
|
|
100
|
+
function mountPlayerUI(player, root, options = {}) {
|
|
13
101
|
const doc = root.ownerDocument ?? document;
|
|
14
102
|
const debugEnabled = options.debugEnabled ?? player.getDebug();
|
|
103
|
+
const enableAudioDrop = options.enableAudioDrop ?? true;
|
|
104
|
+
const enableErrorNotice = options.enableErrorNotice ?? true;
|
|
105
|
+
const showLogo = options.showLogo ?? true;
|
|
106
|
+
const persistUserPreferences = options.persistUserPreferences ?? true;
|
|
107
|
+
const preferencesCookieName = options.userPreferencesCookieName ?? "smooth_player_prefs";
|
|
108
|
+
const preferencesMaxAgeDays = Math.max(1, options.userPreferencesMaxAgeDays ?? 365);
|
|
109
|
+
renderPlayerMarkup(root, debugEnabled);
|
|
15
110
|
const title = requiredElement(root, "#title");
|
|
16
111
|
const artist = requiredElement(root, "#artist");
|
|
17
112
|
const playlistTitle = requiredElement(root, ".smooth-player__top-title");
|
|
@@ -22,6 +117,7 @@ function mountStandardPlayerUI(player, root, options = {}) {
|
|
|
22
117
|
const playText = requiredElement(root, "#play-text");
|
|
23
118
|
const prevButton = requiredElement(root, "#prev");
|
|
24
119
|
const nextButton = requiredElement(root, "#next");
|
|
120
|
+
const transport = requiredElement(root, ".smooth-player__transport");
|
|
25
121
|
const playlistToggle = requiredElement(root, "#playlist-toggle");
|
|
26
122
|
const shuffleToggle = requiredElement(root, "#shuffle-toggle");
|
|
27
123
|
const shuffleText = requiredElement(root, "#shuffle-text");
|
|
@@ -31,8 +127,267 @@ function mountStandardPlayerUI(player, root, options = {}) {
|
|
|
31
127
|
const playlistList = requiredElement(root, "#playlist-list");
|
|
32
128
|
const radialCanvas = requiredElement(root, "#radial-visualizer");
|
|
33
129
|
const progressRing = requiredElement(root, "#progress-ring");
|
|
130
|
+
const top = playlistTitle.parentElement;
|
|
34
131
|
const unmounts = [];
|
|
35
132
|
let radial = null;
|
|
133
|
+
let noticeTimer = null;
|
|
134
|
+
let brandLogo = null;
|
|
135
|
+
let visualizerPanelOpen = false;
|
|
136
|
+
const parsePreferencesCookie = () => {
|
|
137
|
+
if (!persistUserPreferences)
|
|
138
|
+
return null;
|
|
139
|
+
const cookie = doc.cookie
|
|
140
|
+
.split(";")
|
|
141
|
+
.map((part) => part.trim())
|
|
142
|
+
.find((part) => part.startsWith(`${preferencesCookieName}=`));
|
|
143
|
+
if (!cookie)
|
|
144
|
+
return null;
|
|
145
|
+
const encoded = cookie.slice(preferencesCookieName.length + 1);
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(decodeURIComponent(encoded));
|
|
148
|
+
if (!parsed || typeof parsed !== "object")
|
|
149
|
+
return null;
|
|
150
|
+
return parsed;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const persistPreferences = () => {
|
|
157
|
+
if (!persistUserPreferences)
|
|
158
|
+
return;
|
|
159
|
+
const state = player.getState();
|
|
160
|
+
const payload = {
|
|
161
|
+
visualizer: state.visualizer,
|
|
162
|
+
spectrumStyle: state.spectrumStyle,
|
|
163
|
+
waveformStyle: state.waveformStyle,
|
|
164
|
+
shuffle: state.shuffle,
|
|
165
|
+
};
|
|
166
|
+
try {
|
|
167
|
+
const serialized = encodeURIComponent(JSON.stringify(payload));
|
|
168
|
+
const maxAge = Math.floor(preferencesMaxAgeDays * 24 * 60 * 60);
|
|
169
|
+
doc.cookie = `${preferencesCookieName}=${serialized}; Path=/; Max-Age=${maxAge}; SameSite=Lax`;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// Ignore cookie failures silently.
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
const applyStoredPreferences = () => {
|
|
176
|
+
const stored = parsePreferencesCookie();
|
|
177
|
+
if (!stored)
|
|
178
|
+
return;
|
|
179
|
+
if (stored.visualizer === "spectrum" || stored.visualizer === "waveform" || stored.visualizer === "none") {
|
|
180
|
+
player.setVisualizer(stored.visualizer);
|
|
181
|
+
}
|
|
182
|
+
if (stored.spectrumStyle && typeof stored.spectrumStyle === "object") {
|
|
183
|
+
const nextSpectrumStyle = {};
|
|
184
|
+
if (typeof stored.spectrumStyle.dualLayer === "boolean")
|
|
185
|
+
nextSpectrumStyle.dualLayer = stored.spectrumStyle.dualLayer;
|
|
186
|
+
if (typeof stored.spectrumStyle.inverted === "boolean")
|
|
187
|
+
nextSpectrumStyle.inverted = stored.spectrumStyle.inverted;
|
|
188
|
+
if (stored.spectrumStyle.barWidth === "thin" || stored.spectrumStyle.barWidth === "medium" || stored.spectrumStyle.barWidth === "large") {
|
|
189
|
+
nextSpectrumStyle.barWidth = stored.spectrumStyle.barWidth;
|
|
190
|
+
}
|
|
191
|
+
player.setSpectrumStyle(nextSpectrumStyle);
|
|
192
|
+
}
|
|
193
|
+
if (stored.waveformStyle && typeof stored.waveformStyle === "object") {
|
|
194
|
+
const nextWaveformStyle = {};
|
|
195
|
+
if (typeof stored.waveformStyle.doubleLine === "boolean")
|
|
196
|
+
nextWaveformStyle.doubleLine = stored.waveformStyle.doubleLine;
|
|
197
|
+
if (typeof stored.waveformStyle.fill === "boolean")
|
|
198
|
+
nextWaveformStyle.fill = stored.waveformStyle.fill;
|
|
199
|
+
if (typeof stored.waveformStyle.thickLine === "boolean")
|
|
200
|
+
nextWaveformStyle.thickLine = stored.waveformStyle.thickLine;
|
|
201
|
+
player.setWaveformStyle(nextWaveformStyle);
|
|
202
|
+
}
|
|
203
|
+
if (typeof stored.shuffle === "boolean") {
|
|
204
|
+
player.setShuffle(stored.shuffle);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
applyStoredPreferences();
|
|
208
|
+
if (showLogo && top instanceof HTMLElement && !top.querySelector(".smooth-player__brand")) {
|
|
209
|
+
brandLogo = doc.createElement("span");
|
|
210
|
+
brandLogo.className = "smooth-player__brand";
|
|
211
|
+
brandLogo.setAttribute("aria-hidden", "true");
|
|
212
|
+
top.insertAdjacentElement("afterbegin", brandLogo);
|
|
213
|
+
}
|
|
214
|
+
const visualizerToggle = doc.createElement("button");
|
|
215
|
+
visualizerToggle.id = "visualizer-toggle";
|
|
216
|
+
visualizerToggle.type = "button";
|
|
217
|
+
visualizerToggle.className = "secondary";
|
|
218
|
+
const visualizerIcon = doc.createElement("span");
|
|
219
|
+
visualizerIcon.className = "smooth-player__icon-visualizer";
|
|
220
|
+
visualizerIcon.setAttribute("aria-hidden", "true");
|
|
221
|
+
const visualizerText = doc.createElement("span");
|
|
222
|
+
visualizerText.className = "smooth-player__sr-only";
|
|
223
|
+
visualizerToggle.append(visualizerIcon, visualizerText);
|
|
224
|
+
const stopButton = doc.createElement("button");
|
|
225
|
+
stopButton.id = "stop";
|
|
226
|
+
stopButton.type = "button";
|
|
227
|
+
stopButton.className = "secondary";
|
|
228
|
+
stopButton.setAttribute("aria-label", strings_js_1.strings.playback.stopLabel);
|
|
229
|
+
const stopIcon = doc.createElement("span");
|
|
230
|
+
stopIcon.className = "smooth-player__icon-stop";
|
|
231
|
+
stopIcon.setAttribute("aria-hidden", "true");
|
|
232
|
+
const stopText = doc.createElement("span");
|
|
233
|
+
stopText.className = "smooth-player__sr-only";
|
|
234
|
+
stopText.textContent = strings_js_1.strings.playback.stopLabel;
|
|
235
|
+
stopButton.append(stopIcon, stopText);
|
|
236
|
+
transport.insertBefore(stopButton, nextButton);
|
|
237
|
+
const playlistTop = doc.createElement("div");
|
|
238
|
+
playlistTop.className = "smooth-player__playlist-top";
|
|
239
|
+
top?.append(playlistTop);
|
|
240
|
+
playlistTop.append(playlistToggle, playlistTitle);
|
|
241
|
+
transport.prepend(shuffleToggle);
|
|
242
|
+
shuffleToggle.hidden = false;
|
|
243
|
+
transport.append(visualizerToggle);
|
|
244
|
+
const visualizerPanel = doc.createElement("div");
|
|
245
|
+
visualizerPanel.className = "smooth-player__visualizer-panel";
|
|
246
|
+
visualizerPanel.setAttribute("aria-hidden", "true");
|
|
247
|
+
visualizerPanel.innerHTML = `
|
|
248
|
+
<div class="smooth-player__playlist-head">
|
|
249
|
+
<h2>${strings_js_1.strings.visualizer.panelTitle}</h2>
|
|
250
|
+
<button id="visualizer-close" class="smooth-player__playlist-close secondary" type="button" aria-label="${strings_js_1.strings.visualizer.closeLabel}">
|
|
251
|
+
<span aria-hidden="true">×</span>
|
|
252
|
+
<span class="smooth-player__sr-only">${strings_js_1.strings.visualizer.closeLabel}</span>
|
|
253
|
+
</button>
|
|
254
|
+
</div>
|
|
255
|
+
<div class="smooth-player__visualizer-mode-row" role="tablist" aria-label="${strings_js_1.strings.visualizer.modeLabel}">
|
|
256
|
+
<button type="button" class="smooth-player__visualizer-mode" role="tab" data-mode="spectrum" aria-label="${strings_js_1.strings.visualizer.modeSpectrum}" aria-controls="visualizer-spectrum-panel">
|
|
257
|
+
<svg class="smooth-player__visualizer-mode-icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
258
|
+
<rect x="4" y="11" width="3" height="8" rx="1"></rect>
|
|
259
|
+
<rect x="10.5" y="6" width="3" height="13" rx="1"></rect>
|
|
260
|
+
<rect x="17" y="9" width="3" height="10" rx="1"></rect>
|
|
261
|
+
</svg>
|
|
262
|
+
<span class="smooth-player__visualizer-mode-text">${strings_js_1.strings.visualizer.modeSpectrum}</span>
|
|
263
|
+
</button>
|
|
264
|
+
<button type="button" class="smooth-player__visualizer-mode" role="tab" data-mode="waveform" aria-label="${strings_js_1.strings.visualizer.modeWaveform}" aria-controls="visualizer-waveform-panel">
|
|
265
|
+
<svg class="smooth-player__visualizer-mode-icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
266
|
+
<path d="M2 12h3l2-4 3 8 3-8 2 4h7"></path>
|
|
267
|
+
</svg>
|
|
268
|
+
<span class="smooth-player__visualizer-mode-text">${strings_js_1.strings.visualizer.modeWaveform}</span>
|
|
269
|
+
</button>
|
|
270
|
+
<button type="button" class="smooth-player__visualizer-mode" role="tab" data-mode="none" aria-label="${strings_js_1.strings.visualizer.modeNone}" aria-controls="visualizer-off-panel">
|
|
271
|
+
<svg class="smooth-player__visualizer-mode-icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
272
|
+
<circle cx="12" cy="12" r="7"></circle>
|
|
273
|
+
<path d="M7 17 17 7"></path>
|
|
274
|
+
</svg>
|
|
275
|
+
<span class="smooth-player__visualizer-mode-text">${strings_js_1.strings.visualizer.modeNone}</span>
|
|
276
|
+
</button>
|
|
277
|
+
</div>
|
|
278
|
+
<div id="visualizer-spectrum-panel" class="smooth-player__visualizer-spectrum-options" role="tabpanel">
|
|
279
|
+
<div class="smooth-player__visualizer-spectrum-grid">
|
|
280
|
+
<div class="smooth-player__visualizer-spectrum-col">
|
|
281
|
+
<div class="smooth-player__visualizer-label">${strings_js_1.strings.visualizer.effectLabel}</div>
|
|
282
|
+
<label class="smooth-player__visualizer-check"><input type="checkbox" id="visualizer-effect-dual" /> ${strings_js_1.strings.visualizer.effectDualLayer}</label>
|
|
283
|
+
<label class="smooth-player__visualizer-check"><input type="checkbox" id="visualizer-effect-inverted" /> ${strings_js_1.strings.visualizer.effectInverted}</label>
|
|
284
|
+
</div>
|
|
285
|
+
<div class="smooth-player__visualizer-spectrum-col">
|
|
286
|
+
<div class="smooth-player__visualizer-label">${strings_js_1.strings.visualizer.barWidthLabel}</div>
|
|
287
|
+
<div class="smooth-player__visualizer-size-list" role="group" aria-label="${strings_js_1.strings.visualizer.barWidthLabel}">
|
|
288
|
+
<button type="button" class="smooth-player__visualizer-size" data-size="thin" aria-label="${strings_js_1.strings.visualizer.barWidthThin}">
|
|
289
|
+
<span class="smooth-player__size-bars smooth-player__size-bars--thin"><span></span><span></span><span></span></span>
|
|
290
|
+
</button>
|
|
291
|
+
<button type="button" class="smooth-player__visualizer-size" data-size="medium" aria-label="${strings_js_1.strings.visualizer.barWidthMedium}">
|
|
292
|
+
<span class="smooth-player__size-bars smooth-player__size-bars--medium"><span></span><span></span><span></span></span>
|
|
293
|
+
</button>
|
|
294
|
+
<button type="button" class="smooth-player__visualizer-size" data-size="large" aria-label="${strings_js_1.strings.visualizer.barWidthLarge}">
|
|
295
|
+
<span class="smooth-player__size-bars smooth-player__size-bars--large"><span></span><span></span><span></span></span>
|
|
296
|
+
</button>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
<div id="visualizer-waveform-panel" class="smooth-player__visualizer-waveform-options" role="tabpanel">
|
|
302
|
+
<div class="smooth-player__visualizer-label">${strings_js_1.strings.visualizer.waveformEffectLabel}</div>
|
|
303
|
+
<label class="smooth-player__visualizer-check"><input type="checkbox" id="visualizer-wave-double" /> ${strings_js_1.strings.visualizer.waveformEffectDoubleLine}</label>
|
|
304
|
+
<label class="smooth-player__visualizer-check"><input type="checkbox" id="visualizer-wave-fill" /> ${strings_js_1.strings.visualizer.waveformEffectFill}</label>
|
|
305
|
+
<label class="smooth-player__visualizer-check"><input type="checkbox" id="visualizer-wave-thick" /> ${strings_js_1.strings.visualizer.waveformEffectThickLine}</label>
|
|
306
|
+
</div>
|
|
307
|
+
<div id="visualizer-off-panel" class="smooth-player__visualizer-off-options" role="tabpanel" hidden></div>
|
|
308
|
+
`;
|
|
309
|
+
root.append(visualizerPanel);
|
|
310
|
+
const visualizerClose = visualizerPanel.querySelector("#visualizer-close");
|
|
311
|
+
const visualizerModeButtons = Array.from(visualizerPanel.querySelectorAll(".smooth-player__visualizer-mode"));
|
|
312
|
+
const spectrumOptions = visualizerPanel.querySelector(".smooth-player__visualizer-spectrum-options");
|
|
313
|
+
const waveformOptions = visualizerPanel.querySelector(".smooth-player__visualizer-waveform-options");
|
|
314
|
+
const offOptions = visualizerPanel.querySelector(".smooth-player__visualizer-off-options");
|
|
315
|
+
const effectDual = visualizerPanel.querySelector("#visualizer-effect-dual");
|
|
316
|
+
const effectInverted = visualizerPanel.querySelector("#visualizer-effect-inverted");
|
|
317
|
+
const spectrumBarWidthButtons = Array.from(visualizerPanel.querySelectorAll(".smooth-player__visualizer-size"));
|
|
318
|
+
const waveDouble = visualizerPanel.querySelector("#visualizer-wave-double");
|
|
319
|
+
const waveFill = visualizerPanel.querySelector("#visualizer-wave-fill");
|
|
320
|
+
const waveThick = visualizerPanel.querySelector("#visualizer-wave-thick");
|
|
321
|
+
const errorNotice = doc.createElement("div");
|
|
322
|
+
errorNotice.className = "smooth-player__notice";
|
|
323
|
+
errorNotice.setAttribute("role", "status");
|
|
324
|
+
errorNotice.setAttribute("aria-live", "assertive");
|
|
325
|
+
errorNotice.hidden = true;
|
|
326
|
+
root.append(errorNotice);
|
|
327
|
+
const showErrorNotice = (message) => {
|
|
328
|
+
if (!enableErrorNotice)
|
|
329
|
+
return;
|
|
330
|
+
errorNotice.textContent = message;
|
|
331
|
+
errorNotice.hidden = false;
|
|
332
|
+
errorNotice.classList.add("is-visible");
|
|
333
|
+
if (noticeTimer) {
|
|
334
|
+
window.clearTimeout(noticeTimer);
|
|
335
|
+
}
|
|
336
|
+
noticeTimer = window.setTimeout(() => {
|
|
337
|
+
errorNotice.classList.remove("is-visible");
|
|
338
|
+
errorNotice.hidden = true;
|
|
339
|
+
noticeTimer = null;
|
|
340
|
+
}, 6500);
|
|
341
|
+
};
|
|
342
|
+
const hideErrorNotice = () => {
|
|
343
|
+
if (noticeTimer) {
|
|
344
|
+
window.clearTimeout(noticeTimer);
|
|
345
|
+
noticeTimer = null;
|
|
346
|
+
}
|
|
347
|
+
errorNotice.classList.remove("is-visible");
|
|
348
|
+
errorNotice.hidden = true;
|
|
349
|
+
};
|
|
350
|
+
const clearTrackInfoState = () => {
|
|
351
|
+
title.classList.remove("is-track-exit", "is-track-enter");
|
|
352
|
+
artist.classList.remove("is-track-exit", "is-track-enter");
|
|
353
|
+
};
|
|
354
|
+
let trackInfoTimers = [];
|
|
355
|
+
const clearTrackInfoTimers = () => {
|
|
356
|
+
for (const timer of trackInfoTimers) {
|
|
357
|
+
window.clearTimeout(timer);
|
|
358
|
+
}
|
|
359
|
+
trackInfoTimers = [];
|
|
360
|
+
};
|
|
361
|
+
const renderTrackInfo = (animated) => {
|
|
362
|
+
const track = player.getCurrentTrack();
|
|
363
|
+
const nextTitle = track?.metadata?.title ?? strings_js_1.strings.track.unknownTitle;
|
|
364
|
+
const nextArtist = track?.metadata?.artist ?? strings_js_1.strings.track.unknownArtist;
|
|
365
|
+
clearTrackInfoTimers();
|
|
366
|
+
clearTrackInfoState();
|
|
367
|
+
if (!animated) {
|
|
368
|
+
title.textContent = nextTitle;
|
|
369
|
+
artist.textContent = nextArtist;
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
title.classList.add("is-track-exit");
|
|
373
|
+
trackInfoTimers.push(window.setTimeout(() => {
|
|
374
|
+
artist.classList.add("is-track-exit");
|
|
375
|
+
}, 70));
|
|
376
|
+
trackInfoTimers.push(window.setTimeout(() => {
|
|
377
|
+
title.textContent = nextTitle;
|
|
378
|
+
title.classList.remove("is-track-exit");
|
|
379
|
+
title.classList.add("is-track-enter");
|
|
380
|
+
void title.offsetWidth;
|
|
381
|
+
title.classList.remove("is-track-enter");
|
|
382
|
+
}, 180));
|
|
383
|
+
trackInfoTimers.push(window.setTimeout(() => {
|
|
384
|
+
artist.textContent = nextArtist;
|
|
385
|
+
artist.classList.remove("is-track-exit");
|
|
386
|
+
artist.classList.add("is-track-enter");
|
|
387
|
+
void artist.offsetWidth;
|
|
388
|
+
artist.classList.remove("is-track-enter");
|
|
389
|
+
}, 260));
|
|
390
|
+
};
|
|
36
391
|
const rebuildVisualizer = () => {
|
|
37
392
|
radial?.stop();
|
|
38
393
|
const mode = player.getVisualizer();
|
|
@@ -48,6 +403,156 @@ function mountStandardPlayerUI(player, root, options = {}) {
|
|
|
48
403
|
});
|
|
49
404
|
radial.start();
|
|
50
405
|
};
|
|
406
|
+
const modeLabel = (mode) => {
|
|
407
|
+
if (mode === "waveform")
|
|
408
|
+
return strings_js_1.strings.visualizer.modeWaveform;
|
|
409
|
+
if (mode === "none")
|
|
410
|
+
return strings_js_1.strings.visualizer.modeNone;
|
|
411
|
+
return strings_js_1.strings.visualizer.modeSpectrum;
|
|
412
|
+
};
|
|
413
|
+
const renderVisualizerToggle = () => {
|
|
414
|
+
const mode = player.getVisualizer();
|
|
415
|
+
const label = `${strings_js_1.strings.visualizer.toggleLabel}: ${modeLabel(mode)}`;
|
|
416
|
+
visualizerToggle.setAttribute("aria-label", label);
|
|
417
|
+
visualizerToggle.setAttribute("data-mode", mode);
|
|
418
|
+
visualizerToggle.setAttribute("aria-expanded", String(visualizerPanelOpen));
|
|
419
|
+
visualizerText.textContent = label;
|
|
420
|
+
};
|
|
421
|
+
const renderTopPlaylist = () => {
|
|
422
|
+
const playlist = player.getCurrentPlaylist();
|
|
423
|
+
playlistTitle.textContent = playlist?.title ?? strings_js_1.strings.playlist.defaultTitle;
|
|
424
|
+
};
|
|
425
|
+
const renderVisualizerPanel = () => {
|
|
426
|
+
const mode = player.getVisualizer();
|
|
427
|
+
const style = player.getSpectrumStyle();
|
|
428
|
+
const wave = player.getWaveformStyle();
|
|
429
|
+
visualizerModeButtons.forEach((button) => {
|
|
430
|
+
const active = button.dataset.mode === mode;
|
|
431
|
+
button.classList.toggle("is-active", active);
|
|
432
|
+
button.setAttribute("aria-selected", String(active));
|
|
433
|
+
button.tabIndex = active ? 0 : -1;
|
|
434
|
+
});
|
|
435
|
+
effectDual.checked = style.dualLayer;
|
|
436
|
+
effectInverted.checked = style.inverted;
|
|
437
|
+
spectrumBarWidthButtons.forEach((button) => {
|
|
438
|
+
const size = button.dataset.size;
|
|
439
|
+
const active = size === style.barWidth;
|
|
440
|
+
button.classList.toggle("is-active", active);
|
|
441
|
+
button.setAttribute("aria-pressed", String(active));
|
|
442
|
+
});
|
|
443
|
+
waveDouble.checked = wave.doubleLine;
|
|
444
|
+
waveFill.checked = wave.fill;
|
|
445
|
+
waveThick.checked = wave.thickLine;
|
|
446
|
+
const spectrumEnabled = mode === "spectrum";
|
|
447
|
+
const waveformEnabled = mode === "waveform";
|
|
448
|
+
const offEnabled = mode === "none";
|
|
449
|
+
spectrumOptions.hidden = !spectrumEnabled;
|
|
450
|
+
waveformOptions.hidden = !waveformEnabled;
|
|
451
|
+
offOptions.hidden = !offEnabled;
|
|
452
|
+
effectDual.disabled = !spectrumEnabled;
|
|
453
|
+
effectInverted.disabled = !spectrumEnabled;
|
|
454
|
+
spectrumBarWidthButtons.forEach((button) => {
|
|
455
|
+
button.disabled = !spectrumEnabled;
|
|
456
|
+
});
|
|
457
|
+
waveDouble.disabled = !waveformEnabled;
|
|
458
|
+
waveFill.disabled = !waveformEnabled;
|
|
459
|
+
waveThick.disabled = !waveformEnabled;
|
|
460
|
+
};
|
|
461
|
+
const setVisualizerPanelOpen = (open) => {
|
|
462
|
+
const activeElement = (root.ownerDocument ?? document).activeElement;
|
|
463
|
+
const focusedInsidePanel = activeElement instanceof Node && visualizerPanel.contains(activeElement);
|
|
464
|
+
visualizerPanelOpen = open;
|
|
465
|
+
visualizerPanel.classList.toggle("is-open", open);
|
|
466
|
+
if (!open && focusedInsidePanel) {
|
|
467
|
+
visualizerToggle.focus();
|
|
468
|
+
}
|
|
469
|
+
visualizerPanel.setAttribute("aria-hidden", String(!open));
|
|
470
|
+
if (open) {
|
|
471
|
+
visualizerPanel.removeAttribute("inert");
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
visualizerPanel.setAttribute("inert", "");
|
|
475
|
+
}
|
|
476
|
+
renderVisualizerToggle();
|
|
477
|
+
if (open) {
|
|
478
|
+
renderVisualizerPanel();
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
const onVisualizerModeClick = (event) => {
|
|
482
|
+
const target = event.currentTarget;
|
|
483
|
+
if (!(target instanceof HTMLButtonElement))
|
|
484
|
+
return;
|
|
485
|
+
const mode = target.dataset.mode;
|
|
486
|
+
if (mode !== "spectrum" && mode !== "waveform" && mode !== "none")
|
|
487
|
+
return;
|
|
488
|
+
player.setVisualizer(mode);
|
|
489
|
+
rebuildVisualizer();
|
|
490
|
+
renderVisualizerPanel();
|
|
491
|
+
renderVisualizerToggle();
|
|
492
|
+
persistPreferences();
|
|
493
|
+
};
|
|
494
|
+
const onSpectrumStyleChange = () => {
|
|
495
|
+
const nextStyle = {
|
|
496
|
+
dualLayer: effectDual.checked,
|
|
497
|
+
inverted: effectInverted.checked,
|
|
498
|
+
};
|
|
499
|
+
player.setSpectrumStyle(nextStyle);
|
|
500
|
+
rebuildVisualizer();
|
|
501
|
+
renderVisualizerPanel();
|
|
502
|
+
persistPreferences();
|
|
503
|
+
};
|
|
504
|
+
const onSpectrumBarWidthClick = (event) => {
|
|
505
|
+
const target = event.currentTarget;
|
|
506
|
+
if (!(target instanceof HTMLButtonElement))
|
|
507
|
+
return;
|
|
508
|
+
const size = target.dataset.size;
|
|
509
|
+
if (size !== "thin" && size !== "medium" && size !== "large")
|
|
510
|
+
return;
|
|
511
|
+
player.setSpectrumStyle({ barWidth: size });
|
|
512
|
+
rebuildVisualizer();
|
|
513
|
+
renderVisualizerPanel();
|
|
514
|
+
persistPreferences();
|
|
515
|
+
};
|
|
516
|
+
const onWaveformStyleChange = () => {
|
|
517
|
+
player.setWaveformStyle({
|
|
518
|
+
doubleLine: waveDouble.checked,
|
|
519
|
+
fill: waveFill.checked,
|
|
520
|
+
thickLine: waveThick.checked,
|
|
521
|
+
});
|
|
522
|
+
rebuildVisualizer();
|
|
523
|
+
renderVisualizerPanel();
|
|
524
|
+
persistPreferences();
|
|
525
|
+
};
|
|
526
|
+
const onVisualizerToggle = () => {
|
|
527
|
+
setVisualizerPanelOpen(!visualizerPanelOpen);
|
|
528
|
+
};
|
|
529
|
+
const onVisualizerClose = () => {
|
|
530
|
+
setVisualizerPanelOpen(false);
|
|
531
|
+
};
|
|
532
|
+
const onStop = () => {
|
|
533
|
+
player.pause();
|
|
534
|
+
player.seek(0);
|
|
535
|
+
};
|
|
536
|
+
const onShufflePersist = () => {
|
|
537
|
+
persistPreferences();
|
|
538
|
+
};
|
|
539
|
+
const onOutsidePointerDown = (event) => {
|
|
540
|
+
if (!visualizerPanelOpen)
|
|
541
|
+
return;
|
|
542
|
+
const target = event.target;
|
|
543
|
+
if (!(target instanceof Node))
|
|
544
|
+
return;
|
|
545
|
+
if (visualizerPanel.contains(target) || visualizerToggle.contains(target))
|
|
546
|
+
return;
|
|
547
|
+
setVisualizerPanelOpen(false);
|
|
548
|
+
};
|
|
549
|
+
const onEscapePanel = (event) => {
|
|
550
|
+
if (!visualizerPanelOpen)
|
|
551
|
+
return;
|
|
552
|
+
if (event.key !== "Escape")
|
|
553
|
+
return;
|
|
554
|
+
setVisualizerPanelOpen(false);
|
|
555
|
+
};
|
|
51
556
|
const playlistPanelController = player.mountPlaylistPanel({
|
|
52
557
|
root,
|
|
53
558
|
toggleButton: playlistToggle,
|
|
@@ -59,21 +564,34 @@ function mountStandardPlayerUI(player, root, options = {}) {
|
|
|
59
564
|
unmounts.push(player.mountShuffleToggle({
|
|
60
565
|
button: shuffleToggle,
|
|
61
566
|
labelElement: shuffleText,
|
|
62
|
-
initialEnabled:
|
|
567
|
+
initialEnabled: player.getShuffle(),
|
|
63
568
|
}));
|
|
64
569
|
unmounts.push(player.mountTransportControls({
|
|
65
570
|
previousButton: prevButton,
|
|
66
571
|
nextButton: nextButton,
|
|
67
572
|
}));
|
|
573
|
+
visualizerToggle.addEventListener("click", onVisualizerToggle);
|
|
574
|
+
shuffleToggle.addEventListener("click", onShufflePersist);
|
|
575
|
+
visualizerClose.addEventListener("click", onVisualizerClose);
|
|
576
|
+
stopButton.addEventListener("click", onStop);
|
|
577
|
+
visualizerModeButtons.forEach((button) => button.addEventListener("click", onVisualizerModeClick));
|
|
578
|
+
effectDual.addEventListener("change", onSpectrumStyleChange);
|
|
579
|
+
effectInverted.addEventListener("change", onSpectrumStyleChange);
|
|
580
|
+
spectrumBarWidthButtons.forEach((button) => button.addEventListener("click", onSpectrumBarWidthClick));
|
|
581
|
+
waveDouble.addEventListener("change", onWaveformStyleChange);
|
|
582
|
+
waveFill.addEventListener("change", onWaveformStyleChange);
|
|
583
|
+
waveThick.addEventListener("change", onWaveformStyleChange);
|
|
584
|
+
doc.addEventListener("pointerdown", onOutsidePointerDown);
|
|
585
|
+
doc.addEventListener("keydown", onEscapePanel);
|
|
68
586
|
if (debugEnabled) {
|
|
69
|
-
const debugPanel = requiredElement(
|
|
70
|
-
const dbgSrc = requiredElement(
|
|
71
|
-
const dbgCurrentTime = requiredElement(
|
|
72
|
-
const dbgDuration = requiredElement(
|
|
73
|
-
const dbgReadyState = requiredElement(
|
|
74
|
-
const dbgNetworkState = requiredElement(
|
|
75
|
-
const dbgPaused = requiredElement(
|
|
76
|
-
const dbgEvents = requiredElement(
|
|
587
|
+
const debugPanel = requiredElement(root, "#debug-panel");
|
|
588
|
+
const dbgSrc = requiredElement(root, "#dbg-src");
|
|
589
|
+
const dbgCurrentTime = requiredElement(root, "#dbg-current-time");
|
|
590
|
+
const dbgDuration = requiredElement(root, "#dbg-duration");
|
|
591
|
+
const dbgReadyState = requiredElement(root, "#dbg-ready-state");
|
|
592
|
+
const dbgNetworkState = requiredElement(root, "#dbg-network-state");
|
|
593
|
+
const dbgPaused = requiredElement(root, "#dbg-paused");
|
|
594
|
+
const dbgEvents = requiredElement(root, "#dbg-events");
|
|
77
595
|
unmounts.push(player.mountDebugPanel({
|
|
78
596
|
enabled: true,
|
|
79
597
|
panel: debugPanel,
|
|
@@ -86,12 +604,15 @@ function mountStandardPlayerUI(player, root, options = {}) {
|
|
|
86
604
|
eventsElement: dbgEvents,
|
|
87
605
|
}));
|
|
88
606
|
}
|
|
89
|
-
player.
|
|
90
|
-
|
|
607
|
+
player.applyTheme(root);
|
|
608
|
+
renderTrackInfo(false);
|
|
609
|
+
unmounts.push(player.on("trackchange", () => {
|
|
610
|
+
renderTrackInfo(true);
|
|
611
|
+
}));
|
|
91
612
|
unmounts.push(player.mountPlayButton(playButton, {
|
|
92
613
|
labelElement: playText,
|
|
93
|
-
playLabel:
|
|
94
|
-
pauseLabel:
|
|
614
|
+
playLabel: strings_js_1.strings.playback.playLabel,
|
|
615
|
+
pauseLabel: strings_js_1.strings.playback.pauseLabel,
|
|
95
616
|
}));
|
|
96
617
|
unmounts.push(player.mountProgress({
|
|
97
618
|
range: progress,
|
|
@@ -103,20 +624,52 @@ function mountStandardPlayerUI(player, root, options = {}) {
|
|
|
103
624
|
unmounts.push(player.mountPlaylist(playlistList, {
|
|
104
625
|
onSelect: () => playlistPanelController.setOpen(false),
|
|
105
626
|
}));
|
|
627
|
+
if (enableAudioDrop) {
|
|
628
|
+
unmounts.push(player.mountAudioDrop(root));
|
|
629
|
+
}
|
|
630
|
+
unmounts.push(player.on("error", ({ error }) => {
|
|
631
|
+
showErrorNotice(error.message);
|
|
632
|
+
}));
|
|
633
|
+
unmounts.push(player.on("play", hideErrorNotice));
|
|
634
|
+
unmounts.push(player.on("playlistchange", renderTopPlaylist));
|
|
106
635
|
const switcher = doc.createElement("div");
|
|
107
636
|
switcher.className = "smooth-player__playlist-switcher";
|
|
108
637
|
playlistHead.insertAdjacentElement("afterend", switcher);
|
|
109
638
|
unmounts.push(player.mountPlaylistSwitcher(switcher));
|
|
110
639
|
rebuildVisualizer();
|
|
640
|
+
renderVisualizerToggle();
|
|
641
|
+
renderVisualizerPanel();
|
|
642
|
+
renderTopPlaylist();
|
|
111
643
|
return {
|
|
112
644
|
rebuildVisualizer,
|
|
113
645
|
destroy: () => {
|
|
114
646
|
radial?.stop();
|
|
115
647
|
radial = null;
|
|
648
|
+
clearTrackInfoTimers();
|
|
649
|
+
clearTrackInfoState();
|
|
650
|
+
hideErrorNotice();
|
|
116
651
|
for (const unmount of unmounts) {
|
|
117
652
|
unmount();
|
|
118
653
|
}
|
|
654
|
+
visualizerToggle.removeEventListener("click", onVisualizerToggle);
|
|
655
|
+
shuffleToggle.removeEventListener("click", onShufflePersist);
|
|
656
|
+
visualizerClose.removeEventListener("click", onVisualizerClose);
|
|
657
|
+
stopButton.removeEventListener("click", onStop);
|
|
658
|
+
visualizerModeButtons.forEach((button) => button.removeEventListener("click", onVisualizerModeClick));
|
|
659
|
+
effectDual.removeEventListener("change", onSpectrumStyleChange);
|
|
660
|
+
effectInverted.removeEventListener("change", onSpectrumStyleChange);
|
|
661
|
+
spectrumBarWidthButtons.forEach((button) => button.removeEventListener("click", onSpectrumBarWidthClick));
|
|
662
|
+
waveDouble.removeEventListener("change", onWaveformStyleChange);
|
|
663
|
+
waveFill.removeEventListener("change", onWaveformStyleChange);
|
|
664
|
+
waveThick.removeEventListener("change", onWaveformStyleChange);
|
|
665
|
+
doc.removeEventListener("pointerdown", onOutsidePointerDown);
|
|
666
|
+
doc.removeEventListener("keydown", onEscapePanel);
|
|
667
|
+
visualizerToggle.remove();
|
|
668
|
+
stopButton.remove();
|
|
669
|
+
visualizerPanel.remove();
|
|
670
|
+
errorNotice.remove();
|
|
119
671
|
switcher.remove();
|
|
672
|
+
brandLogo?.remove();
|
|
120
673
|
},
|
|
121
674
|
};
|
|
122
675
|
}
|