US Card Forum 全文下载器

在 uscardforum.com 帖子页面右下角添加一个按钮,用于下载整个话题的 Markdown 全文。能正确处理 .../topic_id/post_id 格式的URL。

// ==UserScript==
// @name         US Card Forum 全文下载器
// @name:en      US Card Forum Full Topic Downloader
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  在 uscardforum.com 帖子页面右下角添加一个按钮,用于下载整个话题的 Markdown 全文。能正确处理 .../topic_id/post_id 格式的URL。
// @description:en Adds a button to the bottom right of uscardforum.com topic pages to download the entire topic as a single Markdown file. Correctly handles .../topic_id/post_id URLs.
// @author       Gemini
// @match        https://www.uscardforum.com/t/*/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- 配置 ---
    const BUTTON_STYLE = {
        position: 'fixed',
        bottom: '20px',
        right: '20px',
        zIndex: '9999',
        backgroundColor: '#313131',
        color: '#A6A6A6',
        border: 'none',
        padding: '10px 20px',
        borderRadius: '5px',
        cursor: 'pointer',
        fontSize: '14px',
        fontFamily: 'sans-serif',
        boxShadow: '0 2px 5px rgba(0,0,0,0.2)'
    };

    const BUTTON_TEXT = '下载全文';
    const BUTTON_TEXT_LOADING = '下载中...';
    const BUTTON_TEXT_DONE = '下载完成';

    // --- 主逻辑 ---

    /**
     * 从当前 URL 中提取 topic_id (已修正)
     * @returns {string|null} topic_id 或 null
     */
    function getTopicId() {
        // 修正正则表达式以匹配 /t/slug/<topic_id> 结构
        // 这样可以避免捕获末尾的 post_id
        const match = window.location.pathname.match(/\/t\/[^\/]+\/(\d+)/);
        return match ? match[1] : null;
    }

    /**
     * 从页面标题中提取并清理话题名称
     * @returns {string} 清理后的话题名
     */
    function getTopicName() {
        // 例如: "这是一个话题标题 - 美国信用卡指南" -> "这是一个话题标题"
        const title = document.title.replace(/\s-\s(美国信用卡指南|US Card Forum)$/, '').trim();
        // 替换在文件名中非法的字符
        return title.replace(/[\\/:*?"<>|]/g, '_');
    }

    /**
     * 触发文件下载
     * @param {string} filename - 下载的文件名
     * @param {string} content - 文件内容
     */
    function downloadFile(filename, content) {
        const blob = new Blob([content], { type: 'text/markdown;charset=utf-8;' });
        const link = document.createElement('a');
        link.href = URL.createObjectURL(blob);
        link.setAttribute('download', filename);
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
        URL.revokeObjectURL(link.href);
    }

    /**
     * 递归获取所有页面的 Markdown 内容
     * @param {string} topicId - 话题 ID
     * @param {number} page - 当前要获取的页码
     * @param {string[]} accumulatedContent - 已累积的内容数组
     * @returns {Promise<string>} 拼接后的完整内容
     */
    async function fetchAllPages(topicId, page = 1, accumulatedContent = []) {
        const url = `https://www.uscardforum.com/raw/${topicId}?page=${page}`;
        try {
            const response = await fetch(url, { credentials: 'include' });
            if (!response.ok) {
                throw new Error(`网络请求失败: ${response.status} ${response.statusText}`);
            }
            const text = await response.text();
            if (text && text.trim().length > 0) {
                accumulatedContent.push(text);
                // 为了避免请求过于频繁,可以加入一个小的延时
                await new Promise(resolve => setTimeout(resolve, 100));
                return fetchAllPages(topicId, page + 1, accumulatedContent);
            } else {
                return accumulatedContent.join('\n\n---\n\n'); // 使用分隔符拼接不同页面的内容
            }
        } catch (error) {
            console.error(`获取第 ${page} 页内容时出错:`, error);
            alert(`获取第 ${page} 页内容时出错,请检查控制台获取更多信息。`);
            return null; // 返回 null 表示失败
        }
    }


    /**
     * 按钮点击事件处理函数
     * @param {Event} event - 点击事件对象
     */
    async function handleDownloadClick(event) {
        const button = event.target;
        const topicId = getTopicId();
        const topicName = getTopicName();

        if (!topicId || !topicName) {
            alert('无法从此页面提取话题 ID 或标题。');
            return;
        }

        button.textContent = BUTTON_TEXT_LOADING;
        button.disabled = true;

        const fullMarkdown = await fetchAllPages(topicId);

        if (fullMarkdown !== null) {
            downloadFile(`${topicName}.md`, fullMarkdown);
            button.textContent = BUTTON_TEXT_DONE;
        } else {
            button.textContent = '下载失败';
        }

        // 几秒后恢复按钮状态
        setTimeout(() => {
            button.textContent = BUTTON_TEXT;
            button.disabled = false;
        }, 3000);
    }

    /**
     * 创建并显示下载按钮
     */
    function createDownloadButton() {
        if (!getTopicId()) {
            console.log('当前页面不是一个有效的话题页面,不加载下载按钮。');
            return;
        }

        const button = document.createElement('button');
        button.textContent = BUTTON_TEXT;
        Object.assign(button.style, BUTTON_STYLE);

        button.addEventListener('click', handleDownloadClick);

        document.body.appendChild(button);
    }

    // --- 启动脚本 ---
    // 等待页面加载完成再执行
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', createDownloadButton);
    } else {
        createDownloadButton();
    }

})();