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