1/** 2 * MOM Mini-Player (Player V3) 3 * --------------------------------------------------------------------------- 4 * Persistente Player-Leiste: dockt auf dem Desktop unter dem Header, 5 * auf Mobile über der Bottom-Navigation. Rendert in #momMiniPlayer 6 * (Knoten kommt aus site_header.inc.php) und spricht mit MOM.audio. 7 */ 8(function (window, $) { 9 'use strict'; 10 if (!window.MOM || !window.MOM.audio) return; 11 if (window.MOM.miniplayer) return; 12 13 var A = window.MOM.audio; 14 var $root = null; 15 16 function fmt(sec) { 17 sec = Math.max(0, Math.round(sec || 0)); 18 var m = Math.floor(sec / 60), s = sec % 60; 19 return m + ':' + (s < 10 ? '0' : '') + s; 20 } 21 22 // Cover-Fallback-Kette: die 70x70-Variante fehlt auf dem Asset-Host für 23 // manche Songs (Generierung hängt) â auf das statisch ausgelieferte 24 // Original umschreiben (/covers_70x70/{id}_70x70.ext â /covers/{id}.ext), 25 // und wenn auch das fehlt â Platzhalter. 26 function coverError() { 27 var img = this; 28 var src = img.getAttribute('src') || ''; 29 var m = src.match(/^(.*)\/covers_(\d+)x\2\/(\d+)_\2x\2\.([A-Za-z0-9]+)(\?.*)?$/); 30 if (m) { img.src = m[1] + '/covers/' + m[3] + '.' + m[4]; return; } 31 img.onerror = null; 32 img.src = MOM.noCover(35); 33 } 34 35 function html() { 36 return '' + 37 '<div class="mom-mini" hidden>' + 38 ' <div class="mom-mini__inner">' + 39 ' <a class="mom-mini__art" href="#"><img alt="" /></a>' + 40 ' <div class="mom-mini__info">' + 41 ' <a class="mom-mini__title" href="#"></a>' + 42 ' <a class="mom-mini__artist" href="#"></a>' + 43 ' </div>' + 44 ' <div class="mom-mini__controls">' + 45 ' <button type="button" class="mom-mini__btn" data-act="prev" aria-label="Zurück"><i class="fa fa-step-backward"></i></button>' + 46 ' <button type="button" class="mom-mini__btn mom-mini__btn--play" data-act="toggle" aria-label="Play/Pause"><i class="fa fa-play"></i></button>' + 47 ' <button type="button" class="mom-mini__btn" data-act="next" aria-label="Weiter"><i class="fa fa-step-forward"></i></button>' + 48 ' </div>' + 49 ' <div class="mom-mini__time"><span class="mom-mini__pos">0:00</span><span class="mom-mini__dur">0:00</span></div>' + 50 ' <button type="button" class="mom-mini__btn mom-mini__btn--maxi" data-mom-maxi aria-label="Vollbild-Player" title="Vollbild-Player (F)"><i class="fa fa-arrows-alt"></i></button>' + 51 (A.volumeSupported() ? 52 ' <div class="mom-mini__vol">' + 53 ' <button type="button" class="mom-mini__btn mom-mini__btn--vol" data-act="volume" aria-label="Lautstärke" title="Lautstärke" aria-expanded="false"><i class="fa fa-volume-up"></i></button>' + 54 ' <div class="mom-mini__volpanel" hidden>' + 55 ' <button type="button" class="mom-mini__btn mom-mini__volmute" data-act="mute" aria-label="Stumm schalten"><i class="fa fa-volume-up"></i></button>' + 56 ' <input type="range" class="mom-mini__volrange" min="0" max="1" step="0.01" aria-label="Lautstärke">' + 57 ' </div>' + 58 ' </div>' : '') + 59 ' <button type="button" class="mom-mini__btn mom-mini__btn--queue" data-act="queue" aria-label="Queue anzeigen">' + 60 ' <i class="fa fa-list-ul"></i><span class="mom-mini__qcount"></span>' + 61 ' </button>' + 62 ' <button type="button" class="mom-mini__btn mom-mini__btn--close" data-act="close" aria-label="Player schlieÃen"><i class="fa fa-times"></i></button>' + 63 ' </div>' + 64 ' <div class="mom-mini__progress"><div class="mom-mini__buffer"></div><div class="mom-mini__bar"></div></div>' + 65 ' <div class="mom-mini__resume" hidden><i class="fa fa-play-circle"></i> Tippen, um weiterzuhören</div>' + 66 ' <div class="mom-mini__queuepanel" hidden>' + 67 ' <div class="mom-mini__queuehead">' + 68 ' <strong>Warteschlange</strong>' + 69 ' <label class="mom-mini__autosim"><span class="mom-mini__autosim-text">Ãhnliche Songs automatisch spielen</span><span class="mom-switch"><input type="checkbox" class="mom-mini__autosim-check"><span class="mom-switch-track"></span></span></label>' + 70 ' <button type="button" class="mom-mini__btn" data-act="clear" title="Queue leeren">
70<i class="fa fa-trash-o"></i></button>' + 71 ' </div>' + 72 ' <ol class="mom-mini__queuelist"></ol>' + 73 ' </div>' + 74 '</div>'; 75 } 76 77 function render() { 78 var host = document.getElementById('momMiniPlayer'); 79 if (!host) return false; 80 host.innerHTML = html(); 81 $root = $(host).find('.mom-mini'); 82 $root.find('.mom-mini__art img').on('error', coverError); 83 bind(); 84 return true; 85 } 86 87 function updateSong(item) { 88 if (!item) return; 89 var url = '/' + item.artistUrlNick + '/play/?songid=' + item.songId; 90 $root.find('.mom-mini__art').attr('href', url).find('img').attr('src', item.imageUrl || MOM.noCover(35)); 91 $root.find('.mom-mini__title').attr('href', url).text(item.songName); 92 $root.find('.mom-mini__artist').attr('href', '/' + item.artistUrlNick + '/').text(item.artistNick); 93 $root.find('.mom-mini__dur').text(fmt(item.timeSec)); 94 // Vom Nutzer geschlossen â beim State-Restore nach Seitenwechsel NICHT 95 // wieder einblenden. Jede echte Wiedergabe/Songwahl löscht das Flag. 96 if (!A.state().dismissed) show(); 97 } 98 99 function updatePlayState() { 100 var paused = A.state().paused; 101 $root.find('.mom-mini__btn--play i') 102 .toggleClass('fa-play', paused) 103 .toggleClass('fa-pause', !paused); 104 $root.toggleClass('is-playing', !paused); 105 } 106 107 function updateProgress(pos, dur) { 108 if (!dur) dur = A.state().duration || 0; 109 var pct = dur ? Math.min(100, (pos / dur) * 100) : 0; 110 $root.find('.mom-mini__bar').css('width', pct + '%'); 111 $root.find('.mom-mini__pos').text(fmt(pos)); 112 if (dur) $root.find('.mom-mini__dur').text(fmt(dur)); 113 } 114 115 function updateQueue(list) {
116 var idx = A.queue.index(); 117 $root.find('.mom-mini__qcount').text(list.length || ''); 118 var $ol = $root.find('.mom-mini__queuelist').empty(); 119 list.forEach(function (it, i) { 120 // DOM-Aufbau statt String-Konkatenation (kein Markup aus Songdaten) 121 var $li = $('<li>', { 'class': 'mom-mini__qitem' + (i === idx ? ' is-current' : ''), 'data-songid': String(it.songId) }); 122 $('<img>', { src: it.imageUrl || MOM.noCover(35), alt: '' }).on('error', coverError).appendTo($li); 123 $('<span>', { 'class': 'mom-mini__qtitle', text: it.songName }).appendTo($li); 124 $('<span>', { 'class': 'mom-mini__qartist', text: it.artistNick }).appendTo($li); 125 $('<span>', { 'class': 'mom-mini__qtime', text: fmt(it.timeSec) }).appendTo($li); 126 $('<button>', { type: 'button', 'class': 'mom-mini__qremove', 'aria-label': 'Entfernen', html: '<i class="fa fa-times"></i>' }).appendTo($li); 127 $ol.append($li); 128 }); 129 if (!list.length) $ol.append($('<li>', { 'class': 'mom-mini__qempty', text: 'Queue ist leer' })); 130 } 131 132 function show() { $root.prop('hidden', false); $('body').addClass('mom-mini-open'); } 133 function hide() { $root.prop('hidden', true); $('body').removeClass('mom-mini-open'); } 134 135 function bind() { 136 $root.on('click', '[data-act]', function (e) { 137 e.preventDefault(); 138 var act = $(this).data('act'); 139 if (act === 'toggle') A.toggle(); 140 else if (act === 'prev') A.prev(); 141 else if (act === 'next') A.next(); 142 else if (act === 'queue') { closeVolume(); $root.find('.mom-mini__queuepanel').prop('hidden', function (_, v) { return !v; }); } 143 else if (act === 'volume') { 144 $root.find('.mom-mini__queuepanel').prop('hidden', true); // nie beide Felder offen 145 var open = $root.find('.mom-mini__volpanel').prop('hidden'); 146 $root.find('.mom-mini__volpanel').prop('hidden', !open); 147 $root.find('.mom-mini__btn--vol').attr('aria-expanded', open ? 'true' : 'false'); 148 if (open) $root.find('.mom-mini__volrange').focus(); 149 } 150 else if (act === 'mute') A.setMuted(!A.state().muted); 151 else if (act === 'clear') A.queue.clear(); 152 else if (act === 'close') { A.pause(); A.setDismissed(true); hide(); } 153 }); 154 155 // Regler: laufend anwenden. Persistenz und Icon laufen über das 156 // volumechange-Ereignis der Engine, nicht von hier aus. 157 $root.on('input', '.mom-mini__volrange', function () { A.setVolume(this.value); }); 158 159 // Fortschrittsleiste: Klick = Seek 160 $root.find('.mom-mini__progress').on('click', function (e) { 161 var rect = this.getBoundingClientRect(); 162 var frac = (e.clientX - rect.left) / rect.width; 163 var dur = A.state().duration || 0; 164 if (dur) A.seek(frac * dur); 165 }); 166 167 // Resume-Hinweis (Autoplay blockiert) 168 $root.find('.mom-mini__resume').on('click', function () { 169 $(this).prop('hidden', true); 170 A.play(); 171 }); 172 173 // Queue-Sheet und Lautstärke-Feld schlieÃen bei Klick auÃerhalb 174 $(document).on('click.momqueue', function (e) { 175 var $panel = $root.find('.mom-mini__queuepanel'); 176 if (!$panel.prop('hidden') && !$(e.target).closest('.mom-mini__queuepanel, [data-act="queue"]').length) 177 $panel.prop('hidden', true); 178 if (!$(e.target).closest('.mom-mini__vol').length) closeVolume(); 179 }); 180 181 // Queue-Interaktion 182 $root.on('click', '.mom-mini__qitem', function (e) { 183 if ($(e.target).closest('.mom-mini__qremove').length) return; 184 A.play($(this).data('songid')); 185 }); 186 $root.on('click', '.mom-mini__qremove', function (e) { 187 e.stopPropagation(); 188 A.queue.remove($(this).closest('.mom-mini__qitem').data('songid')); 189 }); 190 191 // Autoplay-ähnliche-Songs Schalter 192 $root.find('.mom-mini__autosim-check') 193 .prop('checked', A.state().autoSimilar) 194 .on('change', function () { A.setAutoSimilar(this.checked); }); 195 196 // Engine-Events 197 A.on('songchange', function (item) { updateSong(item); updateQueue(A.queue.list()); }); 198 A.on('paused-restore', function (item) { updateSong(item); updatePlayState(); updateProgress(A.state().position); }); 199 A.on('play', function () { updatePlayState(); show(); }); // hörbare Wiedergabe zeigt die Leiste immer 200 A.on('pause', updatePlayState); 201 A.on('timeupdate', function (d) { updateProgress(d.position, d.duration); }); 202 A.on('queuechange', updateQueue); 203 A.on('resume-blocked', function (item) { 204 updateSong(item); 205 updatePlayState(); 206 $root.find('.mom-mini__resume').prop('hidden', false); 207 }); 208 A.on('resumed', function () { $root.find('.mom-mini__resume').prop('hidden', true); }); 209 A.on('autosimilar', function (item) { 210 $root.find('.mom-mini__autosim').addClass('is-active'); 211 }); 212 A.on('autosimilar-toggle', function (onoff) { 213 $root.find('.mom-mini__autosim-check').prop('checked', onoff); 214 }); 215 A.on('volumechange', updateVolume); 216 updateVolume(A.state()); 217 } 218 219 function closeVolume() { 220 $root.find('.mom-mini__volpanel').prop('hidden', true); 221 $root.find('.mom-mini__btn--vol').attr('aria-expanded', 'false'); 222 } 223 224 // Icon und Regler spiegeln den Pegel der Engine. Bewusst OHNE Ausnahme für
225 // den gerade bedienten Regler: volumechange trifft asynchron ein, eine 226 // Fokus-Sperre verschluckte damit auch Ãnderungen von auÃen. Beim Ziehen 227 // wird ohnehin derselbe Wert zurückgeschrieben, der Griff bleibt ruhig. 228 function updateVolume(d) { 229 var $vol = $root.find('.mom-mini__vol'); 230 if (!$vol.length) return; 231 var v = (d && typeof d.volume === 'number') ? d.volume : A.state().volume; 232 var muted = !!(d && d.muted); 233 var cls = (muted || v === 0) ? 'fa-volume-off' : (v < 0.5 ? 'fa-volume-down' : 'fa-volume-up'); 234 $vol.find('.mom-mini__btn--vol i, .mom-mini__volmute i') 235 .removeClass('fa-volume-off fa-volume-down fa-volume-up').addClass(cls); 236 $vol.find('.mom-mini__volmute').attr('aria-label', muted ? 'Ton einschalten' : 'Stumm schalten'); 237 $vol.find('.mom-mini__volrange').val(muted ? 0 : v); 238 } 239 240 // Globaler "+ Queue"-Handler für Songlisten (Buttons tragen ihre Daten selbst; 241 // die Play-Seite bindet ihre datenlosen Buttons separat). 242 $(document).on('click', '[data-mom-queue-add][data-songid]', function (e) { 243 e.preventDefault(); 244 e.stopPropagation(); 245 var d = $(this).data(); 246 A.queue.add({ 247 songId: d.songid, songName: d.songname, artistNick: d.artistnick, 248 artistUrlNick: d.artisturlnick, imageUrl: d.imageurl, timeSec: d.timesec 249 }); 250 var $i = $(this).find('i'); 251 $i.removeClass('fa-plus').addClass('fa-check'); 252 setTimeout(function () { $i.removeClass('fa-check').addClass('fa-plus'); }, 1200); 253 // Queue-Interaktion = Player wieder erwünscht â auch wenn der Song 254 // schon in der Queue lag (Duplikat), soll die Leiste wieder erscheinen 255 if ($root) { 256 A.setDismissed(false); 257 if (A.current()) updateSong(A.current()); 258 show(); 259 } 260 }); 261 262 // Inline-Abspielen aus eingebetteten Song-Karten (z.B. Forum-Post): spielt den 263 // Song mit Metadaten sofort im Mini-Player. Klick auf die ganze Karte zählt. 264 $(document).on('click', '[data-mom-play][data-songid]', function (e) { 265 e.preventDefault(); 266 e.stopPropagation(); 267 var d = $(this).data(); 268 A.play(d.songid, { item: { 269 songId: d.songid, songName: d.songname, artistNick: d.artistnick, 270 artistUrlNick: d.artisturlnick, imageUrl: d.imageurl, timeSec: d.timesec 271 } }); 272 show(); 273 }); 274 275 // Hover-Play auf Song-Covern (Overlay aus _cards.less): Song direkt hier 276 // abspielen statt zur Song-Seite zu navigieren. Nur für Maus-Geräte â 277 // auf Touch gibt es den Hover-Button nicht, dort bleibt der Tap Navigation. 278 // Metadaten liefert /player/get/songinfo (gleiche Felder wie normalizeItem). 279 var mqHoverFine = window.matchMedia ? window.matchMedia('(hover: hover) and (pointer: fine)') : null; 280 $(document).on('click', '.col_cover > a[href*="/play/"], .rec-row__cover > a[href*="/play/"], .wg-center > a[href*="/play/"], .left > a[href*="/play/"]', function (e) { 281 if (!mqHoverFine || !mqHoverFine.matches) return; 282 if (e.which !== 1 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; // neuer Tab etc. bleibt Browser-Sache 283 if (!$(this).children('img[class*="picture_"]').length) return; // nur Cover mit Hover-Play-Overlay 284 var m = (this.getAttribute('href') || '').match(/[?&]songid=(\d+)/); 285 if (!m) return; 286 var songId = m[1], href = this.href; 287 e.preventDefault(); 288 var cur = A.current(); 289 if (cur && String(cur.songId) === songId) { A.toggle(); show(); return; } // gleicher Song â Play/Pause 290 $.post('/player/get/songinfo', { songid: songId }, null, 'json').done(function (resp) { 291 var info = resp && resp[0] && resp[0].songinfo; 292 if (!info || !info.songName || +info.blocked || +info.invisible) { // gesperrt/unsichtbar â Song-Seite erklärt es ("0" ist als String truthy!) 293 window.location.href = href; 294 return; 295 } 296 A.play(songId, { item: { 297 songId: songId, songName: info.songName, artistNick: info.artistName, 298 artistUrlNick: info.artistUrlNick, imageUrl: info.coverImage, timeSec: info.timeSec 299 } }); 300 show(); 301 }).fail(function () { window.location.href = href; }); 302 }); 303
304 // Tabbar-Höhe exakt messen â Mini-Player hängt auf Mobile bündig darüber. 305 // Auch 0 schreiben: im Landscape-Short-Fall ist die Tabbar display:none, 306 // die Var darf dann nicht auf dem alten Portrait-Wert stehen bleiben. 307 function measureTabbar() { 308 var tb = document.querySelector('.mom-tabbar'); 309 if (tb) { 310 document.documentElement.style.setProperty('--mom-tabbar-h', (tb.offsetHeight || 0) + 'px'); 311 } 312 } 313 314 // Auf Mobile darf der Knoten NICHT im Header hängen: dessen backdrop-filter 315 // macht den Header zum Containing Block für position:fixed â der Player 316 // würde oben statt über der Tabbar kleben. Unterhalb lg an <body> hängen. 317 var mqMobile = window.matchMedia('(max-width: 1023px)'); // = .below-lg (@bp-lg 1024px) 318 function placeHost() { 319 var host = document.getElementById('momMiniPlayer'); 320 if (!host) return; 321 if (mqMobile.matches) { 322 if (host.parentNode !== document.body) document.body.appendChild(host); 323 } else { 324 var bar = document.querySelector('.mom-header__bar'); 325 // vor den End-Spacer, damit der Player zwischen beiden Spacern zentriert 326 if (bar && host.parentNode !== bar) bar.insertBefore(host, document.getElementById('momMiniSpacer') || bar.querySelector('.mom-header__search')); 327 } 328 } 329 if (mqMobile.addEventListener) mqMobile.addEventListener('change', placeHost); 330 else if (mqMobile.addListener) mqMobile.addListener(placeHost); 331 332 $(function () { 333 placeHost(); 334 measureTabbar(); 335 $(window).on('resize orientationchange', measureTabbar); 336 if (!render()) return; 337 // Initialzustand (Engine hat resumeFromState evtl. schon gefeuert â Zustand nachziehen) 338 var st = A.state(); 339 if (st.songId && A.current()) { 340 updateSong(A.current()); 341 updatePlayState(); 342 updateProgress(st.position, st.duration); 343 updateQueue(st.queue); 344 } 345 }); 346 347 window.MOM.miniplayer = { show: show, hide: hide }; 348 349})(window, jQuery);
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.