9kgCoverPeek

在 M-Team 页面提取番号并显示封面图,支持尺寸切换

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         9kgCoverPeek
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  在 M-Team 页面提取番号并显示封面图,支持尺寸切换
// @author       opoa
// @match        https://*.m-team.cc/browse/adult*
// @match        https://*.m-team.cc/showcaseDetail*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @icon         https://res.cfopoa.top/icon/9k-512.webp
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // 尺寸配置
    const SIZE_CONFIG = {
        small:  { maxWidth: '150px', label: '小'},
        medium: { maxWidth: '280px', label: '中'},
        large:  { maxWidth: '450px', label: '大'},
    };

    const STORAGE_KEY = 'nkg_cover_size';
    const POSITION_KEY = 'nkg_panel_position';

    const ICON_URL = 'https://res.cfopoa.top/icon/9k-512.webp';

    function getCurrentSize() {
        return GM_getValue(STORAGE_KEY, 'medium');
    }

    function setCurrentSize(size) {
        GM_setValue(STORAGE_KEY, size);
    }

    // 提取番号,支持大小写
    function extractAvCode(text) {
        const match = text.match(/([A-Za-z]{2,5})-(\d{2,5})/);
        return match ? match[0] : null;
    }

    // 创建封面图元素(含骨架屏容器)
    function createCoverImage(avCode) {
        const lowerCode = avCode.toLowerCase();
        const wrapper = document.createElement('span');
        wrapper.className = 'nkg-cover-wrapper';

        const img = document.createElement('img');
        img.src = `https://fourhoi.com/${lowerCode}/cover-n.jpg`;
        img.className = 'nkg-cover-img';
        img.loading = 'lazy';
        img.alt = avCode;

        img.addEventListener('load', () => wrapper.classList.add('nkg-loaded'), { once: true });
        img.addEventListener('error', () => {
            img.remove();
            wrapper.classList.add('nkg-error');
            wrapper.textContent = avCode;
        }, { once: true });

        wrapper.appendChild(img);
        return wrapper;
    }

    // 防重复处理
    function processSpan(span) {
        if (span.dataset.nkgProcessed) return;
        span.dataset.nkgProcessed = 'true';

        const avCode = extractAvCode(span.textContent);
        if (!avCode) return;

        const img = createCoverImage(avCode);
        span.parentNode.insertBefore(img, span);
    }

    // 切换所有封面图尺寸(通过 CSS 变量)
    function updateAllImages(size) {
        setCurrentSize(size);
        document.documentElement.style.setProperty('--nkg-cover-size', SIZE_CONFIG[size].maxWidth);
        updateSizeIndicator(size);
    }

    // 更新浮动按钮的激活状态
    function updateSizeIndicator(size) {
        const panel = document.getElementById('nkg-size-panel');
        if (!panel) return;
        panel.querySelectorAll('.nkg-btn').forEach(btn => {
            const isActive = btn.dataset.size === size;
            btn.classList.toggle('active', isActive);
            btn.setAttribute('aria-checked', isActive);
        });
    }

    // 展开/收起时自动调整位置,确保面板不超出视口
    function adjustPanelPosition(wrapper) {
        const isExpanded = wrapper.classList.contains('expanded');
        if (isExpanded) {
            wrapper._nkgOrigPos = {
                left: wrapper.style.left,
                top: wrapper.style.top,
                right: wrapper.style.right,
                bottom: wrapper.style.bottom
            };
            requestAnimationFrame(() => {
                const rect = wrapper.getBoundingClientRect();
                const fullW = wrapper.scrollWidth;
                const fullH = wrapper.scrollHeight;
                const vw = window.innerWidth;
                const vh = window.innerHeight;
                const pad = 8;
                let left = rect.left;
                let top = rect.top;
                let adjusted = false;
                if (left + fullW > vw - pad) { left = vw - fullW - pad; adjusted = true; }
                if (left < pad) { left = pad; adjusted = true; }
                if (top + fullH > vh - pad) { top = vh - fullH - pad; adjusted = true; }
                if (top < pad) { top = pad; adjusted = true; }
                if (adjusted) {
                    wrapper.classList.add('nkg-adjusting');
                    wrapper.style.left = left + 'px';
                    wrapper.style.top = top + 'px';
                    wrapper.style.right = 'auto';
                    wrapper.style.bottom = 'auto';
                    wrapper._nkgAdjusted = true;
                }
            });
        } else if (wrapper._nkgAdjusted && wrapper._nkgOrigPos) {
            const pos = wrapper._nkgOrigPos;
            wrapper.style.left = pos.left;
            wrapper.style.top = pos.top;
            wrapper.style.right = pos.right;
            wrapper.style.bottom = pos.bottom;
            wrapper._nkgAdjusted = false;
            setTimeout(() => wrapper.classList.remove('nkg-adjusting'), 380);
        }
    }

    // 使元素可拖动,拖动距离小于阈值视为点击(Pointer Events 支持触屏)
    function makeDraggable(wrapper, handle) {
        let startX, startY, origX, origY, dragging = false;

        handle.addEventListener('pointerdown', e => {
            e.preventDefault();
            handle.setPointerCapture(e.pointerId);
            startX = e.clientX;
            startY = e.clientY;
            const rect = wrapper.getBoundingClientRect();
            origX = rect.left;
            origY = rect.top;
            dragging = false;

            function onMove(e) {
                const dx = e.clientX - startX;
                const dy = e.clientY - startY;
                if (!dragging && Math.abs(dx) + Math.abs(dy) > 4) {
                    dragging = true;
                    wrapper.classList.remove('nkg-adjusting');
                }
                if (dragging) {
                    wrapper.style.left = origX + dx + 'px';
                    wrapper.style.top = origY + dy + 'px';
                    wrapper.style.right = 'auto';
                    wrapper.style.bottom = 'auto';
                }
            }

            function onUp() {
                handle.removeEventListener('pointermove', onMove);
                handle.removeEventListener('pointerup', onUp);
                if (!dragging) {
                    wrapper.classList.toggle('expanded');
                    handle.setAttribute('aria-expanded', wrapper.classList.contains('expanded'));
                    adjustPanelPosition(wrapper);
                } else {
                    wrapper._nkgAdjusted = false;
                    GM_setValue(POSITION_KEY, JSON.stringify({
                        left: wrapper.style.left,
                        top: wrapper.style.top
                    }));
                }
            }

            handle.addEventListener('pointermove', onMove);
            handle.addEventListener('pointerup', onUp);
        });
    }

    // 创建浮动尺寸切换面板(点击触发按钮展开/收起,可拖动)
    function createSizeToggle() {
        const wrapper = document.createElement('div');
        wrapper.id = 'nkg-size-toggle';

        // 头部:图标 + 标题同行
        const header = document.createElement('div');
        header.className = 'nkg-header';

        const trigger = document.createElement('img');
        trigger.id = 'nkg-trigger-btn';
        trigger.src = ICON_URL;
        trigger.alt = '9kgCoverPeek';
        trigger.setAttribute('role', 'button');
        trigger.setAttribute('aria-label', '切换封面图尺寸面板');
        trigger.setAttribute('aria-expanded', 'false');
        header.appendChild(trigger);

        const title = document.createElement('div');
        title.className = 'nkg-panel-title';
        title.textContent = 'Cover Peek 👀';
        header.appendChild(title);

        wrapper.appendChild(header);

        const panel = document.createElement('div');
        panel.id = 'nkg-size-panel';
        panel.setAttribute('role', 'group');
        panel.setAttribute('aria-label', '封面尺寸选项');
        const currentSize = getCurrentSize();

        // 封面尺寸选项行
        const row = document.createElement('div');
        row.className = 'nkg-option-row';
        const label = document.createElement('span');
        label.className = 'nkg-option-label';
        label.id = 'nkg-size-label';
        label.textContent = '封面尺寸:';
        row.appendChild(label);

        const btns = document.createElement('div');
        btns.className = 'nkg-option-btns';
        btns.setAttribute('role', 'radiogroup');
        btns.setAttribute('aria-labelledby', 'nkg-size-label');
        Object.entries(SIZE_CONFIG).forEach(([key, config]) => {
            const btn = document.createElement('button');
            btn.textContent = config.label;
            btn.dataset.size = key;
            btn.className = key === currentSize ? 'nkg-btn active' : 'nkg-btn';
            btn.setAttribute('role', 'radio');
            btn.setAttribute('aria-checked', key === currentSize);
            btn.addEventListener('click', () => updateAllImages(key));
            btns.appendChild(btn);
        });
        row.appendChild(btns);
        panel.appendChild(row);

        wrapper.appendChild(panel);
        document.body.appendChild(wrapper);

        // 恢复拖拽位置(校验视口边界)
        try {
            const pos = JSON.parse(GM_getValue(POSITION_KEY, 'null'));
            if (pos && pos.left && pos.top) {
                const left = parseInt(pos.left, 10);
                const top = parseInt(pos.top, 10);
                if (left >= 0 && left < window.innerWidth - 20 && top >= 0 && top < window.innerHeight - 20) {
                    wrapper.style.left = pos.left;
                    wrapper.style.top = pos.top;
                    wrapper.style.right = 'auto';
                    wrapper.style.bottom = 'auto';
                }
            }
        } catch (_) {}

        makeDraggable(wrapper, trigger);
    }

    // 注入样式
    function addStyles() {
        GM_addStyle(`
            /* 封面图容器(骨架屏) */
            .nkg-cover-wrapper {
                display: inline-block;
                vertical-align: middle;
                margin-right: 5px;
                min-width: 40px;
                min-height: 40px;
                border-radius: 4px;
                background: linear-gradient(90deg, #eee 25%, #ddd 50%, #eee 75%);
                background-size: 200% 100%;
                animation: nkg-shimmer 1.5s ease infinite;
            }
            .nkg-cover-wrapper.nkg-loaded {
                min-width: unset;
                min-height: unset;
                background: none;
                animation: none;
            }
            .nkg-cover-wrapper.nkg-error {
                display: inline-flex;
                align-items: center;
                min-width: unset;
                min-height: unset;
                padding: 2px 6px;
                background: #f8f8f8;
                border: 1px dashed #ccc;
                font-size: 11px;
                color: #999;
                animation: none;
            }
            @keyframes nkg-shimmer {
                0% { background-position: 200% 0; }
                100% { background-position: -200% 0; }
            }
            /* 封面图 */
            .nkg-cover-img {
                max-width: var(--nkg-cover-size, 280px);
                vertical-align: middle;
                border-radius: 4px;
                opacity: 0;
                transition: max-width 0.3s ease, opacity 0.3s ease;
            }
            .nkg-loaded .nkg-cover-img {
                opacity: 1;
            }
            /* 浮动面板 */
            #nkg-size-toggle {
                position: fixed;
                bottom: 20px;
                right: 20px;
                z-index: 9999;
                padding: 6px;
                border-radius: 16px;
                background: transparent;
                box-shadow: none;
                overflow: hidden;
                max-width: 40px;
                max-height: 40px;
                transition: max-width 0.35s cubic-bezier(0.25, 0.8, 0.25, 1),
                            max-height 0.35s cubic-bezier(0.25, 0.8, 0.25, 1),
                            background 0.3s ease,
                            box-shadow 0.3s ease,
                            border-radius 0.3s ease;
            }
            #nkg-size-toggle.expanded {
                max-width: 400px;
                max-height: 200px;
                background: #fff;
                box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
            }
            #nkg-size-toggle.nkg-adjusting {
                transition: max-width 0.35s cubic-bezier(0.25, 0.8, 0.25, 1),
                            max-height 0.35s cubic-bezier(0.25, 0.8, 0.25, 1),
                            background 0.3s ease,
                            box-shadow 0.3s ease,
                            border-radius 0.3s ease,
                            left 0.35s cubic-bezier(0.25, 0.8, 0.25, 1),
                            top 0.35s cubic-bezier(0.25, 0.8, 0.25, 1);
            }
            #nkg-trigger-btn {
                width: 28px;
                height: 28px;
                min-width: 28px;
                border-radius: 50%;
                cursor: grab;
                transition: transform 0.2s;
                user-select: none;
                -webkit-user-drag: none;
            }
            #nkg-trigger-btn:hover {
                transform: scale(1.1);
            }
            .nkg-header {
                display: flex;
                align-items: center;
                gap: 8px;
                white-space: nowrap;
            }
            #nkg-size-panel {
                margin-top: 6px;
                padding: 4px 8px 2px;
                opacity: 0;
                transition: opacity 0.3s ease;
            }
            #nkg-size-toggle.expanded #nkg-size-panel {
                opacity: 1;
            }
            .nkg-panel-title {
                font-size: 16px;
                font-weight: 700;
                color: #222;
                white-space: nowrap;
                flex: 1;
                word-spacing: 0.4em;
                opacity: 0;
                transition: opacity 0.3s ease;
            }
            #nkg-size-toggle.expanded .nkg-panel-title {
                opacity: 1;
            }
            .nkg-option-row {
                display: flex;
                align-items: center;
                gap: 6px;
                white-space: nowrap;
            }
            .nkg-option-label {
                font-size: 13px;
                color: #333;
                font-weight: 500;
            }
            .nkg-option-btns {
                display: flex;
                gap: 4px;
            }
            .nkg-btn {
                border: 1px solid #e0e0e0;
                padding: 4px 14px;
                border-radius: 10px;
                cursor: pointer;
                background: #fafafa;
                color: #555;
                font-size: 13px;
                transition: color 0.2s, background 0.2s, border-color 0.2s, transform 0.15s;
            }
            .nkg-btn:hover {
                color: #333;
                background: #f0f0f0;
                border-color: #ccc;
            }
            .nkg-btn:active {
                transform: scale(0.92);
            }
            .nkg-btn.active {
                background: #4a6cf7;
                color: #fff;
                border-color: #4a6cf7;
            }
        `);
    }

    // 初始化
    function init() {
        addStyles();
        document.documentElement.style.setProperty('--nkg-cover-size', SIZE_CONFIG[getCurrentSize()].maxWidth);
        createSizeToggle();

        document.querySelectorAll('span[aria-describedby]').forEach(processSpan);

        new MutationObserver(mutations => {
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        if (node.matches('span[aria-describedby]')) processSpan(node);
                        node.querySelectorAll('span[aria-describedby]').forEach(processSpan);
                    }
                }
            }
        }).observe(document.body, { childList: true, subtree: true });
    }

    if (document.readyState === 'complete') init();
    else window.addEventListener('load', init);
})();