MediaWiki:Gadget-floatingYouTubePlayer.js

From Angelina Jordan Wiki
Revision as of 20:17, 28 September 2025 by Most2dot0 (talk | contribs) (another rewrite by ChatGPT)

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';
  if (window.CoreYouTubePersistent) return;
  window.CoreYouTubePersistent = true;

  // --- Config ---
  var CORE_CONTAINER_ID = 'core-youtube-player-shell';
  var CORE_PLAYER_DIV_ID = 'core-yt-player';
  var CORE_PLAYLIST_DIV_ID = 'core-yt-playlist';
  var CHECK_INTERVAL_MS = 500;

  var corePlaylist = [];
  var currentIndex = -1;
  var ytPlayer = null;
  var ytApiReady = false;
  var endCheckInterval = null;

  function log() { if (window.console) console.log.apply(console, arguments); }

  // --- Extract YT info ---
  function parseTimeSpec(t) {
    if (!t) return 0;
    if (/^\d+$/.test(t)) return parseInt(t, 10);
    var m = /(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/.exec(t);
    if (m) return (parseInt(m[1] || 0, 10) * 3600) + (parseInt(m[2] || 0, 10) * 60) + (parseInt(m[3] || 0, 10));
    return 0;
  }
  function extractYouTubeFromUrl(href) {
    try { var url = new URL(href, location.href); } catch (e) { return null; }
    var host = url.hostname.toLowerCase(), id = null;
    if (host === 'youtu.be') id = url.pathname.slice(1).split('/')[0];
    if (host.indexOf('youtube.com') !== -1) {
      if (url.pathname === '/watch') id = url.searchParams.get('v');
      if (!id) {
        var m = url.pathname.match(/\/embed\/([^/]+)/) || url.pathname.match(/\/v\/([^/]+)/);
        if (m) id = m[1];
      }
    }
    if (!id) return null;
    return {
      videoId: id,
      start: parseTimeSpec(url.searchParams.get('t') || url.searchParams.get('start')),
      end: url.searchParams.has('end') ? parseTimeSpec(url.searchParams.get('end')) : null
    };
  }

  // --- Shell ---
  (function createShell() {
    if (document.getElementById(CORE_CONTAINER_ID)) return;
    var shell = document.createElement('div');
    shell.id = CORE_CONTAINER_ID;
    shell.innerHTML = `
      <div id="${CORE_PLAYER_DIV_ID}"></div>
      <div style="margin-top:6px;text-align:center">
        <button id="core-yt-prev">◀</button>
        <button id="core-yt-next">▶</button>
      </div>
      <div id="${CORE_PLAYLIST_DIV_ID}" style="display:none;margin-top:8px;max-height:220px;overflow:auto"></div>`;
    Object.assign(shell.style, {
      position: 'fixed', right: '12px', bottom: '12px', zIndex: 99999,
      background: '#fff', border: '1px solid #ccc', padding: '8px',
      borderRadius: '6px', boxShadow: '0 2px 6px rgba(0,0,0,.15)', maxWidth: '520px'
    });
    document.body.appendChild(shell);
    document.getElementById('core-yt-prev').onclick = () => playRelative(-1);
    document.getElementById('core-yt-next').onclick = () => playRelative(1);
  })();

  // --- Playlist UI ---
  function rebuildPlaylistUI() {
    var div = document.getElementById(CORE_PLAYLIST_DIV_ID);
    div.innerHTML = '';
    if (!corePlaylist.length) { div.style.display = 'none'; return; }
    div.style.display = 'block';
    corePlaylist.forEach((v, i) => {
      var item = document.createElement('div');
      item.textContent = (v.title || v.videoId) + (v.start ? ` (+${v.start}s)` : '');
      item.style.cursor = 'pointer';
      if (i === currentIndex) item.style.fontWeight = 'bold';
      item.onclick = () => playAt(i);
      div.appendChild(item);
    });
  }

  // --- Player lifecycle ---
  function ensureApi() {
    if (ytApiReady) return;
    var s = document.createElement('script');
    s.src = 'https://www.youtube.com/iframe_api'; s.id = 'youtube-iframe-api';
    document.head.appendChild(s);
    window.onYouTubeIframeAPIReady = () => { ytApiReady = true; createPlayerIfNeeded(); };
  }
  function createPlayerIfNeeded() {
    if (ytPlayer || !ytApiReady || !corePlaylist.length) return;
    var init = corePlaylist[currentIndex >= 0 ? currentIndex : 0];
    currentIndex = currentIndex >= 0 ? currentIndex : 0;
    ytPlayer = new YT.Player(CORE_PLAYER_DIV_ID, {
      height: '270', width: '480', videoId: init.videoId,
      playerVars: { start: init.start || 0 },
      events: { onStateChange: onState }
    });
  }
  function onState(e) {
    clearInterval(endCheckInterval);
    if (e.data === YT.PlayerState.PLAYING) {
      var v = corePlaylist[currentIndex];
      if (v && v.end) {
        endCheckInterval = setInterval(() => {
          if (ytPlayer.getCurrentTime() >= v.end) playRelative(1);
        }, CHECK_INTERVAL_MS);
      }
    } else if (e.data === YT.PlayerState.ENDED) playRelative(1);
  }
  function playAt(i) {
    if (i < 0 || i >= corePlaylist.length) return;
    currentIndex = i;
    var v = corePlaylist[i];
    if (!ytPlayer) { ensureApi(); return; }
    ytPlayer.loadVideoById({ videoId: v.videoId, startSeconds: v.start || 0 });
    rebuildPlaylistUI();
  }
  function playRelative(d) {
    if (!corePlaylist.length) return;
    var next = (currentIndex + d + corePlaylist.length) % corePlaylist.length;
    playAt(next);
  }

  // --- Playlist scanning ---
  function scanPlaceholders(container) {
    var vids = [];
    container.querySelectorAll('.youtube-player-placeholder a[href]').forEach(a => {
      var info = extractYouTubeFromUrl(a.href);
      if (info) vids.push({ ...info, title: a.textContent.trim() });
    });
    return vids;
  }
  function updatePlaylist(container) {
    var newVids = scanPlaceholders(container);
    var current = corePlaylist[currentIndex];
    corePlaylist = newVids;
    if (current) {
      var idx = corePlaylist.findIndex(v => v.videoId === current.videoId && v.start === current.start);
      if (idx !== -1) currentIndex = idx;
      else { corePlaylist.unshift(current); currentIndex = 0; }
    } else currentIndex = corePlaylist.length ? 0 : -1;
    rebuildPlaylistUI();
    if (corePlaylist.length) ensureApi();
  }

  // --- Navigation ---
  function injectContent(html, rawTitle) {
    var container = document.getElementById('mw-content-text');
    if (container) {
      container.innerHTML = html;
      updatePlaylist(container);
      if (window.mw && mw.hook) mw.hook('wikipage.content').fire(container);
    }
    var h = document.getElementById('firstHeading');
    if (h) h.innerHTML = rawTitle;
    document.title = rawTitle + ' - ' + (mw.config.get('wgSiteName') || '');
  }
  function loadArticle(href, push) {
    var title = decodeURIComponent(href.split('/wiki/')[1] || '');
    var api = mw.util.wikiScript('api') + `?action=parse&page=${encodeURIComponent(title)}&prop=text|displaytitle&format=json`;
    fetch(api).then(r => r.json()).then(d => {
      var html = d.parse.text['*'], rawTitle = d.parse.displaytitle || d.parse.title;
      injectContent(html, rawTitle);
      var state = { html, title: rawTitle };
      if (push) history.pushState(state, '', href);
      else history.replaceState(state, '', href);
    });
  }

  document.addEventListener('click', e => {
    var a = e.target.closest('a[href]');
    if (!a) return;
    if (a.origin !== location.origin || !a.pathname.startsWith('/wiki/')) return;
    e.preventDefault();
    loadArticle(a.href, true);
  });

  window.addEventListener('popstate', e => {
    if (e.state && e.state.html) injectContent(e.state.html, e.state.title);
    else loadArticle(location.href, false);
  });

  // --- Init ---
  (function init() {
    var cont = document.getElementById('mw-content-text');
    if (cont) updatePlaylist(cont);
    var rawTitle = document.getElementById('firstHeading')?.textContent.trim() || document.title;
    history.replaceState({ html: cont.innerHTML, title: rawTitle }, '', location.href);
  })();

})();