跳转到内容
打开/关闭菜单
  • 451 条目
  • 806 文件
  • 22 用户
  • 9167 编辑
Blockland Wiki
打开/关闭外观设置菜单
打开/关闭个人菜单
未登录
未登录用户的IP地址会在进行任意编辑后公开展示。

MediaWiki:Common.js

MediaWiki界面页面
ETTW​(留言 | 贡献)2026年4月18日 (六) 15:42的版本

注意:在发布之后,您可能需要清除浏览器缓存才能看到所作出的更改的影响。

  • Firefox或Safari:按住Shift的同时单击刷新,或按Ctrl-F5或Ctrl-R(Mac为⌘-R)
  • Google Chrome:按Ctrl-Shift-R(Mac为⌘-Shift-R)
  • Edge:按住Ctrl的同时单击刷新,或按Ctrl-F5。
/* 这里的任何JavaScript将为所有用户在每次页面加载时加载。 */

// 侧边栏广告
$(function() {
    if ($('#p-navigation').length) {
        var adHtml = '<div id="sidebar-ad"></div>';
        $('#p-navigation').after(adHtml);
        $.get(mw.util.wikiScript('api'), {
            action: 'parse',
            text: '{{CitizenAd}}',
            format: 'json'
        }, function(data) {
            $('#sidebar-ad').html(data.parse.text['*']);
        });
    }
});

/**
 * Blockland Wiki Liquid Glass Lighting System
 */
(function() {
    'use strict';
    
    var isPerformanceMode = function() {
        return document.documentElement.classList.contains('citizen-feature-performance-mode-clientpref-1');
    };

    var GLASS_SELECTORS = '#mw-panel-toc, #citizen-sidebar-lastmod, .aero-news-box, .citizen-header, .citizen-menu__card, .citizen-toc-card, .citizen-command-palette, .infobox, table.ambox, .mwe-popups-container, .oo-ui-popupWidget-popup, .oo-ui-menuSelectWidget, .voy-box';

    var lastMouseX = 0;
    var lastMouseY = 0;

    var syncLight = function(e) {
        if (isPerformanceMode()) return;

        lastMouseX = e.clientX;
        lastMouseY = e.clientY;

        var elements = document.querySelectorAll(GLASS_SELECTORS);
        elements.forEach(function(el) {
            var rect = el.getBoundingClientRect();
            
            if (rect.width > 0 && rect.top < window.innerHeight && rect.bottom > 0) {
                var x = ((e.clientX - rect.left) / rect.width) * 100;
                var y = ((e.clientY - rect.top) / rect.height) * 100;
                
                el.style.setProperty('--mouse-x', x.toFixed(2) + '%');
                el.style.setProperty('--mouse-y', y.toFixed(2) + '%');
            }
        });
    };

    var initPopupLight = function(container) {
        if (isPerformanceMode()) return;
        
        var rect = container.getBoundingClientRect();
        if (rect.width > 0) {
            var x = ((lastMouseX - rect.left) / rect.width) * 100;
            var y = ((lastMouseY - rect.top) / rect.height) * 100;
            container.style.setProperty('--mouse-x', x.toFixed(2) + '%');
            container.style.setProperty('--mouse-y', y.toFixed(2) + '%');
        }
    };

    setInterval(function() {
        if (isPerformanceMode()) return; 
        var p = document.querySelector('.citizen-command-palette');
        if (p && !p.classList.contains('glass-show')) {
            p.classList.add('glass-show');
        }
    }, 100);

    var ticking = false;
    document.addEventListener('mousemove', function(e) {
        if (isPerformanceMode()) return;
        if (!ticking) {
            window.requestAnimationFrame(function() {
                syncLight(e);
                ticking = false;
            });
            ticking = true;
        }
    }, { passive: true });

    var modeObserver = new MutationObserver(function() {
        if (isPerformanceMode()) {
            document.querySelectorAll(GLASS_SELECTORS).forEach(function(el) {
                el.style.removeProperty('--mouse-x');
                el.style.removeProperty('--mouse-y');
            });
        }
    });
    modeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });

    window._liquidGlassInitPopup = initPopupLight;
})();

