豆瓣图书信息增强(全信息+滴答清单)

提取豆瓣图书全信息,支持普通格式和滴答清单格式复制,优化交互体验

// ==UserScript==
// @name           豆瓣图书信息增强(全信息+滴答清单)
// @description    提取豆瓣图书全信息,支持普通格式和滴答清单格式复制,优化交互体验
// @author         bai
// @version        2.3
// @icon           https://book.douban.com/favicon.ico
// @grant          GM_addStyle
// @require        https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @include        https://book.douban.com/subject/*
// @run-at         document-end
// @license        Apache-2.0
// @namespace      https://greasyfork.org/users/967749
// ==/UserScript==

$(document).ready(function () {
    // 1. 注入样式(更换为莫兰迪灰粉+灰紫配色,优化视觉体验)
    GM_addStyle(`
        .book-copy-container {
            padding: 15px;
            background-color: #f8f5f3; /* 莫兰迪浅暖灰底色,适配粉紫主色 */
            border-radius: 8px;
            margin-bottom: 20px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.05);
            transition: box-shadow 0.3s ease;
        }
        .book-copy-container:hover {
            box-shadow: 0 3px 8px rgba(0,0,0,0.08); /* 容器hover轻微提亮 */
        }
        
        .book-copy-btn {
            padding: 10px 18px;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            margin: 0 8px 10px 0;
            font-size: 14px;
            font-weight: 500;
            transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); /* 更自然的过渡曲线 */
            color: #6b5f59; /* 莫兰迪暖深灰文字,避免刺眼 */
            box-shadow: 0 2px 3px rgba(174, 158, 150, 0.15); /* 暖调阴影,呼应主色 */
        }
        
        /* 莫兰迪色系按钮1 - 灰粉色(全信息复制) */
        .book-copy-btn.full-info {
            background-color: #e4d2cc; /* 低饱和灰粉,柔和不刺眼 */
        }
        .book-copy-btn.full-info:hover {
            background-color: #d1bcb2; /* hover加深10%,保持莫兰迪质感 */
            transform: translateY(-2px);
            box-shadow: 0 4px 6px rgba(174, 158, 150, 0.2);
        }
        
        /* 莫兰迪色系按钮2 - 灰紫色(复制到滴答) */
        .book-copy-btn.dida {
            background-color: #d9d1e0; /* 低饱和灰紫,与灰粉协调互补 */
        }
        .book-copy-btn.dida:hover {
            background-color: #c5bcd6; /* hover加深10%,保持色调统一 */
            transform: translateY(-2px);
            box-shadow: 0 4px 6px rgba(160, 146, 173, 0.2);
        }
        
        /* 按钮交互细节 */
        .book-copy-btn:active {
            transform: translateY(0);
            box-shadow: 0 2px 3px rgba(174, 158, 150, 0.15); /* 点击回归浅阴影 */
        }
        .book-copy-btn:disabled {
            opacity: 0.7;
            cursor: not-allowed;
            transform: none;
            box-shadow: none;
            background-color: #e9e5e2; /* 禁用时统一为浅暖灰,避免色彩混乱 */
        }
        
        /* 状态提示样式(与主色呼应) */
        .copy-status {
            margin-left: 10px;
            font-size: 14px;
            padding: 4px 10px;
            border-radius: 4px;
            transition: all 0.3s ease;
            color: #6b5f59;
            background-color: #f3eee9; /* 基础状态浅暖灰 */
        }
        .copy-success {
            background-color: #f0e6e2; /* 成功状态:浅灰粉底 */
            color: #7d6b62; /* 暖灰文字,提升可读性 */
        }
        .copy-error {
            background-color: #f1e9f0; /* 失败状态:浅灰紫底 */
            color: #7a6d7c; /* 紫调文字,与失败提示呼应 */
        }
        .copy-loading {
            background-color: #f3eee9;
            color: #80746d;
        }
    `);

    // 2. 核心:图书信息提取函数(功能不变,保持原逻辑)
    function getBookInfo() {
        // 提取标题
        const title = $('#wrapper h1 span')
            .first()
            .text()
            .replace(/[:\((].*$/, '')
            .trim() || '未知标题';

        // 提取作者
        let author = '';
        const authorElem = $('#info span.pl:contains("作者")').next();
        if (authorElem.length) {
            author = authorElem.text().replace(/\s+/g, ' ').trim();
        }
        if (!author && authorElem.find('a').length) {
            author = authorElem.find('a').text().replace(/\s+/g, ' ').trim();
        }
        if (!author) {
            const translatorElem = $('#info span.pl:contains("译者")').next();
            if (translatorElem.length) {
                author = translatorElem.text().replace(/\s+/g, ' ').trim();
                author = author ? `译者:${author}` : '';
            }
        }
        author = author || '未知作者';

        // 提取内容简介
        let intro = '';
        const introElem = $('.intro');
        if (introElem.length) {
            intro = introElem.text().replace(/\s+/g, ' ').trim();
        }
        intro = intro || '无内容简介';

        // 通用信息提取函数
        function extractInfo(label) {
            let elem = $(`#info span.pl:contains("${label}")`).next();
            if (elem.length && elem.text().trim()) {
                return elem.text().trim();
            }
            elem = $(`#info span.pl:contains("${label}")`).next('a');
            if (elem.length && elem.text().trim()) {
                return elem.text().trim();
            }
            const text = $('#info').text();
            const match = text.match(new RegExp(`${label}\\s*[::]\\s*([^\\n]+)`));
            if (match && match[1]) {
                return match[1].trim();
            }
            return `未知${label}`;
        }

        // 提取基础信息
        const publisher = extractInfo('出版社');
        const pubYear = extractInfo('出版年');
        const isbn = extractInfo('ISBN');
        const pages = extractInfo('页数');
        const price = extractInfo('定价');
        const binding = extractInfo('装帧');
        const rating = $('.rating_num').text().trim() || '暂无评分';
        const url = window.location.href || '未知链接';

        return {
            title, author, publisher, pubYear, isbn,
            pages, price, binding, rating, url, intro
        };
    }

    // 3. 复制文本格式化函数(保持原格式,适配不同需求)
    function getCopyText(info, type) {
        switch (type) {
            case 'full':
                return `书名:《${info.title}》
作者:${info.author}
出版社:${info.publisher}
出版年:${info.pubYear}
ISBN:${info.isbn}
页数:${info.pages}
定价:${info.price}
装帧:${info.binding}
豆瓣评分:${info.rating}
链接:${info.url}
内容简介:${info.intro}`;
            
            case 'dida':
                return `[《${info.title}》](${info.url}) 🖊:${info.author} ⭐️:${info.rating} 📅:${info.pubYear} 🏢:${info.publisher}`;
            
            default:
                return '';
        }
    }

    // 4. 剪贴板写入函数(兼容原生API和降级方案)
    function copyToClipboard(text) {
        return navigator.clipboard.writeText(text).catch(() => {
            const textarea = $('<textarea>').val(text).appendTo('body');
            textarea[0].select();
            document.execCommand('copy');
            textarea.remove();
        });
    }

    // 5. 按钮点击处理函数(优化状态反馈细节)
    function handleCopyClick(btn, statusElem, copyType) {
        return async function () {
            const originalText = btn.text();
            // 初始加载状态
            btn.text('复制中...').prop('disabled', true);
            statusElem.removeClass().addClass('copy-status copy-loading').text('处理中');

            try {
                const bookInfo = getBookInfo();
                const copyText = getCopyText(bookInfo, copyType);
                await copyToClipboard(copyText);
                
                // 成功状态:增加图标+文字,视觉更清晰
                statusElem.removeClass().addClass('copy-status copy-success').text('👌');
                btn.text('已复制');
            } catch (err) {
                // 失败状态:明确提示,引导重试
                statusElem.removeClass().addClass('copy-status copy-error').text('✗ 复制失败,请重试');
                btn.text(originalText);
            } finally {
                btn.prop('disabled', false);
                // 5秒后恢复初始状态,避免长期占用视觉空间
                setTimeout(() => {
                    statusElem.removeClass().addClass('copy-status').text('');
                    btn.text(originalText);
                }, 5000);
            }
        };
    }

    // 6. 创建按钮容器(整合两个功能按钮,保持布局整洁)
    const $buttonContainer = $('<div class="book-copy-container">').append(
        $('<button class="book-copy-btn full-info">全信息复制</button>'),
        $('<button class="book-copy-btn dida">复制→滴答</button>'),
        $('<span class="copy-status"> </span>')
    ).prependTo('#content .aside');

    // 7. 绑定按钮点击事件(关联对应处理逻辑)
    const $fullBtn = $buttonContainer.find('.book-copy-btn.full-info');
    const $didaBtn = $buttonContainer.find('.book-copy-btn.dida');
    const $status = $buttonContainer.find('.copy-status');

    $fullBtn.on('click', handleCopyClick($fullBtn, $status, 'full'));
    $didaBtn.on('click', handleCopyClick($didaBtn, $status, 'dida'));
});