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.