PageSourceSearch

https://dancingastronaut.com/wp-content/themes/da-theme/assets/js/persistent-player.js?ver=1.0.1

js dancingastronaut.com collected 2026-09-24 18:21:32 UTC 8,940 bytes, 264 lines download raw bytes

1/**
2 * DA Persistent Audio Player
3 * Sticky 60px bottom bar with HTML5 Audio API.
4 *
5 * API:
6 *   window.daPlayer.play(url, title, artist, art)
7 *   window.daPlayer.pause()
8 *   window.daPlayer.resume()
9 *   window.daPlayer.toggle()
10 *   window.daPlayer.isPlaying  {boolean}
11 */
12(function () {
13  'use strict';
14
15  var audio = new Audio();
16  var bar = null;
17  var els = {};
18  var progressTimer = null;
19
20  // -----------------------------------------------------------------------
21  // Build the player bar DOM (only once)
22  // -----------------------------------------------------------------------
23  function buildBar() {
24    if (bar) return;
25
26    bar = document.createElement('div');
27    bar.id = 'da-player-bar';
28    bar.style.cssText = [
29      'position:fixed',
30      'bottom:0',
31      'left:0',
32      'right:0',
33      'height:60px',
34      'background:#111111',
35      'border-top:1px solid #1a1a1a',
36      'display:none',
37      'align-items:center',
38      'padding:0 16px',
39      'gap:12px',
40      'z-index:99999',
41      'font-family:var(--font-body,Inter,sans-serif)',
42    ].join(';');
43
44    // Album art
45    var art = document.createElement('img');
46    art.id = 'da-player-art';
47    art.width = 40;
48    art.height = 40;
49    art.style.cssText = 'width:40px;height:40px;object-fit:cover;border-radius:4px;flex-shrink:0;';
50    art.alt = '';
51    els.art = art;
52
53    // Title + artist
54    var info = document.createElement('div');
55    info.style.cssText = 'flex:1;min-width:0;';
56
57    var titleEl = document.createElement('div');
58    titleEl.id = 'da-player-title';
59    titleEl.style.cssText = 'color:#fff;font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
60    els.title = titleEl;
61
62    var artistEl = document.createElement('div');
63    artistEl.id = 'da-player-artist';
64    artistEl.style.cssText = 'color:#a0a0a0;font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:2px;';
65    els.artist = artistEl;
66
67    info.appendChild(titleEl);
68    info.appendChild(artistEl);
69
70    // Play/Pause button
71    var playBtn = document.createElement('button');
72    playBtn.id = 'da-player-playbtn';
73    playBtn.setAttribute('aria-label', 'Play/Pause');
74    playBtn.style.cssText = 'background:none;border:none;color:#fff;cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;flex-shrink:0;';
75    playBtn.innerHTML = svgPlay();
76    playBtn.addEventListener('click', function () { daPlayer.toggle(); });
77    els.playBtn = playBtn;
78
79    // Progress container
80    var progressWrap = document.createElement('div');
81    progressWrap.style.cssText = 'flex:2;display:flex;flex-direction:column;gap:4px;min-width:80px;max-width:300px;';
82
83    var progressBar = document.createElement('div');
84    progressBar.style.cssText = 'position:relative;height:3px;background:#1a1a1a;border-radius:2px;cursor:pointer;';
85
86    var progressFill = document.createElement('div');
87    progressFill.id = 'da-player-progress';
88    progressFill.style.cssText = 'height:100%;width:0%;background:#e91e8c;border-radius:2px;pointer-events:none;transition:width 0.25s linear;';
89    els.progressFill = progressFill;
90
91    progressBar.appendChild(progressFill);
92    progressBar.addEventListener('click', function (e) {
93      if (!audio.duration) return;
94      var rect = progressBar.getBoundingClientRect();
95      var pct  = (e.clientX - rect.left) / rect.width;
96      audio.currentTime = pct * audio.duration;
97    });
98
99    var timeEl = document.createElement('div');
100    timeEl.id = 'da-player-time';
101    timeEl.style.cssText = 'color:#a0a0a0;font-size:10px;text-align:right;';
102    timeEl.textContent = '0:00 / 0:00';
103    els.time = timeEl;
104
105    progressWrap.appendChild(progressBar);
106    progressWrap.appendChild(timeEl);
107
108    // Close button
109    var closeBtn = document.createElement('button');
110    closeBtn.setAttribute('aria-label', 'Close player');
111    closeBtn.style.cssText = 'background:none;border:none;color:#a0a0a0;cursor:pointer;padding:4px;display:flex;align-items:center;flex-shrink:0;';
112    closeBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
113    closeBtn.addEventListener('click', function () {
114      audio.pause();
115      audio.src = '';
116      daPlayer.isPlaying = false;
117      bar.style.display = 'none';
118      clearInterval(progressTimer);
119      document.body.style.paddingBottom = '';
120    });
121
122    bar.appendChild(art);
123    bar.appendChild(info);
124    bar.appendChild(playBtn);
125    bar.appendChild(progressWrap);
126    bar.appendChild(closeBtn);
127
128    document.body.appendChild(bar);
129
130    // Audio events
131    audio.addEventListener('play',  function () {
132      daPlayer.isPlaying = true;
133      els.playBtn.innerHTML = svgPause();
134      startProgressTimer();
135    });
136    audio.addEventListener('pause', function () {
137      daPlayer.isPlaying = false;
138      els.playBtn.innerHTML = svgPlay();
139      clearInterval(progressTimer);
140    });
141    audio.addEventListener('ended', function () {
142      daPlayer.isPlaying = false;
143      els.playBtn.innerHTML = svgPlay();
144      clearInterval(progressTimer);
145      els.progressFill.style.width = '0%';
146    });
147    audio.addEventListener('timeupdate', updateProgress);
148  }
149
150  // -----------------------------------------------------------------------
151  // Progress tick
152  // -----------------------------------------------------------------------
153  function startProgressTimer() {
154    clearInterval(progressTimer);
155    progressTimer = setInterval(updateProgress, 500);
156  }
157
158  function updateProgress() {
159    if (!audio.duration) return;
160    var pct = (audio.currentTime / audio.duration) * 100;
161    els.progressFill.style.width = pct + '%';
162    els.time.textContent = formatTime(audio.currentTime) + ' / ' + formatTime(audio.duration);
163  }
164
165  function formatTime(s) {
166    s = Math.floor(s || 0);
167    var m = Math.floor(s / 60);
168    var sec = s % 60;
169    return m + ':' + (sec < 10 ? '0' : '') + sec;
170  }
171
172  // -----------------------------------------------------------------------
173  // SVG helpers
174  // -----------------------------------------------------------------------
175  function svgPlay() {
176    return '<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>';
177  }
178
179  function svgPause() {
180    return '<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>';
181  }
182
183  // -----------------------------------------------------------------------
184  // Keyboard: Space toggles when bar is visible
185  // -----------------------------------------------------------------------
186  document.addEventListener('keydown', function (e) {
187    if (e.code !== 'Space') return;
188    if (!bar || bar.style.display === 'none') return;
189    // Don't intercept if user is typing in an input
190    var tag = document.activeElement ? document.activeElement.tagName.toLowerCase() : '';
191    if (tag === 'input' || tag === 'textarea' || tag === 'select') return;
192    e.preventDefault();
193    daPlayer.toggle();
194  });
195
196  // -----------------------------------------------------------------------
197  // Public API
198  // -----------------------------------------------------------------------
199  var daPlayer = {
200    isPlaying: false,
201
202    play: function (url, title, artist, art) {
203      if (!url) return;
204
205      // Spotify full URL → open in new tab
206      if (/open\.spotify\.com\/(track|album|artist|playlist)/.test(url)) {
207        window.open(url, '_blank', 'noopener,noreferrer');
208        return;
209      }
210
211      buildBar();
212
213      // Update metadata
214      els.title.textContent  = title  || 'Unknown Track';
215      els.artist.textContent = artist || '';
216      if (art) {
217        els.art.src   = art;
218        els.art.style.display = 'block';
219      } else {
220        els.art.style.display = 'none';
221      }
222
223      // Reset progress
224      els.progressFill.style.width = '0%';
225      els.time.textContent = '0:00 / 0:00';
226
227      // Load + play
228      if (audio.src !== url) {
229        audio.src = url;
230        audio.load();
231      }
232
233      audio.play().catch(function (err) {
234        console.warn('[daPlayer] play error:', err);
235      });
236
237      // Show bar
238      bar.style.display = 'flex';
239      document.body.style.paddingBottom = '60px';
240    },
241
242    pause: function () {
243      audio.pause();
244    },
245
246    resume: function () {
247      if (audio.src) {
248        audio.play().catch(function (err) {
249          console.warn('[daPlayer] resume error:', err);
250        });
251      }
252    },
253
254    toggle: function () {
255      if (daPlayer.isPlaying) {
256        daPlayer.pause();
257      } else {
258        daPlayer.resume();
259      }
260    },
261  };
262
263  window.daPlayer = daPlayer;
264})();

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.