PageSourceSearch

https://www.myownmusic.de/include/js/mom-miniplayer.js?v=1786365394

js myownmusic.de collected 2026-10-02 04:46:35 UTC 18,438 bytes, 349 lines download raw bytes

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.