PageSourceSearch

https://www.myownmusic.de/include/js/mom-maxiplayer.js?v=1786167064

js myownmusic.de collected 2026-10-02 04:46:36 UTC 16,778 bytes, 377 lines download raw bytes

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.