1/** 2 * MOM Maxi-Player (Vollbild-Player, Player V3) 3 * --------------------------------------------------------------------------- 4 * Vollflächiges Overlay: groÃes Cover links, Songinfos rechts, Waveform unten 5 * über die volle Breite. Läuft NUR auf Desktop (>= 1024px). 6 * 7 * Bewusst ein eigenes Modul (kein Teil der Song-Seite): so überlebt der Player 8 * Soft-Navigation und bleibt beim automatischen Weiterspielen offen â er hängt 9 * ausschlieÃlich an MOM.audio und zeigt immer den Song, der gerade läuft. 10 * 11 * API: window.MOM.maxiplayer = { open, close, toggle, isOpen } 12 */ 13(function (window, $) { 14 'use strict'; 15 if (!window.MOM || !window.MOM.audio) return; 16 if (window.MOM.maxiplayer) return; 17 18 var A = window.MOM.audio; 19 var $root = null; // Overlay-Knoten 20 var WF = null; // eigene Waveform-Instanz (nur solange offen) 21 var open = false; 22 var infoSongId = null; // Song, dessen Infos gerade im DOM stehen 23 var bound = false; // MOM.audio kennt kein off() -> nur einmal binden 24 var castBound = false; // dito für MOM.cast 25 var mqDesktop = window.matchMedia('(min-width: 1024px)'); 26 27 function fmt(sec) { 28 return (window.MOM.waveform && MOM.waveform.fmt) 29 ? MOM.waveform.fmt(sec) 30 : '0:00'; 31 } 32 33 function esc(s) { 34 return $('<i>').text(s == null ? '' : String(s)).html(); 35 } 36 37 /* Cast/AirPlay-Zeichen als Inline-SVG: das ist das Symbol, das Videoplayer 38 seit Jahren benutzen, und FontAwesome 4.3 (die Fassung im Projekt) hat 39 nichts Vergleichbares. currentColor, damit die Knopf-Zustände greifen. */ 40 function icon(connected) { 41 return '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path fill="currentColor" d="' 42 + (connected 43 ? 'M1 18v3h3c0-1.66-1.34-3-3-3zm0-4v2c2.76 0 5 2.24 5 5h2c0-3.87-3.13-7-7-7zm18-7H5v1.63c3.96 1.28 7.09 4.41 8.37 8.37H19V7zM1 10v2c4.97 0 9 4.03 9 9h2c0-6.08-4.93-11-11-11zm20-7H3c-1.1 0-2 .9-2 2v3h2V5h18v14h-7v2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z' 44 : 'M1 18v3h3c0-1.66-1.34-3-3-3zm0-4v2c2.76 0 5 2.24 5 5h2c0-3.87-3.13-7-7-7zm0-4v2c4.97 0 9 4.03 9 9h2c0-6.08-4.93-11-11-11zm20-7H3c-1.1 0-2 .9-2 2v3h2V5h18v14h-7v2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z') 45 + '"/></svg>'; 46 } 47 48 var CAST_LABEL = { 49 disconnected: 'Auf einem anderen Gerät abspielen', 50 connecting: 'Verbindung zum Gerät wird hergestellt â¦', 51 connected: 'Läuft auf einem anderen Gerät â Gerät wechseln oder beenden' 52 }; 53 54 // Platzhalter-Cover -> kein Ambient-Effekt (sonst nur ein grauer Schleier) 55 function isRealCover(url) { 56 return !!url && String(url).indexOf('no_cover') === -1; 57 } 58 59 function html() { 60 return '' + 61 '<div class="mom-maxi" id="momMaxi" hidden aria-hidden="true" role="dialog" aria-modal="true" aria-label="Vollbild-Player">' + 62 ' <div class="mom-maxi__ambient"></div>' + 63 ' <button type="button" class="mom-maxi__close" data-act="close" title="Vollbild verlassen (Esc)" aria-label="Vollbild verlassen">' + 64 ' <i class="fa fa-times"></i></button>' + 65 ' <div class="mom-maxi__stage">' + 66 ' <div class="mom-maxi__art"><img alt="" /></div>' + 67 ' <div class="mom-maxi__info">' + 68 ' <h2 class="mom-maxi__title"></h2>' + 69 ' <div class="mom-maxi__artist"><a href="#"></a></div>' + 70 ' <div class="mom-maxi__chips"></div>' + 71 ' <div class="mom-maxi__meta"></div>' + 72 ' <div class="mom-maxi__controls">' + 73 ' <button type="button" class="mom-maxi__btn" data-act="prev" aria-label="Vorheriger Song"><i class="fa fa-step-backward"></i></button>' + 74 ' <button type="button" class="mom-maxi__btn mom-maxi__btn--play" data-act="toggle" aria-label="Play/Pause"><i class="fa fa-play"></i></button>' + 75 ' <button type="button" class="mom-maxi__btn" data-act="next" aria-label="Nächster Song"><i class="fa fa-step-forward"></i></button>' + 76 // Cast-Knopf vorerst stillgelegt (2026-08-06). Alles Ãbrige bleibt: der 77 // Sender (mom-cast.js), der eigene Receiver (player/cast/) und die 78 // Zustandslogik unten (updateCast, CAST_LABEL, icon) sind unverändert 79 // vorhanden. Zum Wiedereinschalten genügt es, die beiden Zeilen unten 80 // zu entkommentieren â updateCast findet den Knopf dann von selbst und 81 // blendet ihn ein, sobald ein Gerät in Reichweite ist. 82 // Sinnvoll erst zusammen mit einer eingetragenen CAST_APP_ID 83 // (lib/lib_init.inc.php), sonst zeigt der Fernseher nur seinen 84 // Standardbildschirm statt unseres Players. 85 // ' <button type="button" class="mom-maxi__btn mom-maxi__btn--cast" data-act="cast" hidden' + 86 // ' aria-label="' + CAST_LABEL.disconnected + '" title="' + CAST_LABEL.disconnected + '">' + icon(false) + '</button>' + 87 ' </div>' + 88 ' </div>' + 89 ' </div>' + 90 ' <div class="mom-maxi__wavebar">' + 91 ' <div class="mom-maxi__wave" id="momMaxiWave"></div>' + 92 ' <div class="mom-maxi__times"><span class="mom-maxi__pos">0:00</span><span class="mom-maxi__dur">0:00</span></div>' + 93 ' </div>' + 94 '</div>'; 95 } 96 97 function render() { 98 if ($root && $root.length && document.body.contains($root[0])) return true; 99 $(document.body).append(html()); 100 $root = $('#momMaxi'); 101 102 $root.on('click', '[data-act]', function () { 103 var act = $(this).data('act'); 104 if (act === 'close') close();
105 else if (act === 'toggle') A.toggle(); 106 else if (act === 'next') A.next(); 107 else if (act === 'prev') A.prev(); 108 else if (act === 'cast' && window.MOM.cast) MOM.cast.prompt(); 109 }); 110 111 // Nur einmal abonnieren: render() läuft nach einer Soft-Navigation 112 // erneut, wenn der Overlay-Knoten mit dem Body verschwunden ist. 113 if (window.MOM.cast && !castBound) { 114 castBound = true; 115 MOM.cast.on(function () { updateCast(); wakeUp(); }); 116 } 117 updateCast(); 118 119 return true; 120 } 121 122 /* Der Knopf folgt dem, was der Browser meldet: kein Gerät in Reichweite 123 (oder ein Browser ohne die Schnittstellen) â er bleibt weg. */ 124 function updateCast() { 125 if (!$root) return; 126 var $b = $root.find('.mom-maxi__btn--cast'); 127 if (!$b.length) return; 128 129 if (!window.MOM.cast || !MOM.cast.available()) { 130 $b.prop('hidden', true); 131 return; 132 } 133 var st = MOM.cast.state(); 134 var label = CAST_LABEL[st] || CAST_LABEL.disconnected; 135 $b.prop('hidden', false) 136 .toggleClass('is-on', st === 'connected') 137 .toggleClass('is-connecting', st === 'connecting') 138 .html(icon(st === 'connected' || st === 'connecting')) 139 .attr({ 'title': label, 'aria-label': label }); 140 } 141 142 /* ------------------------------------------------------------- Inhalte */ 143 144 // Basisdaten kommen sofort aus dem Queue-Item (kein Flackern), die 145 // ausführlichen Infos (Genre, Mood, KI, Upload) danach vom Server. 146 function fillFromItem(item) { 147 if (!item) return; 148 $root.find('.mom-maxi__title').text(item.songName || ''); 149 $root.find('.mom-maxi__artist a') 150 .text(item.artistNick || '') 151 .attr('href', item.artistUrlNick ? '/' + item.artistUrlNick + '/' : '#'); 152 $root.find('.mom-maxi__dur').text(fmt(item.timeSec)); 153 setCover(item.imageUrl); 154 } 155 156 // fallback: das Original kann im Storage fehlen, während die kleine Variante da 157 // ist â dann lieber unscharf als gar kein Bild, danach Platzhalter 158 function setCover(url, fallback) { 159 $root.find('.mom-maxi__art img') 160 .off('error.momcover') 161 .on('error.momcover', function () { 162 if (fallback && this.src.indexOf(fallback) === -1) { this.src = fallback; return; } 163 $(this).off('error.momcover'); 164 this.src = MOM.noCover(512); 165 }) 166 .attr('src', url || ''); 167 var $amb = $root.find('.mom-maxi__ambient'); 168 if (isRealCover(url)) { 169 $amb.css('--mom-ambient', 'url(' + url + ')').addClass('is-on'); 170 } else { 171 $amb.removeClass('is-on').css('--mom-ambient', ''); 172 } 173 } 174 175 function loadInfo(songId) { 176 if (!songId || String(songId) === String(infoSongId)) return; 177 infoSongId = String(songId); 178 179 $.post('/player/get/songinfo', { songid: songId }, null, 'json').done(function (resp) { 180 var d = resp && resp[0] && resp[0].songinfo; 181 if (!d || !open || String(d.songId) !== String(infoSongId)) return; // Songwechsel überholt 182 183 $root.find('.mom-maxi__title').text(d.songName || ''); 184 $root.find('.mom-maxi__artist a') 185 .text(d.artistName || '') 186 .attr('href', d.artistUrlNick ? '/' + d.artistUrlNick + '/' : '#'); 187 188 // GroÃes Cover erst hier â coverImageLarge gibt es nur vom Endpoint 189 if (d.coverImageLarge) setCover(d.coverImageLarge, d.coverImage); 190 191 var chips = ''; 192 if (d.mainstyle) 193 chips += '<a class="mom-chip" href="/' + esc(d.mainstyleSlug) + '/">' + esc(d.mainstyle) + '</a>'; 194 if (d.style) 195 chips += '<a class="mom-chip" href="/' + esc(d.mainstyleSlug) + '/' + esc(d.styleSlug) + '/">' + esc(d.style) + '</a>'; 196 if (d.moodName) 197 chips += '<span class="mom-chip mom-chip--soft">' + esc(d.moodName) + '</span>';
198 // Wie das Badge auf der Song-Seite: im Chip nur die Stufe, die 199 // deklarierten Bereiche erst im Tooltip 200 if (d.aiLabel) 201 chips += '<span class="mom-chip mom-chip--ai" title="' + esc(d.aiLabel + (d.aiAreasLabel ? ': ' + d.aiAreasLabel : '')) + '">' + esc(d.aiLabel) + '</span>'; 202 $root.find('.mom-maxi__chips').html(chips); 203 204 $root.find('.mom-maxi__meta').html(d.uploadDate 205 ? '<i class="fa fa-upload"></i> ' + esc(d.uploadDate) : ''); 206 }); 207 } 208 209 function showSong(item) { 210 if (!item) return; 211 fillFromItem(item); 212 loadInfo(item.songId); 213 if (WF) WF.load(item.songId, item.timeSec); 214 $root.find('.mom-maxi__pos').text('0:00'); 215 } 216 217 function updatePlayState() { 218 var paused = A.state().paused; 219 $root.find('.mom-maxi__btn--play i') 220 .removeClass('fa-play fa-pause') 221 .addClass(paused ? 'fa-play' : 'fa-pause'); 222 } 223 224 /* -------------------------------------------------------- öffnen/schlieÃen */ 225 226 /* Echtes Browser-Vollbild (Fullscreen API) â nur ein Zusatz: schlägt es fehl 227 (fehlende Nutzergeste, Richtlinie des Browsers), bleibt das Overlay trotzdem 228 nutzbar. Verlässt der Nutzer das Browser-Vollbild per Esc/F11, schlieÃt der 229 fullscreenchange-Handler weiter unten auch das Overlay. */ 230 function requestBrowserFullscreen() { 231 var el = document.documentElement; 232 var fn = el.requestFullscreen || el.webkitRequestFullscreen || el.msRequestFullscreen; 233 if (!fn) return; 234 try { 235 var p = fn.call(el); 236 if (p && p.catch) p.catch(function () {}); 237 } catch (e) {} 238 } 239 240 function exitBrowserFullscreen() { 241 if (!(document.fullscreenElement || document.webkitFullscreenElement)) return; 242 var fn = document.exitFullscreen || document.webkitExitFullscreen || document.msExitFullscreen; 243 if (!fn) return; 244 try { 245 var p = fn.call(document); 246 if (p && p.catch) p.catch(function () {}); 247 } catch (e) {} 248 } 249 250 function doOpen() { 251 if (open || !mqDesktop.matches) return; 252 var cur = A.current(); 253 if (!cur) return; // ohne laufenden Song kein Vollbild 254 255 render(); 256 open = true; 257 requestBrowserFullscreen(); 258 infoSongId = null; 259 $root.prop('hidden', false).attr('aria-hidden', 'false'); 260 document.body.classList.add('mom-maxi-open'); 261 262 // Waveform ERST jetzt mounten: ein verstecktes Element misst 0x0 263 var el = document.getElementById('momMaxiWave'); 264 if (el && window.MOM.waveform) { 265 WF = MOM.waveform.mount(el, { 266 onSeek: function (sec) { A.seek(sec); A.play(); } 267 }); 268 } 269 270 showSong(cur); 271 var st = A.state(); 272 if (WF) WF.setPosition(st.position); 273 $root.find('.mom-maxi__pos').text(fmt(st.position)); 274 if (st.duration) $root.find('.mom-maxi__dur').text(fmt(st.duration)); 275 updatePlayState(); 276 wakeUp(); // Bedienung zeigen, Ruhe-Timer starten 277 } 278 279 function close() { 280 if (!open) return; 281 open = false; 282 stopIdleWatch(); 283 exitBrowserFullscreen(); 284 document.body.classList.remove('mom-maxi-open'); 285 if ($root) $root.prop('hidden', true).attr('aria-hidden', 'true'); 286 if (WF) { try { WF.destroy(); } catch (e) {} WF = null; } 287 infoSongId = null; 288 } 289 290 // Browser-Vollbild anders verlassen (Esc, F11, Fenstertausch) â Overlay mitnehmen, 291 // damit beide Zustände nicht auseinanderlaufen 292 $(document).on('fullscreenchange webkitfullscreenchange', function () { 293 if (open && !(document.fullscreenElement || document.webkitFullscreenElement)) close(); 294 }); 295 296 function toggle() { open ? close() : doOpen(); } 297 298 /* ------------------------------------------------------------------ Ruhe 299 * Ohne Mausbewegung tritt die Bedienung zurück (Klasse is-idle, Details im 300 * CSS): es bleiben nur Cover, Artist, Titel und Waveform â die Schrift wächst. 301 * Jede Bewegung oder Eingabe holt die Bedienung sofort zurück. 302 */ 303 var IDLE_MS = 2600; 304 var idleTimer = null; 305 306 function goIdle() { 307 if (open && $root) $root.addClass('is-idle'); 308 } 309 310 function wakeUp() { 311 if (!open) return; 312 if ($root) $root.removeClass('is-idle'); 313 clearTimeout(idleTimer); 314 idleTimer = setTimeout(goIdle, IDLE_MS); 315 } 316 317 function stopIdleWatch() { 318 clearTimeout(idleTimer); 319 if ($root) $root.removeClass('is-idle'); 320 } 321 322 $(document).on('mousemove mousedown wheel touchstart keydown', function () { 323 if (open) wakeUp(); 324 }); 325 326 /* ----------------------------------------------------------- Engine-Events */ 327 328 if (!bound) { 329 bound = true; 330 331 // Auch beim automatischen Weiterspielen bleibt das Overlay offen â 332 // es tauscht nur seinen Inhalt aus. 333 A.on('songchange', function (item) { if (open) showSong(item); }); 334 A.on('timeupdate', function (d) { 335 if (!open) return; 336 if (WF) WF.setPosition(d.position); 337 $root.find('.mom-maxi__pos').text(fmt(d.position)); 338 if (d.duration) $root.find('.mom-maxi__dur').text(fmt(d.duration)); 339 }); 340 A.on('play', function () { if (open) updatePlayState(); }); 341 A.on('pause', function () { if (open) updatePlayState(); }); 342 // Bewusst KEIN SchlieÃen bei 'queue-ended': das Vollbild bleibt mit dem 343 // letzten Song stehen (pausiert). Verlassen wird der Modus nur vom Nutzer. 344 } 345 346 /* --------------------------------------------------------------- Einstiege */ 347 348 // Mini-Player-Button und Play-Seite (delegiert, überlebt Soft-Navigation) 349 $(document).on('click', '[data-mom-maxi], .mom-play__maxi-open', function (e) { 350 e.preventDefault(); 351 toggle(); 352 }); 353 354 // Tastatur: F schaltet um, Esc schlieÃt â nie während einer Eingabe 355 $(document).on('keydown', function (e) { 356 if (e.key === 'Escape' && open) { close(); return; } 357 if (e.key !== 'f' && e.key !== 'F') return; 358 if (e.ctrlKey || e.metaKey || e.altKey) return; 359 var t = e.target;
360 if (t && (t.isContentEditable || /^(input|textarea|select)$/i.test(t.tagName))) return; 361 e.preventDefault(); 362 toggle(); 363 }); 364 365 // Unter 1024px gibt es den Modus nicht â offenes Overlay sauber schlieÃen 366 function onBreakpoint() { if (!mqDesktop.matches) close(); } 367 if (mqDesktop.addEventListener) mqDesktop.addEventListener('change', onBreakpoint); 368 else if (mqDesktop.addListener) mqDesktop.addListener(onBreakpoint); 369 370 window.MOM.maxiplayer = { 371 open: doOpen, 372 close: close, 373 toggle: toggle, 374 isOpen: function () { return open; } 375 }; 376 377})(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.