/**
 * Section25. Wiki 弹出预览菜单动画控制
 */
(function() {
    'use strict';
    
    var style = document.createElement('style');
    style.textContent = `
        .mwe-popups .mwe-popups-container { 
            opacity: 0 !important; 
            transform: translateY(-12px) !important;
            transition: opacity 0.35s cubic-bezier(0.16, 1, 0.3, 1), 
                        transform 0.35s cubic-bezier(0.16, 1, 0.3, 1) !important;
        } 
        .mwe-popups.popup-show .mwe-popups-container { 
            opacity: 1 !important; 
            transform: translateY(0) !important;
        }
        .mwe-popups.flipped-y .mwe-popups-container {
            transform: translateY(12px) !important;
        }
        .mwe-popups.flipped-y.popup-show .mwe-popups-container {
            transform: translateY(0) !important;
        }
    `;
    document.head.appendChild(style);
    
    var seen = new WeakSet();
    
    new MutationObserver(function(muts) {
        muts.forEach(function(m) {
            m.addedNodes.forEach(function(node) {
                if (node.classList && node.classList.contains('mwe-popups') && !seen.has(node)) {
                    seen.add(node);
                    
                    var container = node.querySelector('.mwe-popups-container');
                    if (!container) return;
                    
                    if (window._liquidGlassInitPopup) {
                        window._liquidGlassInitPopup(container);
                    }
                    
                    node.style.pointerEvents = 'none';
                    container.offsetHeight;
                    
                    requestAnimationFrame(function() {
                        requestAnimationFrame(function() {
                            node.classList.add('popup-show');
                            setTimeout(function() {
                                node.style.pointerEvents = '';
                            }, 350);
                        });
                    });
                }
            });
        });
    }).observe(document.body, { childList: true, subtree: true });
})();
/**
 * Section19. 通知面板动画控制
 * 检测到面板后再开始动画
 */
(function() {
    'use strict';
    
    function handlePopup(popup) {
        if (popup.dataset.animatorInited) return;
        popup.dataset.animatorInited = 'true';

        const observer = new MutationObserver(() => {
            const inner = popup.querySelector('.oo-ui-popupWidget-popup');
            if (!inner) return;

            const isVisible = popup.style.display !== 'none' && popup.offsetParent !== null;
            
if (isVisible) {
    if (inner.getAttribute('data-is-active') !== 'true') {
        inner.setAttribute('data-is-active', 'true');

        // 1. 初始化:置于下方并透明
        inner.style.setProperty('transition', 'none', 'important');
        inner.style.setProperty('opacity', '0', 'important');
        inner.style.setProperty('transform', 'translateY(20px)', 'important');

        inner.offsetHeight; // 触发重绘

        requestAnimationFrame(() => {
            setTimeout(() => {
                // 2. 向上滑动并渐显
                inner.style.setProperty('transition', 'opacity 0.3s ease-out, transform 0.4s cubic-bezier(0.23, 1, 0.32, 1)', 'important');
                inner.style.setProperty('opacity', '1', 'important');
                inner.style.setProperty('transform', 'translateY(0)', 'important');
            }, 10);
        });
    }
            } else {
                // 关闭时直接重置,不加动画,确保下次打开干净利落
                if (inner.getAttribute('data-is-active') === 'true') {
                    inner.removeAttribute('data-is-active');
                    inner.style.removeProperty('opacity');
                    inner.style.removeProperty('transform');
                    inner.style.removeProperty('transition');
                    inner.style.removeProperty('transform-origin');
                }
            }
        });

        observer.observe(popup, { attributes: true, attributeFilter: ['style', 'class'] });
    }

    const scanner = new MutationObserver(() => {
        document.querySelectorAll('.mw-echo-ui-notificationBadgeButtonPopupWidget-popup').forEach(handlePopup);
    });
    scanner.observe(document.body, { childList: true, subtree: true });
    document.querySelectorAll('.mw-echo-ui-notificationBadgeButtonPopupWidget-popup').forEach(handlePopup);
})();

