PageSourceSearch

https://senat.bi/wp-content/plugins/public/js/player.js?ver=1.0.0

js senat.bi collected 2026-10-02 11:51:47 UTC 25,362 bytes, 547 lines download raw bytes

1/* =========================================================
2   WP Audio Player Pro — player.js  v2.0
3   Design "barre inline" — correspond à l'image de référence
4   =========================================================
5   Autoplay fix :
6   - preload="auto" forcé → audio chargé immédiatement
7   - Tentative play() dès 'canplay' (pas d'attente load)
8   - Si NotAllowedError → muted play + overlay unmute
9   - Le slider volume et le bouton mute NE sont PAS altérés
10     lors du fallback (volume affiché ouvert par défaut)
11   ========================================================= */
12(function () {
13  'use strict';
14
15  const cfg = window.wappCfg || {};
16
17  /* ── Utilitaires ──────────────────────────────────────── */
18  function fmt(sec) {
19    sec = Math.floor(sec || 0);
20    return Math.floor(sec / 60) + ':' + ('0' + (sec % 60)).slice(-2);
21  }
22
23  function ajax(data) {
24    if (!cfg.ajax) return;
25    fetch(cfg.ajax, {
26      method: 'POST',
27      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
28      body: new URLSearchParams({ nonce: cfg.nonce || '', ...data }),
29    }).catch(() => {});
30  }
31
32  /* ── Menu de partage (réseaux sociaux) ─────────────────── */
33  const SHARE_NETWORKS = [
34    {
35      key: 'facebook',
36      label: 'shareFacebook',
37      icon: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M22 12c0-5.52-4.48-10-10-10S2 6.48 2 12c0 4.84 3.44 8.87 8 9.8V15H8v-3h2V9.5C10 7.57 11.57 6 13.5 6H16v3h-2c-.55 0-1 .45-1 1v2h3v3h-3v6.95c5.05-.5 9-4.76 9-9.95z"/></svg>',
38      url: (u) => 'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(u),
39    },
40    {
41      key: 'twitter',
42      label: 'shareTwitter',
43      icon: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.26 10.99h-6.466l-5.06-6.62-5.79 6.62H1.96l7.73-8.835L1.5 2.25h6.59l4.583 6.06zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>',
44      url: (u, t) => 'https://twitter.com/intent/tweet?url=' + encodeURIComponent(u) + '&text=' + encodeURIComponent(t || ''),
45    },
46    {
47      key: 'whatsapp',
48      label: 'shareWhatsapp',
49      icon: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.149-.149.347-.388.521-.581.173-.198.231-.347.347-.581.116-.233.058-.43-.034-.581-.084-.149-.624-1.498-.866-2.05-.221-.49-.444-.43-.61-.43-.165 0-.347-.025-.521-.025-.173 0-.456.067-.69.337-.232.27-.892.875-.892 2.117 0 1.243.916 2.448 1.043 2.617.13.167 1.78 2.72 4.31 3.704 2.531.984 2.531.658 2.99.617.456-.042 1.478-.605 1.685-1.19.207-.582.207-1.08.145-1.189-.058-.108-.231-.173-.521-.322zM12.025 22h-.004a9.94 9.94 0 01-5.07-1.388l-.364-.217-3.762.987 1.004-3.667-.236-.378A9.928 9.928 0 012.025 12C2.025 6.486 6.512 2 12.025 2c2.652 0 5.144 1.034 7.018 2.91A9.91 9.91 0 0122 12.014C22 17.53 17.512 22 12.025 22z"/></svg>',
50      url: (u, t) => 'https://wa.me/?text=' + encodeURIComponent((t ? t + ' - ' : '') + u),
51    },
52    {
53      key: 'telegram',
54      label: 'shareTelegram',
55      icon: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21.946 4.616a1.5 1.5 0 0 0-1.527-.27L2.6 11.077a1.397 1.397 0 0 0 .073 2.616l4.498 1.49 1.722 5.55a1.04 1.04 0 0 0 1.79.36l2.52-2.79 4.55 3.36a1.39 1.39 0 0 0 2.22-.85l3.18-14.6a1.5 1.5 0 0 0-.207-1.617zM9.1 14.18l-3.93-1.3 12.36-5.06zM9.96 16.4l-.71-2.29 8.34-5.97-7.63 8.26zm1.97 4.5-.99-3.18 1.93-1.7 1.86 4.88z"/></svg>',
56      url: (u, t) => 'https://t.me/share/url?url=' + encodeURIComponent(u) + '&text=' + encodeURIComponent(t || ''),
57    },
58    {
59      key: 'email',
60      label: 'shareEmail',
61      icon: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4-8 5-8-5V6l8 5 8-5v2z"/></svg>',
62      url: (u, t) => 'mailto:?subject=' + encodeURIComponent(t || '') + '&body=' + encodeURIComponent(u),
63    },
64  ];
65
66  function closeShareMenu() {
67    const existing = document.querySelector('.wapp-share-menu');
68    if (existing) {
69      const ownerBtn = existing._ownerBtn;
70      existing.remove();
71      if (ownerBtn) ownerBtn.setAttribute('aria-expanded', 'false');
72    }
73  }
74
75  function buildShareMenu(url, title) {
76    const menu = document.createElement('div');
77    menu.className = 'wapp-share-menu';
78
79    SHARE_NETWORKS.forEach(net => {
80      const a = document.createElement('a');
81      a.className = 'wapp-share-item wapp-share-' + net.key;
82      a.href = net.url(url, title);
83      a.target = '_blank';
84      a.rel = 'noopener noreferrer';
85      a.innerHTML = net.icon + '<span>
85' + ((cfg.i18n && cfg.i18n[net.label]) || net.key) + '</span>';
86      a.addEventListener('click', () => closeShareMenu());
87      menu.appendChild(a);
88    });
89
90    return menu;
91  }
92
93  function toggleShareMenu(btn, url, title) {
94    const existing = document.querySelector('.wapp-share-menu');
95    if (existing) {
96      const wasOwner = existing._ownerBtn === btn;
97      closeShareMenu();
98      if (wasOwner) return;
99    }
100
101    const menu = buildShareMenu(url, title);
102    menu._ownerBtn = btn;
103    document.body.appendChild(menu);
104
105    const r = btn.getBoundingClientRect();
106    const menuW = menu.offsetWidth || 170;
107    let left = r.right - menuW;
108    if (left < 8) left = 8;
109    if (left + menuW > window.innerWidth - 8) left = window.innerWidth - menuW - 8;
110    let top = r.top - menu.offsetHeight - 6;
111    if (top < 8) top = r.bottom + 6; // si pas de place au-dessus, ouvrir vers le bas
112
113    menu.style.left = left + 'px';
114    menu.style.top = top + 'px';
115
116    requestAnimationFrame(() => menu.classList.add('open'));
117    btn.setAttribute('aria-expanded', 'true');
118  }
119
120  // Fermer le menu de partage au clic extérieur, à l'échappement, scroll ou resize
121  document.addEventListener('click', e => {
122    if (!e.target.closest('.wapp-share-menu, .wapp-bar-share, .wapp-pl-share-btn')) closeShareMenu();
123  });
124  document.addEventListener('keydown', e => {
125    if (e.key === 'Escape') closeShareMenu();
126  });
127  window.addEventListener('scroll', closeShareMenu, true);
128  window.addEventListener('resize', closeShareMenu);
129
130  /* ══════════════════════════════════════════════════════
131     AUTOPLAY ENGINE v2.0
132     ────────────────────────────────────────────────────
133     1. Force preload="auto" pour déclencher le chargement
134     2. Écoute 'canplay' — tente play() non-muté
135     3. NotAllowedError → bascule muted SANS toucher
136        au slider ni au bouton mute (UI reste "son ouvert")
137     4. Overlay "Cliquez pour activer le son"
138     5. Au clic utilisateur → unmute propre
139  ══════════════════════════════════════════════════════ */
140  function tryAutoplay(audio, wrap, volPct, onSuccess) {
141    /* Garantir que l'audio charge */
142    if (audio.preload !== 'auto') audio.preload = 'auto';
143    if (!audio.src && audio.dataset && audio.dataset.url) audio.src = audio.dataset.url;
144
145    function attempt() {
146      audio.muted  = false;
147      audio.volume = Math.min(1, (volPct || 80) / 100);
148
149      audio.play()
150        .then(() => { if (onSuccess) onSuccess(); })
151        .catch(err => {
152          if (err.name !== 'NotAllowedError') return;
153
154          /* Fallback muted — NE pas toucher à l'UI volume */
155          audio.muted = true;
156          wrap.classList.add('wapp-muted-autoplay');
157
158          audio.play()
159            .then(() => showUnmuteOverlay(audio, wrap, volPct))
160            .catch(() => {
161              wrap.classList.remove('wapp-muted-autoplay');
162              audio.muted  = false;
163              audio.volume = Math.min(1, (volPct || 80) / 100);
164            });
165        });
166    }
167
168    /* readyState 3 = HAVE_FUTURE_DATA, 4 = HAVE_ENOUGH_DATA */
169    if (audio.readyState >= 3) {
170      attempt();
171    } else {
172      audio.addEventListener('canplay', attempt, { once: true });
173      /* Filet de sécurité : si canplay ne se déclenche pas dans 2s */
174      setTimeout(() => { if (audio.paused) attempt(); }, 2000);
175    }
176  }
177
178  function showUnmuteOverlay(audio, wrap, volPct) {
179    if (wrap.querySelector('.wapp-unmute-overlay')) return;
180
181    const ov = document.createElement('div');
182    ov.className = 'wapp-unmute-overlay';
183    ov.innerHTML =
184      '<div class="wapp-unmute-inner">' +
185        '<span class="wapp-unmute-icon">🔊</span>' +
186        '<span class="wapp-unmute-label">' +
187          ((cfg.i18n && cfg.i18n.clickUnmute) || 'Cliquez pour activer le son') +
188        '</span>' +
189      '</div>';
190    wrap.appendChild(ov);
191
192    function unmute(e) {
193      if (e && e.isTrusted === false) return;
194      audio.muted  = false;
195      audio.volume = Math.min(1, (volPct || 80) / 100);
196
197      /* Synchro UI mute button si présent */
198      const btn = wrap.querySelector('.wapp-bar-mute');
199      if (btn) btn.classList.remove('muted');
200
201      wrap.classList.remove('wapp-muted-autoplay');
202      ov.classList.add('wapp-unmute-fading');
203      setTimeout(() => ov.remove(), 380);
204      ['click', 'keydown', 'touchend'].forEach(ev =>
205        document.removeEventListener(ev, unmute)
206      );
207    }
208
209    ov.addEventListener('click', unmute);
210    setTimeout(() => {
211      ['click', 'keydown', 'touchend'].forEach(ev =>
212        document.addEventListener(ev, unmute, { once: true, passive: true })
213      );
214    }, 120);
215  }
216
217  /* ══════════════════════════════════════════════════════
218     INIT PLAYER BAR
219  ══════════════════════════════════════════════════════ */
220  function initPlayer(wrap) {
221    const d = wrap.dataset;
222    if (!d.url) return;
223
224    /* Créer l'élément <audio> */
225    const audio    = document.createElement('audio');
226    audio.preload  = 'auto';          /* toujours "auto" pour autoplay fiable */
227    audio.loop     = d.loop === '1';
228    audio.volume   = Math.min(1, Math.max(0, (parseInt(d.volume) || 80) / 100));
229    audio.src      = d.url;
230
231    /* Refs DOM */
232    const btnPlay   = wrap.querySelector('.wapp-bar-play');
233    const iconPlay  = wrap.querySelector('.wapp-icon-play');
234    const iconPause = wrap.querySelector('.wapp-icon-pause');
235    const btnMute   = wrap.querySelector('.wapp-bar-mute');
236    const iconVolOn = wrap.querySelector('.wapp-icon-vol-on');
237    const iconVolOff= wrap.querySelector('.wapp-icon-vol-off');
238    const waveBars  = Array.from(wrap.querySelectorAll('.wapp-bar-wbar'));
239    const waveSeek  = wrap.querySelector('.wapp-bar-wave-seek');
240    const cursor    = wrap.querySelector('.wapp-bar-cursor');
241    const timerEl   = wrap.querySelector('.wapp-time-current');
242    const btnLoop   = wrap.querySelector('.wapp-bar-loop');
243    const btnShare  = wrap.querySelector('.wapp-bar-share');
244    const btnDown   = wrap.querySelector('.wapp-bar-download');
245    const btnSpeed  = wrap.querySelector('.wapp-bar-speed');
246    const speedMenu = wrap.querySelector('.wapp-bar-speed-menu');
247
248    /* ── UI play/pause ───────────────────────────────── */
249    function setPlayUI(playing) {
250      if (iconPlay)  iconPlay.style.display  = playing ? 'none' : '';
251      if (iconPause) iconPause.style.display = playing ? '' : 'none';
252      wrap.classList.toggle('wapp-playing', playing);
253    }
254
255    /* ── UI mute ─────────────────────────────────────── */
256    function setMuteUI(muted) {
257      if (!btnMute) return;
258      btnMute.classList.toggle('muted', muted);
259      if (iconVolOn)  iconVolOn.style.display  = muted ? 'none' : '';
260      if (iconVolOff) iconVolOff.style.display = muted ? '' : 'none';
261    }
262    /* Initialisation : son ouvert */
263    setMuteUI(false);
264
265    /* ── Progression & waveform ──────────────────────── */
266    function updateProgress() {
267      const pct = audio.duration ? audio.currentTime / audio.duration : 0;
268
269      /* Curseur */
270      if (cursor) cursor.style.left = (pct * 100) + '%';
271
272      /* Timer */
273      if (timerEl) timerEl.textContent = fmt(audio.currentTime);
274
275      /* Waveform coloring */
276      if (waveBars.length) {
277        const activeIdx = Math.round(pct * (waveBars.length - 1));
278        waveBars.forEach((b, i) => {
279          b.classList.toggle('played', i < activeIdx);
280          b.classList.toggle('active', i === activeIdx);
281        });
282      }
283    }
284
285    /* ── Seek ────────────────────────────────────────── */
286    function seekByEvent(e, el) {
287      const r = el.getBoundingClientRect();
288      const clientX = e.touches ? e.touches[0].clientX : e.clientX;
289      const pct = Math.max(0, Math.min(1, (clientX - r.left) / r.width));
290      if (audio.duration) {
291        audio.currentTime = pct * audio.duration;
292        ajax({ action: 'wapp_event', event: 'seek', id: d.id || '' });
293      }
294    }
295
296    /* ── Play/pause toggle ───────────────────────────── */
297    function togglePlay() {
298      if (audio.paused) {
299        audio.play()
300          .then(() => ajax({ action: 'wapp_event', event: 'play', id: d.id || '' }))
301          .catch(() => {});
302      } else {
303        audio.pause();
304        ajax({ action: 'wapp_event', event: 'pause', id: d.id || '' });
305      }
306    }
307
308    /* ── Événements audio ────────────────────────────── */
309    audio.addEventListener('play',        () => setPlayUI(true));
310    audio.addEventListener('pause',       () => setPlayUI(false));
311    audio.addEventListener('timeupdate',  updateProgress);
312    audio.addEventListener('loadedmetadata', updateProgress);
313    audio.addEventListener('ended', () => {
314      setPlayUI(false);
315      ajax({ action: 'wapp_event', event: 'complete', id: d.id || '' });
316    });
317
318    /* ── Bouton play ─────────────────────────────────── */
319    if (btnPlay) btnPlay.addEventListener('click', togglePlay);
320
321    /* ── Bouton mute ─────────────────────────────────── */
322    if (btnMute) btnMute.addEventListener('click', () => {
323      audio.muted = !audio.muted;
324      setMuteUI(audio.muted);
325    });
326
327    /* ── Seek sur waveform ───────────────────────────── */
328    if (waveSeek) {
329      let dragging = false;
330      waveSeek.addEventListener('mousedown', e => { dragging = true; seekByEvent(e, waveSeek); });
331      document.addEventListener('mousemove', e => { if (dragging) seekByEvent(e, waveSeek); });
332      document.addEventListener('mouseup',   () => { dragging = false; });
333      waveSeek.addEventListener('touchstart', e => seekByEvent(e, waveSeek), { passive: true });
334      waveSeek.addEventListener('touchmove',  e => seekByEvent(e, waveSeek), { passive: true });
335    }
336
337    /* ── Loop ────────────────────────────────────────── */
338    if (btnLoop) btnLoop.addEventListener('click', () => {
339      audio.loop = !audio.loop;
340      btnLoop.classList.toggle('active', audio.loop);
341    });
342
343    /* ── Share ───────────────────────────────────────── */
344    if (btnShare) btnShare.addEventListener('click', e => {
345      e.preventDefault(); e.stopPropagation();
346      toggleShareMenu(btnShare, d.url || window.location.href, d.title || document.title);
347    });
348
349    /* ── Speed ───────────────────────────────────────── */
350    if (btnSpeed && speedMenu) {
351      btnSpeed.addEventListener('click', e => {
352        e.stopPropagation();
353        speedMenu.classList.toggle('open');
354      });
355      speedMenu.querySelectorAll('button[data-speed]').forEach(b => {
356        b.addEventListener('click', () => {
357          audio.playbackRate = parseFloat(b.dataset.speed) || 1;
358          btnSpeed.textContent = b.dataset.speed + '×';
359          speedMenu.querySelectorAll('button').forEach(x => x.classList.remove('active'));
360          b.classList.add('active');
361          speedMenu.classList.remove('open');
362        });
363      });
364      document.addEventListener('click', e => {
365        if (btnSpeed && !btnSpeed.contains(e.target)) speedMenu.classList.remove('open');
366      });
367    }
368
369    /* ── Clavier ─────────────────────────────────────── */
370    if (d.keyboard === '1') {
371      wrap.setAttribute('tabindex', '0');
372      wrap.addEventListener('keydown', e => {
373        const map = {
374          'Space':      () => { e.preventDefault(); togglePlay(); },
375          'ArrowRight': () => { e.preventDefault(); audio.currentTime = Math.min(audio.duration || 0, audio.currentTime + 5); },
376          'ArrowLeft':  () => { e.preventDefault(); audio.currentTime = Math.max(0, audio.currentTime - 5); },
377          'ArrowUp':    () => { e.preventDefault(); audio.volume = Math.min(1, audio.volume + 0.1); },
378          'ArrowDown':  () => { e.preventDefault(); audio.volume = Math.max(0, audio.volume - 0.1); },
379          'KeyM':       () => { audio.muted = !audio.muted; setMuteUI(audio.muted); },
380        };
381        if (map[e.code]) map[e.code]();
382      });
383    }
384
385    /* ── Start at ────────────────────────────────────── */
386    const startAt = parseInt(d.startAt) || 0;
387    if (startAt > 0) {
388      audio.addEventListener('loadedmetadata', () => { audio.currentTime = startAt; }, { once: true });
389    }
390
391    /* ══════════════════════════════════════════════════
392       AUTOPLAY
393       preload="auto" est déjà défini → le navigateur
394       charge le fichier dès que possible.
395       On lance tryAutoplay() :
396       - immédiatement si le doc est prêt
397       - sinon on attend DOMContentLoaded (pas "load"
398         qui est trop tardif sur certains navigateurs)
399    ══════════════════════════════════════════════════ */
400    if (d.autoplay === '1') {
401      const volPct = parseInt(d.volume) || 80;
402      const doAP   = () => tryAutoplay(audio, wrap, volPct, null);
403
404      if (document.readyState !== 'loading') {
405        /* Page déjà interactive → lancer tout de suite */
406        doAP();
407      } else {
408        document.addEventListener('DOMContentLoaded', doAP, { once: true });
409      }
410    }
411  }
412
413  /* ══════════════════════════════════════════════════════
414     PLAYLIST (inchangée v1.2)
415  ══════════════════════════════════════════════════════ */
416  function initPlaylist(wrap) {
417    const d      = wrap.dataset;
418    const audio  = wrap.querySelector('.wapp-pl-audio');
419    if (!audio) return;
420
421    const trackEls = Array.from(wrap.querySelectorAll('.wapp-pl-track'));
422    let current = -1, looping = false, shuffled = d.shuffle === '1';
423    let order   = trackEls.map((_, i) => i);
424
425    function buildOrder() {
426      order = trackEls.map((_, i) => i);
427      if (shuffled) order.sort(() => Math.random() - 0.5);
428    }
429    buildOrder();
430
431    const btnPlay    = wrap.querySelector('.wapp-pl-btn-play');
432    const btnPrev    = wrap.querySelector('.wapp-pl-btn-prev');
433    const btnNext    = wrap.querySelector('.wapp-pl-btn-next');
434    const btnShuffle = wrap.querySelector('.wapp-pl-btn-shuffle');
435    const btnLoop    = wrap.querySelector('.wapp-pl-btn-loop');
436    const progFill   = wrap.querySelector('.wapp-pl-progress-fill');
437    const progThumb  = wrap.querySelector('.wapp-pl-progress-thumb');
438    const progWrap   = wrap.querySelector('.wapp-pl-progress');
439    const timeCur    = wrap.querySelector('.wapp-pl-time-cur');
440    const timeTot    = wrap.querySelector('.wapp-pl-time-tot');
441    const titleEl    = wrap.querySelector('.wapp-pl-title');
442    const artistEl   = wrap.querySelector('.wapp-pl-artist');
443    const imgEl      = wrap.querySelector('.wapp-pl-img');
444
445    if (shuffled && btnShuffle) btnShuffle.classList.add('active');
446
447    function loadTrack(idx, andPlay) {
448      const el = trackEls[idx];
449      if (!el) return;
450      current = idx;
451      const td = el.dataset;
452      audio.src = td.url || ''; audio.preload = 'auto'; audio.load();
453      if (titleEl)  titleEl.textContent  = td.title  || '—';
454      if (artistEl) artistEl.textContent = td.artist || '';
455      if (imgEl) {
456        if (td.image) { imgEl.src = td.image; imgEl.style.display = ''; }
457        else imgEl.style.display = 'none';
458      }
459      trackEls.forEach(t => t.classList.remove('active'));
460      el.classList.add('active');
461      el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
462      if (andPlay) tryAutoplay(audio, wrap.querySelector('.wapp-pl-player') || wrap, 80, null);
463    }
464
465    function playAdj(dir) {
466      const ni = (order.indexOf(current) + dir + order.length) % order.length;
467      loadTrack(order[ni], true);
468    }
469
470    function updateProg() {
471      const pct = audio.duration ? (audio.currentTime / audio.duration) * 100 : 0;
472      if (progFill)  progFill.style.width  = pct + '%';
473      if (progThumb) progThumb.style.left   = pct + '%';
474      if (timeCur)   timeCur.textContent    = fmt(audio.currentTime);
475      if (timeTot)   timeTot.textContent    = fmt(audio.duration);
476    }
477
478    function setUI(playing) {
479      if (btnPlay) btnPlay.textContent = playing ? '⏸' : '▶';
480      wrap.classList.toggle('wapp-playing', playing);
481    }
482
483    audio.addEventListener('timeupdate', updateProg);
484    audio.addEventListener('loadedmetadata', updateProg);
485    audio.addEventListener('play',  () => setUI(true));
486    audio.addEventListener('pause', () => setUI(false));
487    audio.addEventListener('ended', () => {
488      if (looping) { audio.play(); return; }
489      if (d.continuous === '1') playAdj(1); else setUI(false);
490    });
491
492    if (btnPlay) btnPlay.addEventListener('click', () => {
493      if (current < 0) { loadTrack(order[0], true); return; }
494      audio.paused ? audio.play().catch(() => {}) : audio.pause();
495    });
496    if (btnNext)    btnNext.addEventListener('click', () => playAdj(1));
497    if (btnPrev)    btnPrev.addEventListener('click', () => {
498      if (audio.currentTime > 3) { audio.currentTime = 0; return; }
499      playAdj(-1);
500    });
501    if (btnShuffle) btnShuffle.addEventListener('click', () => {
502      shuffled = !shuffled; btnShuffle.classList.toggle('active', shuffled); buildOrder();
503    });
504    if (btnLoop)    btnLoop.addEventListener('click', () => {
505      looping = !looping; btnLoop.classList.toggle('active', looping);
506    });
507    if (progWrap)   progWrap.addEventListener('click', e => {
508      const r = progWrap.getBoundingClientRect();
509      if (audio.duration) audio.currentTime = ((e.clientX - r.left) / r.width) * audio.duration;
510    });
511
512    trackEls.forEach(el => {
513      el.addEventListener('click', e => {
514        if (e.target.closest('.wapp-pl-act-btn')) return;
515        const ti = parseInt(el.dataset.index);
516        if (ti === current && !audio.paused) { audio.pause(); return; }
517        loadTrack(ti, true);
518      });
519      const shareBtn = el.querySelector('.wapp-pl-share-btn');
520      if (shareBtn) shareBtn.addEventListener('click', e => {
521        e.stopPropagation();
522        const url = el.dataset.url || shareBtn.dataset.url || window.location.href;
523        const title = el.dataset.title || '';
524        toggleShareMenu(shareBtn, url, title);
525      });
526    });
527
528    if (d.autoplay === '1' && trackEls.length) {
529      const doAP = () => loadTrack(order[0], true);
530      if (document.readyState !== 'loading') doAP();
531      else document.addEventListener('DOMContentLoaded', doAP, { once: true });
532    }
533  }
534
535  /* ── Boot ─────────────────────────────────────────────── */
536  function boot() {
537    document.querySelectorAll('.wapp-player-wrap').forEach(initPlayer);
538    document.querySelectorAll('.wapp-playlist-wrap').forEach(initPlaylist);
539  }
540
541  if (document.readyState === 'loading') {
542    document.addEventListener('DOMContentLoaded', boot);
543  } else {
544    boot();
545  }
546
547})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.