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/dist-cjs/ui.js CHANGED
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.mountStandardPlayerUI = mountStandardPlayerUI;
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 mountStandardPlayerUI(player, root, options = {}) {
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">&times;</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">&times;</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: false,
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(doc, "#debug-panel");
70
- const dbgSrc = requiredElement(doc, "#dbg-src");
71
- const dbgCurrentTime = requiredElement(doc, "#dbg-current-time");
72
- const dbgDuration = requiredElement(doc, "#dbg-duration");
73
- const dbgReadyState = requiredElement(doc, "#dbg-ready-state");
74
- const dbgNetworkState = requiredElement(doc, "#dbg-network-state");
75
- const dbgPaused = requiredElement(doc, "#dbg-paused");
76
- const dbgEvents = requiredElement(doc, "#dbg-events");
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.applyAccentColor(root);
90
- unmounts.push(player.mountTrackInfo(title, artist));
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: "Riproduci",
94
- pauseLabel: "Pausa",
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
  }