// 添加液态玻璃按钮
(function() {
    'use strict';
    
    function addLiquidGlassOption() {
        const form = document.querySelector('#skin-client-prefs-citizen-feature-performance-mode form');
        
        if (!form) {
            setTimeout(addLiquidGlassOption, 100);
            return;
        }
        
        // 检查是否已经添加过
        if (document.getElementById('skin-client-pref-citizen-feature-performance-mode-value-2')) {
            return;
        }
        
        // 创建液态玻璃选项(状态3 - 高级液态玻璃)
        const advancedLiquidGlassOption = document.createElement('div');
        advancedLiquidGlassOption.className = 'citizen-client-prefs-radio';
        advancedLiquidGlassOption.innerHTML = `
            <input name="skin-client-pref-citizen-feature-performance-mode-group" 
                   id="skin-client-pref-citizen-feature-performance-mode-value-3" 
                   type="radio" 
                   value="3" 
                   data-event-name="skin-client-pref-citizen-feature-performance-mode-value-3" 
                   class="citizen-client-prefs-radio__input">
            <span class="citizen-client-prefs-radio__icon"></span>
            <label for="skin-client-pref-citizen-feature-performance-mode-value-3" 
                   class="citizen-client-prefs-radio__label">液态玻璃Pro</label>
        `;
        
        // 创建基础液态玻璃选项(状态2)
        const basicLiquidGlassOption = document.createElement('div');
        basicLiquidGlassOption.className = 'citizen-client-prefs-radio';
        basicLiquidGlassOption.innerHTML = `
            <input name="skin-client-pref-citizen-feature-performance-mode-group" 
                   id="skin-client-pref-citizen-feature-performance-mode-value-2" 
                   type="radio" 
                   value="2" 
                   data-event-name="skin-client-pref-citizen-feature-performance-mode-value-2" 
                   class="citizen-client-prefs-radio__input">
            <span class="citizen-client-prefs-radio__icon"></span>
            <label for="skin-client-pref-citizen-feature-performance-mode-value-2" 
                   class="citizen-client-prefs-radio__label">液态玻璃</label>
        `;
        
        // 按顺序添加:状态3在前,状态2在后
        form.appendChild(advancedLiquidGlassOption);
        form.appendChild(basicLiquidGlassOption);
        
        console.log('液态玻璃按钮已添加');
        
        // 绑定所有单选按钮的事件
        const allRadios = document.querySelectorAll('input[name="skin-client-pref-citizen-feature-performance-mode-group"]');
        allRadios.forEach(radio => {
            radio.addEventListener('change', function() {
                const value = this.value;
                console.log('选中值:', value);
                localStorage.setItem('citizen-feature-performance-mode-clientpref-', value);
                
                // 更新 HTML class
                const htmlElement = document.documentElement;
                htmlElement.className = htmlElement.className.replace(/citizen-feature-performance-mode-clientpref-\d/g, '');
                htmlElement.classList.add('citizen-feature-performance-mode-clientpref-' + value);
                
                // 状态3时初始化高级液态玻璃
                if (value === '3') {
                    initAdvancedLiquidGlass();
                } else {
                    destroyAdvancedLiquidGlass();
                }
            });
        });
    }
    
    // 页面加载时执行
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', addLiquidGlassOption);
    } else {
        addLiquidGlassOption();
    }
    
})();

