Bilibili分享优化

劫持Bilibili分享按钮,直接提取分享文本,让分享更加清爽

// ==UserScript==
// @name         Bilibili分享优化
// @description  劫持Bilibili分享按钮,直接提取分享文本,让分享更加清爽
// @version      0.6
// @author       DoubleCat
// @copyright    2025, DoubleCat (https://github.com/doublebobcat)
// @match        https://www.bilibili.com/video/*
// @license      GPL_V3
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @icon         https://raw.githubusercontent.com/doublebobcat/Bilibili-Share-Optimization/master/images/logo-small.png
// @icon64       https://raw.githubusercontent.com/doublebobcat/Bilibili-Share-Optimization/master/images/logo.png
// @namespace https://greasyfork.org/users/1505795
// ==/UserScript==

(function () {
    'use strict';

    const defaultStyles = {
        position: 'fixed',
        bottom: '30px',
        right: '30px',
        backgroundColor: 'rgba(255, 255, 255, 1)',
        color: '#000',
        padding: '12px 18px',
        borderRadius: '6px',
        zIndex: 9999,
        fontSize: '16px',
        transition: 'opacity 0.5s ease',
        opacity: '1'
    };

    const maxChars = 70;
    const maxLines = 4;

    let styles = defaultStyles;

    // 从存储加载配置
    function loadConfig() {
        try {
            const config = GM_getValue('messageStyles');
            return config ? JSON.parse(config) : defaultStyles;
        } catch (err) {
            return defaultStyles;
        }
    }

    // 保存配置
    function saveConfig(config) {
        GM_setValue('messageStyles', JSON.stringify(config));
    }

    function extractWithXPath(xpathExpression) {
        const result = document.evaluate(xpathExpression, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
        return result.singleNodeValue ? result.singleNodeValue.textContent : '未找到';
    }

    async function copyToClipboard(text) {
        if (navigator.clipboard && navigator.clipboard.writeText) {
            try {
                await navigator.clipboard.writeText(text);
                return true;
            } catch (err) {
                console.warn('[Bilibili-Share-Optimization] Clipboard API 失败,回退到 execCommand');
            }
        }
        // fallback
        const tempInput = document.createElement('textarea');
        tempInput.style.position = 'absolute';
        tempInput.style.left = '-9999px';
        tempInput.value = text;
        document.body.appendChild(tempInput);
        tempInput.select();
        document.execCommand('copy');
        document.body.removeChild(tempInput);
        return true;
    }

    function copyInfo() {
        const pageURL = window.location.href;
        const title = extractWithXPath('/html/body/div[2]/div[2]/div[1]/div[1]/div[1]/div/h1').trim();
        let nickname = extractWithXPath('/html/body/div[2]/div[2]/div[2]/div/div[1]/div[1]/div[2]/div[1]/div/div[1]/a[1]').trim();
        if (nickname === '直播中') {
            nickname = extractWithXPath('/html/body/div[2]/div[2]/div[2]/div/div[1]/div[1]/div[2]/div[1]/div/div[1]/a[2]').trim();
        }
        let description = extractWithXPath('/html/body/div[2]/div[2]/div[1]/div[4]/div/span').trim();

        let lines = description.split(/\r?\n/);

        if (description.length > maxChars || lines.length > maxLines) {
            // 先按字符限制截断
            let truncated = description.substring(0, maxChars);

            // 确保不会超过4行
            let truncatedLines = truncated.split(/\r?\n/).slice(0, maxLines);
            description = truncatedLines.join("\n") + "......";
        }

        const combinedInfo = `URL: ${pageURL}\nUP主: ${nickname}\n标题: ${title}\n简介: ${description}`;

        copyToClipboard(combinedInfo);

        const copiedMessage = document.createElement('div');
        copiedMessage.textContent = '已复制';

        for (const [property, value] of Object.entries(styles)) {
            copiedMessage.style[property] = value;
        }

        document.body.appendChild(copiedMessage);

        setTimeout(() => {
            if (copiedMessage.parentNode) {
                copiedMessage.parentNode.removeChild(copiedMessage);
            }
        }, 3000);
    }

    // 页面加载完毕->劫持分享按钮
    const observer = new MutationObserver(() => {
        const shareBtn = document.querySelector('#share-btn-outer');
        if (shareBtn) {
            try {
                styles = loadConfig();
            } catch (err) {
                styles = defaultStyles;
            }

            // 移除原有点击事件
            shareBtn.replaceWith(shareBtn.cloneNode(true));
            const newShareBtn = document.querySelector('#share-btn-outer');

            // 添加自有点击事件
            newShareBtn.addEventListener('click', function (e) {
                e.stopPropagation();
                e.preventDefault();
                copyInfo();
            });

            console.log('[Bilibili-Share-Optimization] 已劫持B站分享按钮');
            observer.disconnect();
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });

    // 添加菜单项:打开设置界面
    GM_registerMenuCommand("设置分享窗口样式", openSettingsWindow);

    function openSettingsWindow() {
        const settingsWindow = document.createElement('div');
        settingsWindow.style.position = 'fixed';
        settingsWindow.style.top = '10%';
        settingsWindow.style.left = '50%';
        settingsWindow.style.transform = 'translateX(-50%)';
        settingsWindow.style.backgroundColor = '#000000ff';
        settingsWindow.style.color = '#fff';
        settingsWindow.style.padding = '20px';
        settingsWindow.style.borderRadius = '10px';
        settingsWindow.style.border = '0'
        settingsWindow.style.zIndex = 9999;
        settingsWindow.style.width = '300px';

        settingsWindow.innerHTML = `
            <h3>设置分享窗口样式</h3>
            <label for="backgroundColor">背景颜色:</label>
            <input type="color" id="backgroundColor" value="${styles.backgroundColor}"><br><br>
            <label for="color">字体颜色:</label>
            <input type="color" id="color" value="${styles.color}"><br><br>
            <label for="fontSize">字体大小:</label>
            <input type="number" id="fontSize" value="${parseInt(styles.fontSize)}" min="10" max="30"><br><br>
            <button id="saveButton">保存设置</button>
            <button id="cancelButton">取消</button>
        `;

        document.body.appendChild(settingsWindow);

        document.getElementById('saveButton').addEventListener('click', () => {
            styles.backgroundColor = document.getElementById('backgroundColor').value;
            styles.color = document.getElementById('color').value;
            styles.fontSize = document.getElementById('fontSize').value + 'px';
            saveConfig(styles);
            document.body.removeChild(settingsWindow);
        });

        document.getElementById('cancelButton').addEventListener('click', () => {
            document.body.removeChild(settingsWindow);
        });
    }
})();