Greasy Fork

来自缓存

Greasy Fork is available in English.

Linux.do 下崽器 (新版)

备份你珍贵的水贴为Markdown,包含图片下载,可拖拽调整按钮位置。

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Linux.do 下崽器 (新版)
// @namespace    http://linux.do/
// @version      2.0.0
// @description  备份你珍贵的水贴为Markdown,包含图片下载,可拖拽调整按钮位置。
// @author       PastKing
// @match        https://www.linux.do/t/topic/*
// @match        https://linux.do/t/topic/*
// @license      MIT
// @icon         https://cdn.linux.do/uploads/default/optimized/1X/3a18b4b0da3e8cf96f7eea15241c3d251f28a39b_2_32x32.png
// @grant        none
// @require      https://unpkg.com/[email protected]/dist/turndown.js
// @require      https://unpkg.com/[email protected]/dist/jszip.min.js
// ==/UserScript==

(function() {
    'use strict';

    let isDragging = false;
    let isMouseDown = false;

    // 创建并插入下载按钮
    function createDownloadButton() {
        const button = document.createElement('button');
        button.innerHTML = `
            <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="margin-right: 6px; vertical-align: middle;">
                <path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
            </svg>
            下载为压缩包
        `;
        button.style.cssText = `
            padding: 12px 20px;
            font-size: 14px;
            font-weight: 600;
            color: #ffffff;
            background: linear-gradient(135deg, #0088cc 0%, #0099dd 100%);
            border: none;
            border-radius: 8px;
            cursor: pointer;
            transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
            box-shadow: 0 4px 12px rgba(0, 136, 204, 0.3);
            position: fixed;
            z-index: 9999;
            display: flex;
            align-items: center;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
            user-select: none;
            backdrop-filter: blur(10px);
            border: 1px solid rgba(255, 255, 255, 0.2);
        `;

        // 添加悬停效果
        button.onmouseover = function() {
            this.style.background = 'linear-gradient(135deg, #0077bb 0%, #0088cc 100%)';
            this.style.transform = 'translateY(-2px)';
            this.style.boxShadow = '0 6px 20px rgba(0, 136, 204, 0.4)';
        };
        button.onmouseout = function() {
            this.style.background = 'linear-gradient(135deg, #0088cc 0%, #0099dd 100%)';
            this.style.transform = 'translateY(0)';
            this.style.boxShadow = '0 4px 12px rgba(0, 136, 204, 0.3)';
        };

        // 添加点击效果
        button.onmousedown = function() {
            this.style.transform = 'translateY(0) scale(0.98)';
        };
        button.onmouseup = function() {
            this.style.transform = 'translateY(-2px) scale(1)';
        };

        // 从localStorage获取保存的位置
        const savedPosition = JSON.parse(localStorage.getItem('downloadButtonPosition'));
        if (savedPosition) {
            button.style.left = savedPosition.left;
            button.style.top = savedPosition.top;
        } else {
            button.style.right = '20px';
            button.style.bottom = '20px';
        }

        document.body.appendChild(button);

        return button;
    }

    // 创建进度提示组件
    function createProgressToast(message) {
        // 移除已存在的提示
        const existingToast = document.getElementById('download-progress-toast');
        if (existingToast) {
            existingToast.remove();
        }

        const toast = document.createElement('div');
        toast.id = 'download-progress-toast';
        toast.innerHTML = `
            <div style="display: flex; align-items: center;">
                <div style="
                    width: 20px;
                    height: 20px;
                    border: 2px solid #ffffff;
                    border-top: 2px solid transparent;
                    border-radius: 50%;
                    animation: spin 1s linear infinite;
                    margin-right: 10px;
                "></div>
                <span style="color: #ffffff; font-weight: 500;">${message}</span>
            </div>
        `;
        toast.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            background: linear-gradient(135deg, #0088cc 0%, #0099dd 100%);
            color: white;
            padding: 16px 24px;
            border-radius: 8px;
            box-shadow: 0 6px 20px rgba(0, 136, 204, 0.4);
            z-index: 10000;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
            font-size: 14px;
            backdrop-filter: blur(10px);
            border: 1px solid rgba(255, 255, 255, 0.2);
            transform: translateX(100%);
            transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
        `;

        // 添加旋转动画
        const style = document.createElement('style');
        style.textContent = `
            @keyframes spin {
                0% { transform: rotate(0deg); }
                100% { transform: rotate(360deg); }
            }
        `;
        document.head.appendChild(style);

        document.body.appendChild(toast);

        // 显示动画
        setTimeout(() => {
            toast.style.transform = 'translateX(0)';
        }, 100);

        return toast;
    }

    // 移除进度提示
    function removeProgressToast() {
        const toast = document.getElementById('download-progress-toast');
        if (toast) {
            toast.style.transform = 'translateX(100%)';
            setTimeout(() => {
                toast.remove();
            }, 300);
        }
    }

    // 显示成功提示
    function showSuccessToast(message) {
        const toast = document.createElement('div');
        toast.innerHTML = `
            <div style="display: flex; align-items: center;">
                <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" style="margin-right: 10px; color: #ffffff;">
                    <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
                </svg>
                <span style="color: #ffffff; font-weight: 500;">${message}</span>
            </div>
        `;
        toast.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            background: linear-gradient(135deg, #28a745 0%, #34ce57 100%);
            color: white;
            padding: 16px 24px;
            border-radius: 8px;
            box-shadow: 0 6px 20px rgba(40, 167, 69, 0.4);
            z-index: 10000;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
            font-size: 14px;
            backdrop-filter: blur(10px);
            border: 1px solid rgba(255, 255, 255, 0.2);
            transform: translateX(100%);
            transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
        `;

        document.body.appendChild(toast);

        // 显示动画
        setTimeout(() => {
            toast.style.transform = 'translateX(0)';
        }, 100);

        // 自动隐藏
        setTimeout(() => {
            toast.style.transform = 'translateX(100%)';
            setTimeout(() => {
                toast.remove();
            }, 300);
        }, 3000);
    }

    // 添加拖拽功能
    function makeDraggable(element) {
        let startX, startY, startLeft, startTop;

        element.addEventListener('mousedown', startDragging);
        document.addEventListener('mousemove', drag);
        document.addEventListener('mouseup', stopDragging);

        function startDragging(e) {
            isMouseDown = true;
            isDragging = false;
            startX = e.clientX;
            startY = e.clientY;
            startLeft = parseInt(element.style.left) || window.innerWidth - parseInt(element.style.right) - element.offsetWidth;
            startTop = parseInt(element.style.top) || window.innerHeight - parseInt(element.style.bottom) - element.offsetHeight;
            e.preventDefault();
        }

        function drag(e) {
            if (!isMouseDown) return;
            isDragging = true;
            const dx = e.clientX - startX;
            const dy = e.clientY - startY;
            element.style.left = `${startLeft + dx}px`;
            element.style.top = `${startTop + dy}px`;
            element.style.right = 'auto';
            element.style.bottom = 'auto';
        }

        function stopDragging() {
            if (isMouseDown && isDragging) {
                localStorage.setItem('downloadButtonPosition', JSON.stringify({
                    left: element.style.left,
                    top: element.style.top
                }));
            }
            isMouseDown = false;
            setTimeout(() => {
                isDragging = false;
            }, 10);
        }
    }

    // 下载图片
    async function downloadImage(url) {
        try {
            const response = await fetch(url);
            if (!response.ok) {
                throw new Error(`HTTP错误! 状态: ${response.status}`);
            }
            return await response.blob();
        } catch (error) {
            console.warn(`下载图片失败: ${url}`, error);
            return null;
        }
    }

    // 获取文件扩展名
    function getFileExtension(url) {
        try {
            const urlObj = new URL(url);
            const pathname = urlObj.pathname;
            const lastDot = pathname.lastIndexOf('.');
            if (lastDot !== -1) {
                return pathname.slice(lastDot);
            }
        } catch (e) {
            // 忽略URL解析错误
        }
        return '.jpg'; // 默认扩展名
    }

    // 生成安全的文件名
    function generateSafeFileName(originalName, index) {
        // 移除特殊字符
        const safeName = originalName.replace(/[<>:"/\\|?*]/g, '_');
        return `image_${index}_${safeName}`;
    }

    // 提取所有图片URL
    function extractImageUrls(htmlContent) {
        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = htmlContent;
        const images = tempDiv.querySelectorAll('img');
        const imageUrls = [];

        images.forEach((img, index) => {
            const src = img.getAttribute('src');
            if (src) {
                // 处理相对URL
                const absoluteUrl = new URL(src, window.location.href).href;
                const alt = img.getAttribute('alt') || `image_${index}`;
                const extension = getFileExtension(absoluteUrl);
                const fileName = generateSafeFileName(alt + extension, index);

                imageUrls.push({
                    originalUrl: src,
                    absoluteUrl: absoluteUrl,
                    fileName: fileName,
                    alt: alt
                });
            }
        });

        return imageUrls;
    }

    // 获取文章内容
    function getArticleContent() {
        const titleElement = document.querySelector('#topic-title > div > h1');
        const contentElement = document.querySelector('#post_1 > div.row > div.topic-body.clearfix > div.regular.contents > div.cooked');

        if (!titleElement) {
            console.error('无法找到文章标题');
            return null;
        }

        if (!contentElement) {
            console.error('无法找到文章内容');
            return null;
        }

        return {
            title: titleElement.textContent.trim(),
            content: contentElement.innerHTML
        };
    }

    // 转换为Markdown并下载(包含图片)
    async function downloadAsMarkdown() {
        const article = getArticleContent();
        if (!article) {
            alert('无法获取文章内容,请检查网页结构是否变更。');
            return;
        }

        const button = document.querySelector('button');
        const originalButtonHTML = button.innerHTML;

        // 禁用按钮并显示加载状态
        button.disabled = true;
        button.style.opacity = '0.7';
        button.style.cursor = 'not-allowed';

        try {
            // 提取所有图片URL
            const imageUrls = extractImageUrls(article.content);

            let toast = createProgressToast('准备下载...');

            // 创建ZIP文件
            const zip = new JSZip();
            const imagesFolder = zip.folder("images");

            // 下载所有图片
            const imageDownloadPromises = imageUrls.map(async (imageInfo, index) => {
                toast.querySelector('span').textContent = `下载图片 ${index + 1}/${imageUrls.length}...`;
                const imageBlob = await downloadImage(imageInfo.absoluteUrl);
                if (imageBlob) {
                    imagesFolder.file(imageInfo.fileName, imageBlob);
                    return {
                        ...imageInfo,
                        downloaded: true
                    };
                }
                return {
                    ...imageInfo,
                    downloaded: false
                };
            });

            const downloadedImages = await Promise.all(imageDownloadPromises);

            // 创建图片映射
            const imageMap = new Map();
            downloadedImages.forEach(img => {
                if (img.downloaded) {
                    imageMap.set(img.originalUrl, `images/${img.fileName}`);
                }
            });

            toast.querySelector('span').textContent = '生成Markdown...';

            // 配置TurndownService
            const turndownService = new TurndownService({
                headingStyle: 'atx',
                codeBlockStyle: 'fenced'
            });

            // 自定义规则处理代码块
            turndownService.addRule('codeBlocks', {
                filter: function (node) {
                    return node.nodeName === 'PRE' && node.querySelector('code');
                },
                replacement: function (content, node) {
                    const codeElement = node.querySelector('code');
                    const codeContent = codeElement ? codeElement.textContent : node.textContent;
                    const language = codeElement ? (codeElement.className.match(/language-(\w+)/) || ['', ''])[1] : '';
                    return `\n\`\`\`${language}\n${codeContent}\n\`\`\`\n`;
                }
            });

            // 处理单独的code标签
            turndownService.addRule('standaloneCode', {
                filter: function (node) {
                    return node.nodeName === 'CODE' &&
                           node.parentNode &&
                           node.parentNode.nodeName !== 'PRE' &&
                           (node.textContent.includes('\n') || node.textContent.length > 50);
                },
                replacement: function (content, node) {
                    const codeContent = node.textContent;
                    const language = (node.className.match(/language-(\w+)/) || ['', ''])[1];
                    return `\n\`\`\`${language}\n${codeContent}\n\`\`\`\n`;
                }
            });

            // 自定义规则处理图片和链接
            turndownService.addRule('images_and_links', {
                filter: ['a', 'img'],
                replacement: function (content, node) {
                    // 处理图片
                    if (node.nodeName === 'IMG') {
                        const alt = node.alt || '';
                        const src = node.getAttribute('src') || '';
                        const title = node.title ? ` "${node.title}"` : '';

                        // 使用本地路径(如果图片已下载)
                        const localPath = imageMap.get(src) || src;
                        return `![${alt}](${localPath}${title})`;
                    }
                    // 处理链接
                    else if (node.nodeName === 'A') {
                        const href = node.getAttribute('href');
                        const title = node.title ? ` "${node.title}"` : '';
                        // 检查链接是否包含图片
                        const img = node.querySelector('img');
                        if (img) {
                            const alt = img.alt || '';
                            const src = img.getAttribute('src') || '';
                            const imgTitle = img.title ? ` "${img.title}"` : '';
                            const localPath = imageMap.get(src) || src;
                            return `[![${alt}](${localPath}${imgTitle})](${href}${title})`;
                        }
                        // 普通链接
                        return `[${node.textContent}](${href}${title})`;
                    }
                }
            });

            const markdown = `# ${article.title}\n\n${turndownService.turndown(article.content)}`;

            // 添加Markdown文件到ZIP
            zip.file(`${article.title}.md`, markdown);

            toast.querySelector('span').textContent = '生成压缩包...';
            const zipBlob = await zip.generateAsync({type: "blob"});

            // 下载ZIP文件
            const url = URL.createObjectURL(zipBlob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `${article.title}.zip`;
            a.click();
            URL.revokeObjectURL(url);

            // 移除进度提示
            removeProgressToast();

            // 显示成功消息
            const downloadedCount = downloadedImages.filter(img => img.downloaded).length;
            const totalCount = imageUrls.length;
            showSuccessToast(`下载完成!成功下载图片: ${downloadedCount}/${totalCount}`);

        } catch (error) {
            console.error('下载过程中出现错误:', error);
            removeProgressToast();
            alert('下载过程中出现错误,请查看控制台了解详情。');
        } finally {
            // 恢复按钮状态
            button.innerHTML = originalButtonHTML;
            button.disabled = false;
            button.style.opacity = '1';
            button.style.cursor = 'pointer';
        }
    }

    // 主函数
    function main() {
        const downloadButton = createDownloadButton();
        makeDraggable(downloadButton);
        downloadButton.addEventListener('click', function(e) {
            if (!isDragging) {
                downloadAsMarkdown();
            }
        });

        // 监听DOM变化,确保按钮始终存在
        const observer = new MutationObserver(function(mutations) {
            if (!document.body.contains(downloadButton)) {
                document.body.appendChild(downloadButton);
            }
        });

        // 观察document.body的子节点变化
        observer.observe(document.body, { childList: true, subtree: true });

        // 处理页面可见性变化
        document.addEventListener('visibilitychange', function() {
            if (document.visibilityState === 'visible' && !document.body.contains(downloadButton)) {
                document.body.appendChild(downloadButton);
            }
        });
    }

    // 运行主函数
    main();
})();