// 高级液态玻璃效果
function initAdvancedLiquidGlass() {
    if (window.liquidGlassAdvanced) {
        return; // 已经初始化
    }
    
    // 创建液态玻璃着色器
    class Shader {
        constructor(options = {}) {
            this.width = options.width || 100;
            this.height = options.height || 100;
            this.fragment = options.fragment || ((uv) => ({ type: 't', x: uv.x, y: uv.y }));
            this.canvasDPI = 1;
            this.id = 'liquid-glass-' + Math.random().toString(36).substr(2, 9);
            this.offset = 10;
            this.mouse = { x: 0, y: 0 };
            this.mouseUsed = false;
            
            this.createElement();
            this.setupEventListeners();
            this.updateShader();
        }

        createElement() {
            this.container = document.createElement('div');
            this.container.style.cssText = `
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                width: ${this.width}px;
                height: ${this.height}px;
                overflow: hidden;
                border-radius: 150px;
                box-shadow: 0 4px 8px rgba(0, 0, 0, 0.25), 0 -10px 25px inset rgba(0, 0, 0, 0.15);
                cursor: grab;
                backdrop-filter: url(#${this.id}_filter) blur(0.25px) contrast(1.2) brightness(1.05) saturate(1.1);
                z-index: 9999;
                pointer-events: auto;
            `;

            this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            this.svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
            this.svg.setAttribute('width', '0');
            this.svg.setAttribute('height', '0');
            this.svg.style.cssText = `
                position: fixed;
                top: 0;
                left: 0;
                pointer-events: none;
                z-index: 9998;
            `;

            const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
            const filter = document.createElementNS('http://www.w3.org/2000/svg', 'filter');
            filter.setAttribute('id', `${this.id}_filter`);
            filter.setAttribute('filterUnits', 'userSpaceOnUse');
            filter.setAttribute('colorInterpolationFilters', 'sRGB');
            filter.setAttribute('x', '0');
            filter.setAttribute('y', '0');
            filter.setAttribute('width', this.width.toString());
            filter.setAttribute('height', this.height.toString());

            this.feImage = document.createElementNS('http://www.w3.org/2000/svg', 'feImage');
            this.feImage.setAttribute('id', `${this.id}_map`);
            this.feImage.setAttribute('width', this.width.toString());
            this.feImage.setAttribute('height', this.height.toString());

            this.feDisplacementMap = document.createElementNS('http://www.w3.org/2000/svg', 'feDisplacementMap');
            this.feDisplacementMap.setAttribute('in', 'SourceGraphic');
            this.feDisplacementMap.setAttribute('in2', `${this.id}_map`);
            this.feDisplacementMap.setAttribute('xChannelSelector', 'R');
            this.feDisplacementMap.setAttribute('yChannelSelector', 'G');

            filter.appendChild(this.feImage);
            filter.appendChild(this.feDisplacementMap);
            defs.appendChild(filter);
            this.svg.appendChild(defs);

            this.canvas = document.createElement('canvas');
            this.canvas.width = this.width * this.canvasDPI;
            this.canvas.height = this.height * this.canvasDPI;
            this.canvas.style.display = 'none';
            this.context = this.canvas.getContext('2d');
        }

        constrainPosition(x, y) {
            const viewportWidth = window.innerWidth;
            const viewportHeight = window.innerHeight;
            const minX = this.offset;
            const maxX = viewportWidth - this.width - this.offset;
            const minY = this.offset;
            const maxY = viewportHeight - this.height - this.offset;
            const constrainedX = Math.max(minX, Math.min(maxX, x));
            const constrainedY = Math.max(minY, Math.min(maxY, y));
            return { x: constrainedX, y: constrainedY };
        }

        setupEventListeners() {
            let isDragging = false;
            let startX, startY, initialX, initialY;

            this.container.addEventListener('mousedown', (e) => {
                isDragging = true;
                this.container.style.cursor = 'grabbing';
                startX = e.clientX;
                startY = e.clientY;
                const rect = this.container.getBoundingClientRect();
                initialX = rect.left;
                initialY = rect.top;
                e.preventDefault();
            });

            document.addEventListener('mousemove', (e) => {
                if (isDragging) {
                    const deltaX = e.clientX - startX;
                    const deltaY = e.clientY - startY;
                    const newX = initialX + deltaX;
                    const newY = initialY + deltaY;
                    const constrained = this.constrainPosition(newX, newY);
                    this.container.style.left = constrained.x + 'px';
                    this.container.style.top = constrained.y + 'px';
                    this.container.style.transform = 'none';
                }

                const rect = this.container.getBoundingClientRect();
                this.mouse.x = (e.clientX - rect.left) / rect.width;
                this.mouse.y = (e.clientY - rect.top) / rect.height;
                
                if (this.mouseUsed) {
                    this.updateShader();
                }
            });

            document.addEventListener('mouseup', () => {
                isDragging = false;
                this.container.style.cursor = 'grab';
            });

            window.addEventListener('resize', () => {
                const rect = this.container.getBoundingClientRect();
                const constrained = this.constrainPosition(rect.left, rect.top);
                if (rect.left !== constrained.x || rect.top !== constrained.y) {
                    this.container.style.left = constrained.x + 'px';
                    this.container.style.top = constrained.y + 'px';
                    this.container.style.transform = 'none';
                }
            });
        }

        smoothStep(a, b, t) {
            t = Math.max(0, Math.min(1, (t - a) / (b - a)));
            return t * t * (3 - 2 * t);
        }

        length(x, y) {
            return Math.sqrt(x * x + y * y);
        }

        roundedRectSDF(x, y, width, height, radius) {
            const qx = Math.abs(x) - width + radius;
            const qy = Math.abs(y) - height + radius;
            return Math.min(Math.max(qx, qy), 0) + this.length(Math.max(qx, 0), Math.max(qy, 0)) - radius;
        }

        updateShader() {
            const mouseProxy = new Proxy(this.mouse, {
                get: (target, prop) => {
                    this.mouseUsed = true;
                    return target[prop];
                }
            });

            this.mouseUsed = false;

            const w = this.width * this.canvasDPI;
            const h = this.height * this.canvasDPI;
            const data = new Uint8ClampedArray(w * h * 4);

            let maxScale = 0;
            const rawValues = [];

            for (let i = 0; i < data.length; i += 4) {
                const x = (i / 4) % w;
                const y = Math.floor(i / 4 / w);
                const pos = this.fragment(
                    { x: x / w, y: y / h },
                    mouseProxy
                );
                const dx = pos.x * w - x;
                const dy = pos.y * h - y;
                maxScale = Math.max(maxScale, Math.abs(dx), Math.abs(dy));
                rawValues.push(dx, dy);
            }

            maxScale *= 0.5;

            let index = 0;
            for (let i = 0; i < data.length; i += 4) {
                const r = rawValues[index++] / maxScale + 0.5;
                const g = rawValues[index++] / maxScale + 0.5;
                data[i] = r * 255;
                data[i + 1] = g * 255;
                data[i + 2] = 0;
                data[i + 3] = 255;
            }

            this.context.putImageData(new ImageData(data, w, h), 0, 0);
            this.feImage.setAttributeNS('http://www.w3.org/1999/xlink', 'href', this.canvas.toDataURL());
            this.feDisplacementMap.setAttribute('scale', (maxScale / this.canvasDPI).toString());
        }

        appendTo(parent) {
            parent.appendChild(this.svg);
            parent.appendChild(this.container);
        }

        destroy() {
            this.svg.remove();
            this.container.remove();
            this.canvas.remove();
        }
    }

    // 创建着色器
    const shader = new Shader({
        width: 300,
        height: 200,
        fragment: function(uv, mouse) {
            const ix = uv.x - 0.5;
            const iy = uv.y - 0.5;
            const distanceToEdge = this.roundedRectSDF(
                ix,
                iy,
                0.3,
                0.2,
                0.6
            );
            const displacement = this.smoothStep(0.8, 0, distanceToEdge - 0.15);
            const scaled = this.smoothStep(0, 1, displacement);
            return { type: 't', x: ix * scaled + 0.5, y: iy * scaled + 0.5 };
        }
    });

    shader.appendTo(document.body);
    window.liquidGlassAdvanced = shader;
    console.log('高级液态玻璃效果已启用');
}

function destroyAdvancedLiquidGlass() {
    if (window.liquidGlassAdvanced) {
        window.liquidGlassAdvanced.destroy();
        window.liquidGlassAdvanced = null;
        console.log('高级液态玻璃效果已关闭');
    }
}