MediaWiki:Gadget-floatingYouTubePlayer.js
From Angelina Jordan Wiki
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);
})();
})();