MediaWiki:Common.js: Difference between revisions
Appearance
No edit summary |
No edit summary |
||
| Line 238: | Line 238: | ||
mw.hook('wikipage.content').add(function ($content) { | mw.hook('wikipage.content').add(function ($content) { | ||
$content.find('.infobox-native-audio').each(function () { | $content.find('.infobox-native-audio').each(function () { | ||
const | if (this.dataset.audioInitialized === '1') { | ||
return; | |||
} | |||
const src = this.dataset.src; | |||
if ( | if (!src) { | ||
return; | return; | ||
} | } | ||
const audio = document.createElement('audio'); | const audio = document.createElement('audio'); | ||
audio.preload = 'metadata'; | |||
audio.src = src; | |||
const player = document.createElement('div'); | |||
player.className = 'custom-audio-player'; | |||
player.innerHTML = ` | |||
<button class="audio-play" type="button" aria-label="Play">▶</button> | |||
<div class="audio-progress-container"> | |||
<input class="audio-progress" type="range" min="0" max="100" value="0"> | |||
</div> | |||
<span class="audio-time">0:00 / 0:00</span> | |||
<button class="audio-volume" type="button" aria-label="Mute">🔊</button> | |||
`; | |||
const playButton = player.querySelector('.audio-play'); | |||
const progress = player.querySelector('.audio-progress'); | |||
const time = player.querySelector('.audio-time'); | |||
const volumeButton = player.querySelector('.audio-volume'); | |||
function formatTime(seconds) { | |||
if (!isFinite(seconds)) { | |||
return '0:00'; | |||
} | |||
const minutes = Math.floor(seconds / 60); | |||
const secs = Math.floor(seconds % 60) | |||
.toString() | |||
.padStart(2, '0'); | |||
return `${minutes}:${secs}`; | |||
} | |||
function updateTime() { | |||
progress.value = audio.duration | |||
? (audio.currentTime / audio.duration) * 100 | |||
: 0; | |||
time.textContent = | |||
`${formatTime(audio.currentTime)} / ${formatTime(audio.duration)}`; | |||
} | |||
playButton.addEventListener('click', function () { | |||
if (audio.paused) { | |||
audio.play(); | |||
} else { | |||
audio.pause(); | |||
} | |||
}); | |||
audio.addEventListener('play', function () { | |||
playButton.textContent = '❚❚'; | |||
playButton.setAttribute('aria-label', 'Pause'); | |||
}); | |||
audio.addEventListener('pause', function () { | |||
playButton.textContent = '▶'; | |||
playButton.setAttribute('aria-label', 'Play'); | |||
}); | |||
audio.addEventListener('timeupdate', updateTime); | |||
audio.addEventListener('loadedmetadata', updateTime); | |||
progress.addEventListener('input', function () { | |||
if (audio.duration) { | |||
audio.currentTime = | |||
(progress.value / 100) * audio.duration; | |||
} | |||
}); | |||
audio. | volumeButton.addEventListener('click', function () { | ||
audio.muted = !audio.muted; | |||
audio. | volumeButton.textContent = audio.muted ? '🔇' : '🔊'; | ||
}); | |||
audio.addEventListener('ended', function () { | |||
playButton.textContent = '▶'; | |||
progress.value = 0; | |||
}); | |||
this.replaceChildren(audio, player); | |||
this.dataset.audioInitialized = '1'; | |||
}); | }); | ||
}); | }); | ||
Revision as of 12:59, 4 October 2026
(function () {
var root = document.getElementById('pc-podcast');
if (!root) return;
var feed = root.dataset.feed, page = root.dataset.page;
var deck = root.querySelector('.pc-deck'), nav = root.querySelector('.pc-nav');
var cards = [], dots = [], active = 0;
function el(tag, cls, text) {
var e = document.createElement(tag);
if (cls) e.className = cls;
if (text) e.textContent = text;
return e;
}
function safeUrl(u) { return /^https?:\/\//i.test(u || '') ? u : ''; }
function txt(node, tag) { var n = node.getElementsByTagName(tag)[0]; return n ? n.textContent.trim() : ''; }
function plain(html) {
return new DOMParser().parseFromString(html || '', 'text/html').body.textContent.replace(/\s+/g, ' ').trim();
}
function parseDate(s) {
var d = new Date(s);
if (isNaN(d)) d = new Date(String(s).replace(' ', 'T') + 'Z');
return d;
}
function fmtDate(s) {
var d = parseDate(s);
return isNaN(d) ? '' : d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
}
function fmtDur(v) {
if (!v) return '';
if (v.indexOf(':') > -1) return v.replace(/^00:/, '').replace(/^0(\d:)/, '$1');
var s = parseInt(v, 10);
if (!s) return '';
var h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;
return (h ? h + ':' + ('0' + m).slice(-2) : m) + ':' + ('0' + sec).slice(-2);
}
function fromXml(xml) {
var doc = new DOMParser().parseFromString(xml, 'application/xml');
var chImg = doc.getElementsByTagName('itunes:image')[0];
var chCover = chImg ? chImg.getAttribute('href') : '';
return Array.prototype.map.call(doc.getElementsByTagName('item'), function (it) {
var img = it.getElementsByTagName('itunes:image')[0];
var s = txt(it, 'itunes:season'), e = txt(it, 'itunes:episode');
return {
title: txt(it, 'title'),
link: txt(it, 'link'),
date: txt(it, 'pubDate'),
dur: fmtDur(txt(it, 'itunes:duration')),
tag: e ? (s ? 'S' + s + ' · ' : '') + 'E' + e : '',
desc: plain(txt(it, 'description')),
cover: (img && img.getAttribute('href')) || chCover
};
});
}
function fromJson(j) {
var chCover = j.feed && j.feed.image;
return (j.items || []).map(function (it) {
return {
title: it.title,
link: it.link,
date: it.pubDate,
dur: '',
tag: '',
desc: plain(it.description),
cover: it.thumbnail || chCover
};
});
}
function buildCard(ep) {
var c = el('article', 'pc-card');
var cover = el('div', 'pc-cover');
if (safeUrl(ep.cover)) {
var img = el('img');
img.alt = '';
img.loading = 'lazy';
img.onerror = function () { cover.textContent = '🎙️'; };
img.src = ep.cover;
cover.appendChild(img);
} else {
cover.textContent = '🎙️';
}
var body = el('div', 'pc-body');
var meta = el('div', 'pc-meta');
if (ep.tag) meta.appendChild(el('span', 'pc-pill pc-pill-strong', ep.tag));
var ds = fmtDate(ep.date);
if (ds) meta.appendChild(el('span', 'pc-pill', ds));
if (ep.dur) meta.appendChild(el('span', 'pc-pill', ep.dur));
if (meta.children.length) body.appendChild(meta);
var href = safeUrl(ep.link) || safeUrl(page);
var title = el('div', 'pc-title');
title.setAttribute('role', 'heading');
title.setAttribute('aria-level', '3');
var a = el('a', '', ep.title || 'Untitled episode');
if (href) {
a.href = href;
a.target = '_blank';
a.rel = 'noopener';
}
title.appendChild(a);
body.appendChild(title);
body.appendChild(el('p', 'pc-desc', ep.desc));
c.appendChild(cover);
c.appendChild(body);
if (href) {
var o = el('div', 'mc-open');
var oa = el('a');
oa.href = href;
oa.target = '_blank';
oa.rel = 'noopener';
oa.title = 'Open episode';
oa.setAttribute('aria-label', 'Open episode');
oa.appendChild(el('span', 'mc-open-icon'));
o.appendChild(oa);
c.appendChild(o);
}
return c;
}
function layout() {
var n = cards.length;
if (!n) return;
cards.forEach(function (c, i) {
var pos = (i - active + n) % n;
c.dataset.pos = pos;
c.inert = pos !== 0;
c.setAttribute('aria-hidden', pos ? 'true' : 'false');
});
dots.forEach(function (d, i) {
d.setAttribute('aria-current', i === active ? 'true' : 'false');
});
}
function go(delta) {
active = (active + delta + cards.length) % cards.length;
layout();
}
function arrow(label, path, delta) {
var b = el('button', 'pc-btn');
b.type = 'button';
b.setAttribute('aria-label', label);
b.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="' + path + '"/></svg>';
b.addEventListener('click', function () { go(delta); });
return b;
}
function render(list) {
if (!list || !list.length) return;
list = list
.filter(function (e) { return e.title; })
.sort(function (a, b) { return parseDate(b.date) - parseDate(a.date); })
.slice(0, 3);
if (!list.length) return;
deck.innerHTML = '';
nav.innerHTML = '';
cards = [];
dots = [];
active = 0;
list.forEach(function (ep) {
var card = buildCard(ep);
cards.push(card);
deck.appendChild(card);
});
for (var k = cards.length; k < 3; k++) {
var g = el('div', 'pc-card pc-ghost');
g.dataset.pos = k;
g.setAttribute('aria-hidden', 'true');
deck.appendChild(g);
}
root.classList.add('is-loaded');
if (cards.length > 1) {
var wrap = el('div', 'pc-dots');
cards.forEach(function (_, i) {
var d = el('button', 'pc-dot');
d.type = 'button';
d.setAttribute('aria-label', 'Episode ' + (i + 1));
d.addEventListener('click', function () {
active = i;
layout();
});
dots.push(d);
wrap.appendChild(d);
});
nav.appendChild(arrow('Previous episode', 'm15 18-6-6 6-6', -1));
nav.appendChild(wrap);
nav.appendChild(arrow('Next episode', 'm9 18 6-6-6-6', 1));
var sx = null;
deck.addEventListener('pointerdown', function (e) { sx = e.clientX; });
deck.addEventListener('pointerup', function (e) {
if (sx === null) return;
var dx = e.clientX - sx;
sx = null;
if (Math.abs(dx) > 45) go(dx < 0 ? 1 : -1);
});
}
layout();
}
fetch(feed)
.then(function (r) { if (!r.ok) throw new Error('feed'); return r.text(); })
.then(fromXml)
.catch(function () {
return fetch('https://api.rss2json.com/v1/api.json?rss_url=' + encodeURIComponent(feed))
.then(function (r) { return r.json(); })
.then(fromJson);
})
.then(render)
.catch(function () { /* leave empty */ });
})();
mw.hook('wikipage.content').add(function ($content) {
$content.find('.infobox-native-audio').each(function () {
if (this.dataset.audioInitialized === '1') {
return;
}
const src = this.dataset.src;
if (!src) {
return;
}
const audio = document.createElement('audio');
audio.preload = 'metadata';
audio.src = src;
const player = document.createElement('div');
player.className = 'custom-audio-player';
player.innerHTML = `
<button class="audio-play" type="button" aria-label="Play">▶</button>
<div class="audio-progress-container">
<input class="audio-progress" type="range" min="0" max="100" value="0">
</div>
<span class="audio-time">0:00 / 0:00</span>
<button class="audio-volume" type="button" aria-label="Mute">🔊</button>
`;
const playButton = player.querySelector('.audio-play');
const progress = player.querySelector('.audio-progress');
const time = player.querySelector('.audio-time');
const volumeButton = player.querySelector('.audio-volume');
function formatTime(seconds) {
if (!isFinite(seconds)) {
return '0:00';
}
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60)
.toString()
.padStart(2, '0');
return `${minutes}:${secs}`;
}
function updateTime() {
progress.value = audio.duration
? (audio.currentTime / audio.duration) * 100
: 0;
time.textContent =
`${formatTime(audio.currentTime)} / ${formatTime(audio.duration)}`;
}
playButton.addEventListener('click', function () {
if (audio.paused) {
audio.play();
} else {
audio.pause();
}
});
audio.addEventListener('play', function () {
playButton.textContent = '❚❚';
playButton.setAttribute('aria-label', 'Pause');
});
audio.addEventListener('pause', function () {
playButton.textContent = '▶';
playButton.setAttribute('aria-label', 'Play');
});
audio.addEventListener('timeupdate', updateTime);
audio.addEventListener('loadedmetadata', updateTime);
progress.addEventListener('input', function () {
if (audio.duration) {
audio.currentTime =
(progress.value / 100) * audio.duration;
}
});
volumeButton.addEventListener('click', function () {
audio.muted = !audio.muted;
volumeButton.textContent = audio.muted ? '🔇' : '🔊';
});
audio.addEventListener('ended', function () {
playButton.textContent = '▶';
progress.value = 0;
});
this.replaceChildren(audio, player);
this.dataset.audioInitialized = '1';
});
});