MediaWiki:Common.js
MediaWiki interface page
More actions
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.
/* 这里的任何JavaScript将为所有用户在每次页面加载时加载。 */
/* JavaScript used for https://zh.wikipedia.org/wiki/MediaWiki:Common.js : */
/** metaBox
*
* Funcionament de la Plantilla:Metacaixa
* Implementat per: Usuari:Peleguer.
* Actualitzat per Joanjoc seguint les indicacions d'en Martorell
*/
document.querySelector('.mw-logo-wordmark').textContent=''
function MetaCaixaInit() {
// S'executa al carregar-se la pàgina, si hi ha metacaixes,
// s'assignen els esdeveniments als botons
//alert("MetaCaixaInit");
var i = 0; // Inicialitzem comptador de caixes
for (i = 0; i <= 9; i++) {
var vMc = document.getElementById("mc" + i);
if (!vMc) break;
//alert("MetaCaixaInit, trobada Metacaixa mc"+i);
var j = 1; // Inicialitzem comptador de botons dins de la caixa
var vPsIni = 0; // Pestanya visible inicial
for (j = 1; j <= 9; j++) {
var vBt = document.getElementById("mc" + i + "bt" + j);
if (!vBt) break;
//alert("MetaCaixaInit, trobat botó mc"+i+"bt"+j);
vBt.onclick = MetaCaixaMostraPestanya; // A cada botó assignem l'esdeveniment onclick
//alert (vBt.className);
if (vBt.className == "mcBotoSel") vPsIni = j; // Si tenim un botó seleccionat, en guardem l'index
}
//alert ("mc="+i+", ps="+j+", psini="+vPsIni );
if (vPsIni === 0) { // Si no tenim cap botó seleccionat, n'agafem un aleatòriament
vPsIni = 1 + Math.floor((j - 1) * Math.random());
//alert ("Activant Pestanya a l'atzar; _mc"+i+"bt"+vPsIni +"_");
document.getElementById("mc" + i + "ps" + vPsIni).style.display = "block";
document.getElementById("mc" + i + "ps" + vPsIni).style.visibility = "visible";
document.getElementById("mc" + i + "bt" + vPsIni).className = "mcBotoSel";
}
}
}
function MetaCaixaMostraPestanya() {
// S'executa al clicar una pestanya,
// aquella es fa visible i les altres s'oculten
var vMcNom = this.id.substr(0, 3); // A partir del nom del botó, deduïm el nom de la caixa
var vIndex = this.id.substr(5, 1); // I l'index
var i = 1;
for (i = 1; i <= 9; i++) { // busquem totes les pestanyes d'aquella caixa
//alert(vMcNom+"ps"+i);
var vPsElem = document.getElementById(vMcNom + "ps" + i);
if (!vPsElem) break;
if (vIndex == i) { // Si és la pestanya bona la mostrem i canviem la classe de botó
vPsElem.style.display = "block";
vPsElem.style.visibility = "visible";
document.getElementById(vMcNom + "bt" + i).className = "mcBotoSel";
} else { // Sinó, l'ocultem i canviem la classe de botó
vPsElem.style.display = "none";
vPsElem.style.visibility = "hidden";
document.getElementById(vMcNom + "bt" + i).className = "mcBoto";
}
}
return false; // evitem la recàrrega de la pàgina
}
$(MetaCaixaInit);
//BMV模板script
(function(mw, $) {
'use strict';
function initBmvPlayer(wrapper) {
var $wrapper = $(wrapper);
var songName = $wrapper.data('song-name');
var rawDifficulties = $wrapper.data('difficulties');
var $difficultyDiv = $wrapper.find('.difficulty-div');
var $ratioDiv = $wrapper.find('.ratio-div');
var $outputSpan = $wrapper.find('.output-span');
var difficultyColors = {
"EZ": "#57E4C4", "HD": "#FDBA61", "IN": "#FE8661", "AT": "#4C364B"
};
function parseDifficulties(input) {
if (!input) return null;
try {
return input.split(',').map(function(item) {
var match = item.trim().match(/^([A-Za-z]+)([\d]+[+-]?)$/);
if (!match) {
throw new Error('Invalid difficulty format for item: ' + item);
}
return (match[1].toUpperCase() + ' ' + match[2]);
});
} catch (e) {
console.error("Error parsing difficulties:", input, e);
return null;
}
}
var difficultyStates = parseDifficulties(rawDifficulties);
if (!songName || !difficultyStates || difficultyStates.length === 0) {
$wrapper.find('table').html('<div style="color:red; padding:10px; text-align:center;">错误: 缺少或无效的曲名/难度参数。</div>');
return;
}
$difficultyDiv.attr('data-states', JSON.stringify(difficultyStates));
function updateOutput() {
var difficulty = $difficultyDiv.text().split(' ')[0];
var ratio = $ratioDiv.text().replace(':', '-');
var processedSongName = songName.replace(/[\\/:\*\?"<>\|]/g, '-');
var encodedSongName = encodeURIComponent(processedSongName);
var videoUrl = 'https://pan.rizwiki.cn/d/' + ratio + '_' + encodedSongName + '_' + difficulty + '.mp4';
var isMobile = window.matchMedia("(max-width: 600px)").matches;
var videoHeightStyle = isMobile ? '' : 'height:100%;';
$outputSpan.html('<video class="html5media-video" src="' + videoUrl + '" controls preload="metadata" loading="lazy" style="' + videoHeightStyle + ' object-fit:contain;"></video>');
}
function cycleDifficulty() {
var states = JSON.parse($difficultyDiv.attr('data-states'));
var current = parseInt($difficultyDiv.attr('data-current'), 10);
current = (current + 1) % states.length;
var nextState = states[current];
$difficultyDiv.text(nextState);
$difficultyDiv.attr('data-current', current.toString());
var difficultyType = nextState.split(' ')[0];
$difficultyDiv.parent().css('background-color', difficultyColors[difficultyType] || '#FFFFFF');
updateOutput();
}
function cycleRatio() {
var states = JSON.parse($ratioDiv.attr('data-states'));
var current = parseInt($ratioDiv.attr('data-current'), 10);
current = (current + 1) % states.length;
$ratioDiv.text(states[current]);
$ratioDiv.attr('data-current', current.toString());
updateOutput();
}
$difficultyDiv.on('click', cycleDifficulty);
$ratioDiv.parent().on('click', cycleRatio);
// 初始化
(function initialize() {
var firstDiff = difficultyStates[0];
var diffType = firstDiff.split(' ')[0];
$difficultyDiv.text(firstDiff);
$difficultyDiv.parent().css('background-color', difficultyColors[diffType]);
updateOutput();
})();
}
mw.hook('wikipage.content').add(function($content) {
// 初始化 BMV 播放器
$content.find('.bmv-player-uninitialized').each(function() {
initBmvPlayer(this);
$(this).removeClass('bmv-player-uninitialized');
});
/* 绿豆诡计播放器js */
var $player = $('#riz-player');
if ($player.length > 0) {
if ($player.parent().is('body') === false) {
$player.appendTo('body');
}
if ($player.data('initialized')) return;
$player.data('initialized', true);
var lastScrollTop = 0;
var scrollThreshold = 10;
$(window).on('scroll resize', function() {
if (window.innerWidth > 768) {
$player.removeClass('rp-scroll-down');
return;
}
var st = $(this).scrollTop();
if (Math.abs(lastScrollTop - st) <= scrollThreshold) return;
if (st > lastScrollTop && st > 50) {
$player.addClass('rp-scroll-down');
} else {
$player.removeClass('rp-scroll-down');
}
lastScrollTop = st;
});
var songUrl = $player.data('url');
if (!songUrl) {
console.error("RizPlayer Error: No audio URL found.");
return;
}
// 封面图修复逻辑
var $coverImg = $player.find('.rp-cover-img');
if ($coverImg.length === 0) $coverImg = $player.find('.rp-cover img');
var backupSrc = $player.data('image');
function fixCoverImage() {
var currentSrc = $coverImg.attr('src');
if ((!currentSrc || currentSrc === "" || currentSrc === "undefined") && backupSrc) {
$coverImg.attr('src', backupSrc);
}
}
fixCoverImage();
var audio = new Audio(songUrl);
audio.preload = "none";
// Media Session API 集成
function updateMediaSession() {
if ('mediaSession' in navigator) {
try {
navigator.mediaSession.metadata = new MediaMetadata({
title: $player.find('.rp-title').text() || '未知曲目',
artist: $player.find('.rp-artist').text() || '未知艺术家',
album: 'RizWiki',
artwork: [{
src: backupSrc || '',
sizes: '512x512',
type: 'image/jpeg'
}]
});
} catch(e) {
console.log('Media Session metadata error:', e);
}
}
}
var $toggleBtn = $player.find('.rp-btn-toggle');
var $sideBtn = $player.find('.rp-side-ctrl');
var $progressWrap = $player.find('.rp-progress-wrap');
var $progressBar = $player.find('.rp-progress-bar');
var $progressCurrent = $player.find('.rp-progress-current');
var $progressHandle = $player.find('.rp-progress-handle');
var $curTime = $player.find('.rp-cur');
var $durTime = $player.find('.rp-dur');
$player.css('display', 'flex');
function formatTime(seconds) {
if (isNaN(seconds)) return "00:00";
var m = Math.floor(seconds / 60);
var s = Math.floor(seconds % 60);
return (m < 10 ? "0" + m : m) + ":" + (s < 10 ? "0" + s : s);
}
function togglePlay(e) {
if(e) e.stopPropagation();
if (audio.paused) {
var playPromise = audio.play();
if (playPromise !== undefined) {
playPromise.then(function() {
$player.addClass('playing');
if (!$player.hasClass('riz-player-expanded')) {
$player.addClass('riz-player-expanded');
}
}).catch(function(error) {
console.error("播放失败:", error);
});
}
} else {
audio.pause();
$player.removeClass('playing');
}
}
if ('mediaSession' in navigator) {
try {
navigator.mediaSession.setActionHandler('play', function() {
audio.play();
$player.addClass('playing');
});
navigator.mediaSession.setActionHandler('pause', function() {
audio.pause();
$player.removeClass('playing');
});
navigator.mediaSession.setActionHandler('seekbackward', function() {
audio.currentTime = Math.max(audio.currentTime - 10, 0);
});
navigator.mediaSession.setActionHandler('seekforward', function() {
audio.currentTime = Math.min(audio.currentTime + 10, audio.duration || 0);
});
navigator.mediaSession.setActionHandler('seekto', function(details) {
if (details.seekTime !== null) {
audio.currentTime = details.seekTime;
}
});
} catch(e) {
console.log('Media Session handlers error:', e);
}
}
updateMediaSession();
// 绑定点击事件
$toggleBtn.on('click', togglePlay);
$sideBtn.on('click', togglePlay);
// 展开/折叠
$player.on('click', function(e) {
if ($(e.target).closest('.rp-progress-wrap, .rp-side-ctrl, .rp-btn-toggle').length > 0) return;
$player.toggleClass('riz-player-expanded');
});
var isDragging = false;
$progressWrap.on('mousedown touchstart', function(e) {
isDragging = true;
$progressWrap.addClass('dragging');
updateProgressFromEvent(e);
});
$(document).on('mousemove touchmove', function(e) {
if (isDragging) {
e.preventDefault();
updateProgressFromEvent(e);
}
});
$(document).on('mouseup touchend', function(e) {
if (isDragging) {
isDragging = false;
$progressWrap.removeClass('dragging');
if (audio.duration) {
var percent = parseFloat($progressCurrent[0].style.width) / 100;
audio.currentTime = percent * audio.duration;
if(audio.paused) {
audio.play();
$player.addClass('playing');
}
}
}
});
function updateProgressFromEvent(e) {
var clientX = e.clientX;
if (e.type.includes('touch')) {
clientX = e.originalEvent.touches[0].clientX;
}
var offset = $progressWrap.offset();
var width = $progressWrap.width();
var relX = clientX - offset.left;
var percent = (relX / width) * 100;
if (percent < 0) percent = 0;
if (percent > 100) percent = 100;
$progressCurrent.css('width', percent + '%');
$progressHandle.css('left', percent + '%');
if (audio.duration) {
var dragTime = (percent / 100) * audio.duration;
$curTime.text(formatTime(dragTime));
}
}
audio.addEventListener('timeupdate', function() {
if (!isDragging && audio.duration) {
var percent = (audio.currentTime / audio.duration) * 100;
$progressCurrent.css('width', percent + '%');
$progressHandle.css('left', percent + '%');
$curTime.text(formatTime(audio.currentTime));
if ('mediaSession' in navigator && 'setPositionState' in navigator.mediaSession) {
try {
navigator.mediaSession.setPositionState({
duration: audio.duration,
playbackRate: audio.playbackRate,
position: audio.currentTime
});
} catch(e) {
}
}
}
});
audio.addEventListener('loadedmetadata', function() {
$durTime.text(formatTime(audio.duration));
updateMediaSession();
if ('mediaSession' in navigator && 'setPositionState' in navigator.mediaSession) {
try {
navigator.mediaSession.setPositionState({
duration: audio.duration,
playbackRate: audio.playbackRate,
position: 0
});
} catch(e) {
}
}
});
audio.addEventListener('ended', function() {
$player.removeClass('playing');
$progressCurrent.css('width', '0%');
$progressHandle.css('left', '0%');
if ('mediaSession' in navigator && 'setPositionState' in navigator.mediaSession) {
try {
navigator.mediaSession.setPositionState({
duration: audio.duration,
playbackRate: audio.playbackRate,
position: 0
});
} catch(e) {
}
}
});
audio.addEventListener('error', function(e) {
console.error("音频加载错误:", audio.src);
});
}
});
})(mediaWiki, jQuery);
/* PWA */
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js').then(function(registration) {
}, function(err) {
});
});
}
// 点击短链接复制
mw.hook('wikipage.content').add(function() {
var shortlink = document.querySelector('.title-shortlink')
if (shortlink) {
shortlink.removeAttribute('href')
shortlink.style.cursor = 'pointer'
shortlink.title = '点击复制'
shortlink.addEventListener('click', function(e) {
e.preventDefault()
navigator.clipboard.writeText(this.textContent).then(function() {
var parent = shortlink.parentNode
parent.removeChild(shortlink)
parent.insertBefore(shortlink, shortlink.nextSibling)
shortlink.title = '已复制'
setTimeout(function() {
shortlink.title = '点击复制'
}, 1000)
}).catch(function(err) {
console.error('复制失败: ', err)
shortlink.title = '复制失败'
var parent = shortlink.parentNode
parent.removeChild(shortlink)
parent.insertBefore(shortlink, shortlink.nextSibling)
})
})
}
})
//随机推荐
$(function() {
var $source = $('#hidden-recommendation-source');
var $target = $('.mw-parser-output');
if ($source.length > 0 && $target.length > 0) {
var $cards = $source.find('.citizen-related-card');
if ($cards.length > 3) {
var cardsArray = $cards.toArray();
cardsArray.sort(function() { return 0.5 - Math.random() });
var $selectedCards = $(cardsArray.slice(0, 3));
$source.find('.citizen-related-grid').empty().append($selectedCards);
}
var $container = $('<div id="custom-read-more-section"></div>');
$container.append($source.contents());
$source.remove();
$target.append($container);
}
});
/* Tag处理 */
$(function() {
var $dataSpans = $('.riz-page-tag-data');
if ($dataSpans.length === 0) return;
var allTags = new Set();
$dataSpans.each(function() {
var tags = ($(this).attr('data-tags') || '').split(',');
tags.forEach(function(t) {
t = t.trim();
if (t) allTags.add(t);
});
});
if (allTags.size === 0) return;
var $ul = $('<ul>');
Array.from(allTags).forEach(function(tag) {
var url = mw.util.getUrl('Special:Search', { search: '', fulltext: '1' }) + '&tags%5B%5D=' + encodeURIComponent(tag);
var $a = $('<a>')
.attr('href', url)
.attr('title', '搜索标签:' + tag)
.text(tag);
$('<li>').append($a).appendTo($ul);
});
var $container = $('<div>')
.attr('id', 'page-tags')
.addClass('catlinks citizen-page-tags mw-collapsible mw-collapsed');
var $header = $('<div>')
.addClass('mw-normal-catlinks')
.css({ 'border': 'none', 'background': 'transparent', 'padding': '0' });
$header.append('<b>页面标签</b>');
var $content = $('<div>')
.addClass('mw-collapsible-content')
.css({ 'margin-top': '5px', 'padding-top': '5px', 'border-top': '1px solid var(--border-color-subtle, #eaecf0)' })
.append($ul);
$container.append($header).append($content);
var $catlinks = $('#catlinks');
if ($catlinks.length) {
$catlinks.before($container);
} else {
$('#mw-content-text').append($container);
}
mw.loader.using(['jquery.makeCollapsible'], function() {
$container.makeCollapsible();
});
});
/* songinfo PE/PC 切换 */
$(function () {
$(document).on('click', '.platform-tab', function () {
var $box = $(this).closest('.song-infobox');
$box.toggleClass('show-pc', $(this).data('platform') === 'pc');
$(this).siblings('.platform-tab').removeClass('active');
$(this).addClass('active');
});
$('table').has('math').addClass('has-math');
$('.navbox').not(':has(.mw-collapsed)').addClass('navbox-not-collapsed');
});
// Logo SVG 动画控制
(function() {
'use strict';
$(document).ready(function() {
var $logo = $('.mw-logo-wordmark');
if ($logo.length === 0) return;
// 点击logo时重新播放动画
$logo.on('click', function(e) {
e.preventDefault();
var timestamp = new Date().getTime();
var currentBg = $logo.css('background-image');
var svgUrl = '/extensions/Rizwiki_logo_animated.svg?t=' + timestamp;
$logo.css('background-image', 'url("' + svgUrl + '")');
});
});
})();
// wikitable 表头固定
function initStickyHeader($content) {
const container = $content instanceof jQuery ? $content[0] : $content;
function processTable(table, thead) {
if (!thead) {
thead = table.querySelector('thead');
}
if (!thead) {
return;
}
const headerRow = thead.querySelector('tr');
if (!headerRow || headerRow.querySelectorAll('th').length === 0) {
return;
}
let scrollParent = table.parentElement;
while (scrollParent && scrollParent !== document.body) {
const style = getComputedStyle(scrollParent);
if (style.overflowX === 'auto' || style.overflowX === 'scroll') {
break;
}
scrollParent = scrollParent.parentElement;
}
const stickyHeader = document.createElement('div');
stickyHeader.style.position = 'fixed';
stickyHeader.style.display = 'none';
stickyHeader.style.zIndex = '1000';
stickyHeader.style.overflow = 'hidden';
const stickyTable = document.createElement('table');
stickyTable.className = table.className;
stickyTable.style.margin = '0';
const stickyThead = document.createElement('thead');
stickyThead.innerHTML = headerRow.outerHTML;
stickyTable.appendChild(stickyThead);
stickyHeader.appendChild(stickyTable);
document.body.appendChild(stickyHeader);
stickyThead.addEventListener('click', function(e) {
const target = e.target;
if (target.tagName === 'TH') {
const index = Array.from(target.parentElement.children).indexOf(target);
const originalTh = headerRow.children[index];
if (originalTh) {
originalTh.click();
}
}
});
function updateStickyHeader() {
const stickyNav = document.querySelector('.citizen-sticky-header');
const headerHeight = stickyNav ? stickyNav.offsetHeight : 0;
const tableRect = table.getBoundingClientRect();
const originalCells = headerRow.querySelectorAll('th');
const stickyCells = stickyThead.querySelectorAll('th');
stickyHeader.style.top = headerHeight + 'px';
stickyTable.style.width = table.offsetWidth + 'px';
originalCells.forEach((cell, i) => {
if (stickyCells[i]) {
stickyCells[i].style.width = cell.offsetWidth + 'px';
stickyCells[i].className = cell.className;
if (cell.hasAttribute('aria-sort')) {
stickyCells[i].setAttribute('aria-sort', cell.getAttribute('aria-sort'));
} else {
stickyCells[i].removeAttribute('aria-sort');
}
}
});
if (scrollParent && scrollParent !== document.body) {
const scrollLeft = scrollParent.scrollLeft;
const parentRect = scrollParent.getBoundingClientRect();
stickyHeader.style.left = parentRect.left + 'px';
stickyHeader.style.width = parentRect.width + 'px';
stickyTable.style.transform = `translateX(${-scrollLeft}px)`;
} else {
stickyTable.style.transform = 'translateX(0)';
}
if (tableRect.top < headerHeight && tableRect.bottom > headerHeight) {
stickyHeader.style.display = 'block';
} else {
stickyHeader.style.display = 'none';
}
}
window.addEventListener('scroll', updateStickyHeader, { passive: true });
if (scrollParent && scrollParent !== document.body) {
scrollParent.addEventListener('scroll', updateStickyHeader, { passive: true });
}
window.addEventListener('resize', updateStickyHeader, { passive: true });
updateStickyHeader();
}
const tables = container.querySelectorAll('table.wikitable');
tables.forEach(table => {
let attempts = 0;
const checkAndProcess = () => {
const thead = table.querySelector('thead');
if (thead) {
processTable(table, thead);
} else if (attempts < 40) {
attempts++;
setTimeout(checkAndProcess, 50);
}
};
checkAndProcess();
});
}
mw.hook('wikipage.content').add(initStickyHeader);
//外链拦截
(function () {
var CONFIRM_BASE = 'https://riz.wiki/';
function isWhitelistedHost(hostname) {
var host = (hostname || '').toLowerCase();
if (!host) return false;
if (/^([a-z0-9-]+\.)*rizwiki\.[a-z0-9-]+$/.test(host)) return true;
if (host === 'riz.wiki' || host.endsWith('.riz.wiki')) return true;
if (host === 'guguwo.top' || host.endsWith('.guguwo.top')) return true;
if (host === 'miukar.com' || host.endsWith('.miukar.com')) return true;
return false;
}
function isExternal(a) {
try {
var u = new URL(a.href, location.href);
return u.origin !== location.origin;
} catch (_) {
return false;
}
}
function encodeB64UrlSafe(str) {
var bytes = new TextEncoder().encode(str);
var bin = '';
for (var i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
}
document.addEventListener('click', function (e) {
var a = e.target.closest && e.target.closest('a[href]');
if (!a) return;
if (e.defaultPrevented) return;
if (e.button !== 0) return;
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
if (a.target === '_blank' || a.hasAttribute('download')) return;
if (!isExternal(a)) return;
try {
var u = new URL(a.href, location.href);
if (!/^https?:$/.test(u.protocol)) return;
if (isWhitelistedHost(u.hostname)) return;
e.preventDefault();
var toB64 = encodeB64UrlSafe(u.toString());
var next = CONFIRM_BASE + '?confirm=1&to_b64=' + encodeURIComponent(toB64);
location.href = next;
} catch (_) {}
}, true);
})();
/* 泡泡背景 */
(function() {
'use strict';
var bubbleEnabled = localStorage.getItem('citizen-bubble-background') === 'true';
var themeClass = 'citizen-bubble-theme-enabled';
var backgroundSelectors = [
'.citizen-page-container',
'.citizen-body-container',
'.mw-body',
'#content',
'#mw-content-text',
'.mw-page-container',
'.mw-body-content',
'.citizen-section',
'main',
'.mw-header',
'.citizen-header',
'html',
'.skin-citizen'
];
function ensureBubbleThemeStyle() {
if (document.getElementById('citizen-bubble-theme-style')) return;
var style = document.createElement('style');
style.id = 'citizen-bubble-theme-style';
style.textContent = [
'html.' + themeClass + ', html.' + themeClass + ' body { background: transparent !important; background-color: transparent !important; background-image: none !important; }',
'html.' + themeClass + ' .citizen-page-container, html.' + themeClass + ' .citizen-body-container, html.' + themeClass + ' .citizen-body, html.' + themeClass + ' .citizen-body-header, html.' + themeClass + ' .citizen-body-content, html.' + themeClass + ' .citizen-page-header, html.' + themeClass + ' .citizen-page-sidebar, html.' + themeClass + ' .citizen-footer, html.' + themeClass + ' .mw-body, html.' + themeClass + ' #content, html.' + themeClass + ' #mw-content-text, html.' + themeClass + ' .mw-page-container, html.' + themeClass + ' .mw-body-content, html.' + themeClass + ' .mw-parser-output, html.' + themeClass + ' .citizen-section, html.' + themeClass + ' main { background: transparent !important; background-color: transparent !important; background-image: none !important; }',
'html.' + themeClass + ' .citizen-section::before, html.' + themeClass + ' .citizen-section::after { background: transparent !important; background-color: transparent !important; background-image: none !important; }'
].join('\n');
document.head.appendChild(style);
}
function addToggleSwitch() {
var appearanceSection = document.querySelector('.citizen-preferences-section__content');
if (!appearanceSection) {
setTimeout(addToggleSwitch, 100);
return;
}
if (document.getElementById('skin-client-prefs-citizen-bubble-background')) {
return;
}
var toggleHTML = '<span class="cdx-toggle-switch cdx-toggle-switch--align-switch citizen-preferences-group">' +
'<input id="skin-client-prefs-citizen-bubble-background" class="cdx-toggle-switch__input" type="checkbox" role="switch" aria-describedby="bubble-bg-desc" ' + (bubbleEnabled ? 'checked' : '') + '>' +
'<span class="cdx-toggle-switch__switch"><span class="cdx-toggle-switch__switch__grip"></span></span>' +
'<div class="cdx-label cdx-toggle-switch__label">' +
'<label class="cdx-label__label" for="skin-client-prefs-citizen-bubble-background">' +
'<span class="cdx-label__label__text">浮动泡泡背景</span>' +
'</label>' +
'<span id="bubble-bg-desc" class="cdx-label__description">显示动态泡泡背景效果</span>' +
'</div></span>';
appearanceSection.insertAdjacentHTML('beforeend', toggleHTML);
var toggle = document.getElementById('skin-client-prefs-citizen-bubble-background');
if (toggle) {
toggle.addEventListener('change', function() {
bubbleEnabled = this.checked;
localStorage.setItem('citizen-bubble-background', bubbleEnabled);
toggleBubbleCanvas(bubbleEnabled);
});
}
}
function eachElement(selectors, callback) {
selectors.forEach(function(selector) {
var elements = document.querySelectorAll(selector);
elements.forEach(callback);
});
}
function toggleBubbleCanvas(enabled) {
var canvas = document.getElementById('bubble-canvas');
if (canvas) {
canvas.style.display = enabled ? 'block' : 'none';
}
if (enabled) {
ensureBubbleThemeStyle();
document.documentElement.classList.add(themeClass);
document.body.style.background = window.matchMedia('(prefers-color-scheme: light)').matches ? '#F8F9FC' : '#1A1B2E';
document.body.style.overflowX = 'hidden';
eachElement(backgroundSelectors, function(el) {
el.style.background = 'transparent';
el.style.backgroundColor = 'transparent';
});
if (window.bubbleAnimation) {
window.bubbleAnimation.start();
}
} else {
document.documentElement.classList.remove(themeClass);
document.body.style.background = '';
document.body.style.overflowX = '';
eachElement(backgroundSelectors, function(el) {
el.style.background = '';
el.style.backgroundColor = '';
el.style.backgroundImage = '';
});
if (window.bubbleAnimation) {
window.bubbleAnimation.stop();
}
}
}
function init() {
ensureBubbleThemeStyle();
addToggleSwitch();
var canvas = document.createElement('canvas');
canvas.id = 'bubble-canvas';
document.body.appendChild(canvas);
function setCanvasStyle() {
canvas.style.position = 'fixed';
canvas.style.top = '0';
canvas.style.left = '0';
canvas.style.width = '100vw';
canvas.style.height = '160vh';
canvas.style.pointerEvents = 'none';
canvas.style.zIndex = '-9999';
canvas.style.filter = 'saturate(1.35) contrast(1.1)';
canvas.style.margin = '0';
canvas.style.padding = '0';
canvas.style.display = bubbleEnabled ? 'block' : 'none';
}
setCanvasStyle();
setTimeout(setCanvasStyle, 100);
setTimeout(setCanvasStyle, 500);
if (bubbleEnabled) {
document.documentElement.classList.add(themeClass);
document.body.style.background = '#1A1B2E';
document.body.style.overflowX = 'hidden';
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) {
document.body.style.background = '#F8F9FC';
}
eachElement(backgroundSelectors, function(el) {
el.style.background = 'transparent';
el.style.backgroundColor = 'transparent';
});
}
startBubbles(canvas);
}
function startBubbles(canvas) {
var ctx = canvas.getContext('2d');
var DARK = !window.matchMedia('(prefers-color-scheme: light)').matches;
var COLORS = ['#57E4C4', '#B8A9C9', '#F5C842'];
var W, H, viewportH, bubbles = [], mx = -9999, my = -9999;
var animationId = null;
var isRunning = false;
var resizeTimer = null;
function resize() {
var vv = window.visualViewport;
var dpr = Math.max(1, Math.min(window.devicePixelRatio || 1, 2));
var baseW = Math.ceil(Math.max(
vv ? vv.width : 0,
window.innerWidth || 0,
document.documentElement.clientWidth || 0
));
var baseH = Math.ceil(Math.max(
vv ? vv.height : 0,
window.innerHeight || 0,
document.documentElement.clientHeight || 0
));
var extraH = Math.ceil(Math.max(500, baseH * 0.65));
var extraW = 80;
viewportH = baseH;
W = baseW + extraW;
H = baseH + extraH;
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
canvas.width = Math.ceil(W * dpr);
canvas.height = Math.ceil(H * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
function srand(seed) {
var s = seed >>> 0;
return function() {
s = Math.imul(s^(s>>>17),0xd4d9c3b7)>>>0;
s = Math.imul(s^(s>>>13),0x9e3779b9)>>>0;
return (s>>>0)/0x100000000;
};
}
function rgb(hex) {
var n = parseInt(hex.slice(1),16);
return [(n>>16)&255,(n>>8)&255,n&255];
}
function initBubbles() {
var r = srand(20240315);
bubbles = [];
[[.08,.04],[.92,.08],[.03,.28],[.97,.22],[.01,.52],[.99,.48],
[.07,.73],[.93,.67],[.02,.88],[.98,.92],[.14,.97],[.86,.98]]
.forEach(function(p,i){
bubbles.push({
x:p[0]*W,
y:p[1]*H,
r:90+r()*130,
c:COLORS[i%3],
a:DARK?.28:.18,
vx:(r()-.5)*.28,
vy:(r()-.5)*.28
});
});
var scale = Math.min(2, Math.max(1, H / Math.max(viewportH || H, 1)));
var mediumCount = Math.round(18 * scale);
var smallCount = Math.round(22 * scale);
for(var i=0;i<mediumCount;i++) {
bubbles.push({
x:r()*W,
y:r()*H,
r:28+r()*65,
c:COLORS[Math.floor(r()*3)],
a:DARK?.17:.12,
vx:(r()-.5)*.38,
vy:(r()-.5)*.38
});
}
for(var i=0;i<smallCount;i++) {
bubbles.push({
x:r()*W,
y:r()*H,
r:8+r()*20,
c:COLORS[Math.floor(r()*3)],
a:DARK?.24:.16,
vx:(r()-.5)*.55,
vy:(r()-.5)*.55
});
}
}
function tick() {
bubbles.forEach(function(b){
b.x+=b.vx;
b.y+=b.vy;
if(b.x-b.r>W) b.x=-b.r;
else if(b.x+b.r<0) b.x=W+b.r;
if(b.y-b.r>H) b.y=-b.r;
else if(b.y+b.r<0) b.y=H+b.r;
var dx=b.x-mx, dy=b.y-my, d=Math.sqrt(dx*dx+dy*dy), rr=170+b.r;
if(d<rr&&d>0){
var f=(rr-d)/rr*1.9;
b.vx+=dx/d*f*.06;
b.vy+=dy/d*f*.06;
}
b.vx*=.984;
b.vy*=.984;
var sp=Math.sqrt(b.vx*b.vx+b.vy*b.vy);
if(sp<.07){
b.vx+=(Math.random()-.5)*.04;
b.vy+=(Math.random()-.5)*.04;
}
if(sp>2.8){
b.vx*=2.8/sp;
b.vy*=2.8/sp;
}
});
}
function draw() {
ctx.clearRect(0,0,W,H);
bubbles.forEach(function(b){
var c=rgb(b.c);
var g2=ctx.createRadialGradient(b.x,b.y,0,b.x,b.y,b.r);
g2.addColorStop(0,"rgba("+c[0]+","+c[1]+","+c[2]+","+b.a+")");
g2.addColorStop(0.45,"rgba("+c[0]+","+c[1]+","+c[2]+","+(b.a*.65)+")");
g2.addColorStop(1,"rgba("+c[0]+","+c[1]+","+c[2]+",0)");
ctx.beginPath();
ctx.arc(b.x,b.y,b.r,0,Math.PI*2);
ctx.fillStyle=g2;
ctx.fill();
});
}
function loop() {
if (!isRunning) return;
tick();
draw();
animationId = requestAnimationFrame(loop);
}
function startAnimation() {
if (isRunning) return;
isRunning = true;
loop();
}
function stopAnimation() {
isRunning = false;
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
}
window.bubbleAnimation = {
start: startAnimation,
stop: stopAnimation
};
function scheduleResize() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function() {
resize();
initBubbles();
if (isRunning) {
draw();
}
}, 150);
}
window.addEventListener('resize', scheduleResize);
window.addEventListener('orientationchange', scheduleResize);
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', scheduleResize);
window.visualViewport.addEventListener('scroll', scheduleResize);
}
window.addEventListener('load', function(){
resize();
initBubbles();
});
window.addEventListener('mousemove', function(e){
mx=e.clientX;
my=e.clientY;
});
window.addEventListener('touchmove', function(e){
if(e.touches.length){
mx=e.touches[0].clientX;
my=e.touches[0].clientY;
}
}, {passive:true});
window.addEventListener('mouseleave', function(){
mx=-9999;
my=-9999;
});
resize();
initBubbles();
if (bubbleEnabled) {
startAnimation();
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();