MediaWiki:Gadget-floatingYouTubePlayer.js

From Angelina Jordan Wiki
Revision as of 21:08, 28 September 2025 by Most2dot0 (talk | contribs) (bugfix)

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
(function () {
    'use strict';

    // ===== CONFIG =====
    var CONTENT_SELECTOR = '#mw-content-text';
    var HEADER_SELECTOR = '#firstHeading';
    var CORE_PLAYER_ID = 'core-youtube-player';
    var YT_SCRIPT_ID = 'youtube-iframe-api';
    var API_PROP = 'text|displaytitle|title';

    function stripTags(s) {
        var d = document.createElement('div');
        d.innerHTML = s || '';
        return d.textContent || d.innerText || '';
    }

    function parseTimeParam(val) {
        if (!val) return 0;
        val = String(val);
        if (/^\d+$/.test(val)) return parseInt(val, 10);
        var m = val.match(/(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/);
        if (m) return (parseInt(m[1] || 0, 10) * 3600) + (parseInt(m[2] || 0, 10) * 60) + (parseInt(m[3] || 0, 10));
        var digits = val.replace(/\D/g, '');
        return digits ? parseInt(digits, 10) : 0;
    }

    function extractTitleFromUrl(href) {
        try {
            var url = new URL(href, location.href);
            if (url.origin !== location.origin) return null;
            var path = url.pathname;
            var m = path.match(/^\/wiki\/(.+)/);
            if (m) return decodeURIComponent(m[1]).replace(/_/g, ' ').split(/[?#]/)[0];
            if (path.indexOf('/index.php') !== -1 || path.indexOf('/w/index.php') !== -1) {
                var t = url.searchParams.get('title');
                if (t) return decodeURIComponent(t).replace(/_/g, ' ').split(/[?#]/)[0];
            }
            return null;
        } catch (e) { return null; }
    }

    function parseYouTubeFromHref(href) {
        if (!href) return null;
        var idMatch = href.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([A-Za-z0-9_-]{5,})/);
        if (!idMatch) return null;
        var videoId = idMatch[1];
        var q = '';
        try { q = (new URL(href, location.href)).search; } catch (e) { var parts = href.split('?'); q = parts[1] ? '?' + parts[1] : ''; }
        var params = new URLSearchParams(q);
        var start = parseTimeParam(params.get('t') || params.get('start'));
        var end = params.get('end') ? parseTimeParam(params.get('end')) : null;
        return { videoId: videoId, start: start, end: end };
    }

    // ===== CorePlayer =====
    var CorePlayer = (function () {
        var playlists = [], player = null, ytReady = false, activePlaylist = null, activeCheckInterval = null;

        function ensureContainer() {
            var c = document.getElementById(CORE_PLAYER_ID);
            if (!c) {
                c = document.createElement('div');
                c.id = CORE_PLAYER_ID;
                c.style.position = 'fixed';
                c.style.bottom = '12px';
                c.style.right = '12px';
                c.style.zIndex = 9999;
                c.style.background = '#000';
                c.style.padding = '6px';
                c.style.borderRadius = '6px';
                c.style.boxShadow = '0 2px 8px rgba(0,0,0,0.35)';
                document.body.appendChild(c);
            }
            return c;
        }

        function createPlayer(initialVideo) {
            var container = ensureContainer();
            if (!ytReady) return;
            if (player) return;
            container.innerHTML = '<div id="core-youtube-player-frame"></div>';
            var opts = {
                height: '270',
                width: '480',
                playerVars: { autoplay: 0, rel: 0 },
                events: {
                    onReady: function () { },
                    onStateChange: onPlayerStateChange
                }
            };
            if (initialVideo) opts.videoId = initialVideo.videoId;
            player = new YT.Player('core-youtube-player-frame', opts);
        }

        function onPlayerStateChange(ev) {
            if (activePlaylist === null) return;
            clearActiveInterval();
            if (ev.data === YT.PlayerState.PLAYING) {
                var cur = playlists[activePlaylist];
                var curVideo = cur && cur.videos[cur.currentIndex];
                if (curVideo && curVideo.end != null) {
                    activeCheckInterval = setInterval(function () {
                        try {
                            var ct = player.getCurrentTime();
                            if (ct >= curVideo.end) { clearActiveInterval(); playNext(activePlaylist); }
                        } catch (e) { clearActiveInterval(); }
                    }, 500);
                }
            } else if (ev.data === YT.PlayerState.ENDED) {
                playNext(activePlaylist);
            }
        }

        function clearActiveInterval() { if (activeCheckInterval) { clearInterval(activeCheckInterval); activeCheckInterval = null; } }

        function registerPlaylist(placeholderEl, videoArray) {
            var pid = playlists.length;
            playlists.push({ placeholderEl: placeholderEl, videos: videoArray.slice(), currentIndex: 0 });
            placeholderEl.setAttribute('data-core-playlist-id', pid);
            return pid;
        }

        function cleanup() {
            for (var i = playlists.length - 1; i >= 0; i--) {
                if (!playlists[i].placeholderEl || !document.body.contains(playlists[i].placeholderEl)) {
                    if (i === activePlaylist) { activePlaylist = null; clearActiveInterval(); }
                    playlists.splice(i, 1);
                }
            }
            playlists.forEach(function (pl, idx) {
                if (pl.placeholderEl) pl.placeholderEl.setAttribute('data-core-playlist-id', idx);
            });
        }

        function gotoVideo(playlistId, idx) {
            playlistId = Number(playlistId); if (!playlists[playlistId]) return;
            var pl = playlists[playlistId]; if (idx < 0 || idx >= pl.videos.length) return;
            pl.currentIndex = idx; var v = pl.videos[idx]; activePlaylist = playlistId; clearActiveInterval();
            if (!ytReady) { loadYouTubeApi(); setTimeout(function () { if (!player && ytReady) createPlayer(v); try { player.loadVideoById({ videoId: v.videoId, startSeconds: v.start }); } catch (e) { } }, 300); return; }
            if (!player) { createPlayer(v); setTimeout(function () { try { player.loadVideoById({ videoId: v.videoId, startSeconds: v.start }); } catch (e) { } }, 200); return; }
            try { player.loadVideoById({ videoId: v.videoId, startSeconds: v.start }); } catch (e) { try { player.cueVideoById({ videoId: v.videoId, startSeconds: v.start }); } catch (e2) { } }
        }

        function playNext(playlistId) { playlistId = Number(playlistId); var pl = playlists[playlistId]; if (!pl) return; pl.currentIndex++; if (pl.currentIndex >= pl.videos.length) pl.currentIndex = 0; gotoVideo(playlistId, pl.currentIndex); }
        function playPrev(playlistId) { playlistId = Number(playlistId); var pl = playlists[playlistId]; if (!pl) return; pl.currentIndex--; if (pl.currentIndex < 0) pl.currentIndex = pl.videos.length - 1; gotoVideo(playlistId, pl.currentIndex); }
        function hasAnyPlaylist() { return playlists.length > 0; }

        return {
            setYTReady: function () { ytReady = true; },
            registerPlaylist: registerPlaylist,
            gotoVideo: gotoVideo,
            next: playNext,
            prev: playPrev,
            cleanup: cleanup,
            hasAnyPlaylist: hasAnyPlaylist
        };
    })();

    // ===== Load YouTube API =====
    function loadYouTubeApi() {
        if (document.getElementById(YT_SCRIPT_ID)) return;
        var s = document.createElement('script');
        s.id = YT_SCRIPT_ID;
        s.src = 'https://www.youtube.com/iframe_api';
        document.head.appendChild(s);
    }

    window.onYouTubeIframeAPIReady = function () {
        CorePlayer.setYTReady();
        // Auto-load first video from first available playlist
        var placeholders = document.querySelectorAll('.youtube-player-placeholder');
        for (var i = 0; i < placeholders.length; i++) {
            var videos = parseVideosFromPlaceholder(placeholders[i]);
            if (videos.length) {
                CorePlayer.registerPlaylist(placeholders[i], videos);
                CorePlayer.gotoVideo(i, 0);
                break;
            }
        }
    };

    // ===== Placeholder Parsing =====
    function parseVideosFromPlaceholder(placeholder) {
        var videoDataList = [], links = placeholder.querySelectorAll('a'), includeRefs = placeholder.getAttribute('data-include-refs') === 'true';
        function tryParseHref(href) { var info = parseYouTubeFromHref(href); if (info) videoDataList.push({ videoId: info.videoId, start: info.start || 0, end: (info.end != null ? info.end : null) }); }
        links.forEach(function (link) { tryParseHref(link.getAttribute('href') || link.getAttribute('url') || ''); });
        if (includeRefs) {
            var refSupTags = placeholder.querySelectorAll('sup a[href^="#"]');
            refSupTags.forEach(function (sup) {
                var refId = sup.getAttribute('href').substring(1);
                var refListItem = document.getElementById(refId);
                if (refListItem) {
                    var rlinks = refListItem.querySelectorAll('a');
                    rlinks.forEach(function (l) { tryParseHref(l.getAttribute('href') || ''); });
                }
            });
        }
        if (videoDataList.length === 0) {
            var dataAttr = placeholder.getAttribute('data-videos') || '';
            if (dataAttr) { dataAttr.split(',').forEach(function (v) { var parts = v.split('&'), vid = parts[0].trim(), start = 0, end = null; parts.forEach(function (p) { p = p.trim(); if (p.startsWith('t=')) start = parseTimeParam(p.substring(2)); else if (p.startsWith('start=')) start = parseTimeParam(p.substring(6)); else if (p.startsWith('end=')) end = parseTimeParam(p.substring(4)); }); if (vid) videoDataList.push({ videoId: vid, start: start, end: end }); }); }
        }
        return videoDataList;
    }

    function attachPlaceholders(root) {
        root = root || document;
        var placeholders = root.querySelectorAll('.youtube-player-placeholder');
        placeholders.forEach(function (ph) {
            if (ph.getAttribute('data-core-processed') === 'true') return;
            var videos = parseVideosFromPlaceholder(ph);
            if (!videos.length) return;
            var pid = CorePlayer.registerPlaylist(ph, videos);
            // clickable links
            var links = ph.querySelectorAll('a');
            links.forEach(function (link) {
                var info = parseYouTubeFromHref(link.getAttribute('href') || link.getAttribute('url') || '');
                if (!info) return;
                link.addEventListener('click', function (ev) {
                    ev.preventDefault(); ev.stopPropagation();
                    var idx = videos.findIndex(function (v) { return v.videoId === info.videoId && v.start === info.start && (v.end === info.end); });
                    if (idx === -1) idx = 0;
                    CorePlayer.gotoVideo(pid, idx);
                }, { passive: false });
            });
            // controls
            if (!ph.querySelector('.youtube-player-controls')) {
                var controls = document.createElement('div'); controls.className = 'youtube-player-controls';
                controls.innerHTML = '<button class="prev-button" data-core-playlist-id="' + pid + '">Previous</button><button class="next-button" data-core-playlist-id="' + pid + '">Next</button>';
                ph.appendChild(controls);
            }
            ph.querySelectorAll('.prev-button,.next-button').forEach(function (btn) {
                btn.addEventListener('click', function (ev) { ev.preventDefault(); var id = btn.getAttribute('data-core-playlist-id'); if (btn.classList.contains('prev-button')) CorePlayer.prev(id); else CorePlayer.next(id); });
            });
            ph.setAttribute('data-core-processed', 'true');
        });
    }

    document.addEventListener('DOMContentLoaded', function () { attachPlaceholders(document); loadYouTubeApi(); });

})();