V2EX Image Uploader

在 V2EX 评论区快速上传图片并插入链接

// ==UserScript==
// @name         V2EX Image Uploader
// @namespace    http://tampermonkey.net/1436051
// @version      1.0
// @description  在 V2EX 评论区快速上传图片并插入链接
// @author       Dogxi
// @match        https://www.v2ex.com/t/*
// @match        https://v2ex.com/t/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=v2ex.com
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      api.imgur.com
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const IMGUR_CLIENT_ID_KEY = 'imgurClientId';
    let CLIENT_ID = GM_getValue(IMGUR_CLIENT_ID_KEY, null);

    const STYLE = `
        .imgur-upload-btn {
            background: none;
            border: none;
            color: #778087;
            cursor: pointer;
            font-size: 13px;
            padding: 0;
            margin-left: 15px;
            text-decoration: none;
            transition: color 0.2s ease;
        }
        .imgur-upload-btn:hover {
            color: #4d5256;
            text-decoration: underline;
        }
        .hidden {
            display: none !important;
        }
        .imgur-upload-modal {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.5);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 9999;
        }
        .imgur-upload-modal-content {
            background-color: #fff;
            padding: 20px;
            border-radius: 3px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
            max-width: 450px;
            width: 90%;
            position: relative;
            font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
        }
        .imgur-upload-modal-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
            padding-bottom: 10px;
            border-bottom: 1px solid #e2e2e2;
        }
        .imgur-upload-modal-title {
            font-size: 15px;
            font-weight: normal;
            color: #000;
        }
        .imgur-upload-modal-close {
            cursor: pointer;
            font-size: 18px;
            color: #ccc;
            width: 20px;
            height: 20px;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: color 0.2s ease;
        }
        .imgur-upload-modal-close:hover {
            color: #999;
        }
        .imgur-upload-dropzone {
            border: 1px dashed #ccc;
            padding: 25px;
            text-align: center;
            margin-bottom: 15px;
            cursor: pointer;
            border-radius: 3px;
            transition: border-color 0.2s ease;
            font-size: 13px;
            color: #666;
        }
        .imgur-upload-dropzone:hover {
            border-color: #999;
        }
        .imgur-upload-dropzone.dragover {
            border-color: #778087;
            background-color: #f9f9f9;
        }
        .imgur-upload-preview {
            margin-top: 10px;
            max-width: 100%;
            max-height: 150px;
            border-radius: 2px;
        }
        .imgur-upload-actions {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-top: 15px;
            padding-top: 10px;
            border-top: 1px solid #e2e2e2;
        }
        .imgur-upload-config-btn {
            background: none;
            border: none;
            color: #778087;
            cursor: pointer;
            font-size: 12px;
            padding: 0;
        }
        .imgur-upload-config-btn:hover {
            color: #4d5256;
            text-decoration: underline;
        }
        .imgur-upload-submit-btn {
            background-color: #f5f5f5;
            border: 1px solid #ccc;
            border-radius: 3px;
            color: #333;
            cursor: pointer;
            font-size: 12px;
            padding: 6px 12px;
            transition: all 0.2s ease;
        }
        .imgur-upload-submit-btn:hover {
            background-color: #e8e8e8;
        }
        .imgur-upload-submit-btn:disabled {
            background-color: #f9f9f9;
            color: #ccc;
            cursor: not-allowed;
        }
        .imgur-upload-config-panel {
            margin-top: 10px;
            padding: 10px;
            background-color: #f9f9f9;
            border-radius: 3px;
            border: 1px solid #e2e2e2;
        }
        .imgur-upload-config-row {
            display: flex;
            align-items: center;
            margin-bottom: 8px;
        }
        .imgur-upload-config-row:last-child {
            margin-bottom: 0;
        }
        .imgur-upload-config-label {
            font-size: 12px;
            color: #666;
            width: 70px;
            flex-shrink: 0;
        }
        .imgur-upload-config-input {
            flex: 1;
            padding: 3px 6px;
            border: 1px solid #ccc;
            border-radius: 2px;
            font-size: 12px;
        }
        .imgur-upload-config-save {
            background-color: #f5f5f5;
            border: 1px solid #ccc;
            border-radius: 2px;
            color: #333;
            cursor: pointer;
            font-size: 11px;
            margin-left: 6px;
            padding: 3px 8px;
        }
        .imgur-upload-config-save:hover {
            background-color: #e8e8e8;
        }
        .imgur-upload-modal-status {
            color: #666;
            font-size: 12px;
            text-align: center;
        }
        .imgur-upload-modal-status.success {
            color: #5cb85c;
        }
        .imgur-upload-modal-status.error {
            color: #d9534f;
        }
    `;

    // 添加样式到页面
    function addStyle() {
        const styleElement = document.createElement('style');
        styleElement.textContent = STYLE;
        document.head.appendChild(styleElement);
    }

    // 创建上传弹窗
    function createUploadModal(textareaElement) {
        const modal = document.createElement('div');
        modal.className = 'imgur-upload-modal';
        
        const content = document.createElement('div');
        content.className = 'imgur-upload-modal-content';
        
        content.innerHTML = `
            <div class="imgur-upload-modal-header">
                <div class="imgur-upload-modal-title">上传图片</div>
                <div class="imgur-upload-modal-close">×</div>
            </div>
            <div class="imgur-upload-dropzone">
                <div>点击选择图片或拖拽图片到此处</div>
                <div style="font-size: 11px; color: #999; margin-top: 5px;">支持 JPG, PNG, GIF 格式</div>
            </div>
            <div class="imgur-upload-actions">
                <button class="imgur-upload-config-btn">⚙️ 配置</button>
                <button class="imgur-upload-submit-btn" disabled>确认上传</button>
            </div>
            <div class="imgur-upload-config-panel hidden">
                <div class="imgur-upload-config-row">
                    <div class="imgur-upload-config-label">Imgur ID:</div>
                    <input type="text" class="imgur-upload-config-input" placeholder="请输入 Imgur Client ID" value="${CLIENT_ID || ''}">
                    <button class="imgur-upload-config-save">保存</button>
                </div>
                <div style="font-size: 11px; color: #666; margin-top: 8px;">
                    在 <a href="https://api.imgur.com/oauth2/addclient" target="_blank">https://api.imgur.com/oauth2/addclient</a> 注册获取(无回调)
                </div>
            </div>
        `;
        
        modal.appendChild(content);
        document.body.appendChild(modal);
        
        setupModalEvents(modal, textareaElement);
        
        return modal;
    }

    // 设置弹窗事件监听
    function setupModalEvents(modal, textareaElement) {
        const closeBtn = modal.querySelector('.imgur-upload-modal-close');
        const dropzone = modal.querySelector('.imgur-upload-dropzone');
        const configBtn = modal.querySelector('.imgur-upload-config-btn');
        const configPanel = modal.querySelector('.imgur-upload-config-panel');
        const configInput = modal.querySelector('.imgur-upload-config-input');
        const configSave = modal.querySelector('.imgur-upload-config-save');
        const submitBtn = modal.querySelector('.imgur-upload-submit-btn');
        
        let selectedFile = null;
        
        function closeModal() {
            document.body.removeChild(modal);
        }
        
        closeBtn.addEventListener('click', closeModal);
        modal.addEventListener('click', function(e) {
            if (e.target === modal) closeModal();
        });
        
        configBtn.addEventListener('click', function() {
            configPanel.classList.toggle('hidden');
        });
        
        configSave.addEventListener('click', function() {
            const newClientId = configInput.value.trim();
            if (newClientId) {
                GM_setValue(IMGUR_CLIENT_ID_KEY, newClientId);
                CLIENT_ID = newClientId;
                configPanel.classList.add('hidden');
                showStatusInModal(modal, '配置已保存', 'success');
            }
        });
        
        const fileInput = document.createElement('input');
        fileInput.type = 'file';
        fileInput.accept = 'image/*';
        fileInput.style.display = 'none';
        modal.appendChild(fileInput);
        
        dropzone.addEventListener('click', () => fileInput.click());
        
        fileInput.addEventListener('change', function(e) {
            handleFileSelect(e.target.files[0]);
        });
        
        dropzone.addEventListener('dragover', function(e) {
            e.preventDefault();
            dropzone.classList.add('dragover');
        });
        
        dropzone.addEventListener('dragleave', function(e) {
            e.preventDefault();
            dropzone.classList.remove('dragover');
        });
        
        dropzone.addEventListener('drop', function(e) {
            e.preventDefault();
            dropzone.classList.remove('dragover');
            const files = e.dataTransfer.files;
            if (files.length > 0) {
                handleFileSelect(files[0]);
            }
        });
        
        // 处理文件选择
        function handleFileSelect(file) {
            if (!file || !file.type.match(/image\/.*/)) {
                showStatusInModal(modal, '请选择图片文件', 'error');
                return;
            }
            
            selectedFile = file;
            
            const reader = new FileReader();
            reader.onload = function(e) {
                const preview = modal.querySelector('.imgur-upload-preview');
                if (preview) preview.remove();
                
                const img = document.createElement('img');
                img.src = e.target.result;
                img.className = 'imgur-upload-preview';
                dropzone.appendChild(img);
                
                submitBtn.disabled = false;
                dropzone.querySelector('div').textContent = '已选择: ' + file.name;
            };
            reader.readAsDataURL(file);
        }
        
        submitBtn.addEventListener('click', function() {
            if (!selectedFile) return;
            
            if (!CLIENT_ID) {
                showStatusInModal(modal, '请先配置 Imgur Client ID', 'error');
                configPanel.classList.remove('hidden');
                return;
            }
            
            submitBtn.disabled = true;
            submitBtn.textContent = '上传中...';
            
            uploadToImgur(selectedFile, textareaElement, modal);
        });
    }

    // 在弹窗中显示状态信息
    function showStatusInModal(modal, message, type) {
        let statusEl = modal.querySelector('.imgur-upload-modal-status');
        if (!statusEl) {
            statusEl = document.createElement('div');
            statusEl.className = 'imgur-upload-modal-status';
            statusEl.style.cssText = 'margin-top: 10px; font-size: 12px; text-align: center;';
            modal.querySelector('.imgur-upload-modal-content').appendChild(statusEl);
        }
        
        statusEl.textContent = message;
        statusEl.className = 'imgur-upload-modal-status ' + (type || '');
        
        if (type === 'success') {
            setTimeout(() => statusEl.textContent = '', 3000);
        }
    }

    // 上传图片到 Imgur
    function uploadToImgur(file, textareaElement, modal) {
        if (!file.type.match(/image\/.*/)) {
            showStatusInModal(modal, '请选择图片文件', 'error');
            const submitBtn = modal.querySelector('.imgur-upload-submit-btn');
            submitBtn.disabled = false;
            submitBtn.textContent = '确认上传';
            return;
        }
        
        const formData = new FormData();
        formData.append('image', file);
        
        GM_xmlhttpRequest({
            method: "POST",
            url: "https://api.imgur.com/3/image",
            headers: {
                "Authorization": "Client-ID " + CLIENT_ID
            },
            data: formData,
            responseType: "json",
            onload: function(response) {
                const submitBtn = modal.querySelector('.imgur-upload-submit-btn');
                
                try {
                    let responseData;
                    if (typeof response.response === 'string') {
                        responseData = JSON.parse(response.response);
                    } else {
                        responseData = response.response;
                    }
                    
                    if (response.status === 200 && responseData && responseData.success) {
                        const imageUrl = responseData.data.link;
                        insertLinkIntoTextarea(textareaElement, imageUrl, file.name);
                        showStatusInModal(modal, '上传成功!', 'success');
                        
                        setTimeout(() => {
                            document.body.removeChild(modal);
                        }, 1500);
                    } else {
                        let errorMessage = '';
                        
                        if (response.status === 400) {
                            if (responseData && responseData.data && responseData.data.error) {
                                if (responseData.data.error === 'These actions are forbidden.') {
                                    errorMessage = 'Client ID 无效或已被禁用,请检查配置';
                                } else {
                                    errorMessage = responseData.data.error;
                                }
                            } else {
                                errorMessage = 'Client ID 配置错误';
                            }
                        } else if (response.status === 403) {
                            errorMessage = '访问被拒绝,请检查 Client ID 权限';
                        } else if (response.status === 429) {
                            errorMessage = '请求过于频繁,请稍后再试';
                        } else {
                            errorMessage = `上传失败 (${response.status})`;
                        }
                        
                        console.error('Imgur 上传错误:', response);
                        showStatusInModal(modal, errorMessage, 'error');
                        
                        if (response.status === 400 || response.status === 403) {
                            const configPanel = modal.querySelector('.imgur-upload-config-panel');
                            configPanel.classList.remove('hidden');
                        }
                        
                        submitBtn.disabled = false;
                        submitBtn.textContent = '确认上传';
                    }
                } catch (e) {
                    console.error('解析响应失败:', e, response);
                    showStatusInModal(modal, '响应解析失败,请重试', 'error');
                    
                    submitBtn.disabled = false;
                    submitBtn.textContent = '确认上传';
                }
            },
            onerror: function(error) {
                console.error('GM_xmlhttpRequest 错误:', error);
                showStatusInModal(modal, '网络请求失败,请检查连接', 'error');
                
                const submitBtn = modal.querySelector('.imgur-upload-submit-btn');
                submitBtn.disabled = false;
                submitBtn.textContent = '确认上传';
            },
            ontimeout: function() {
                console.error('Imgur 上传超时');
                showStatusInModal(modal, '上传超时,请重试', 'error');
                
                const submitBtn = modal.querySelector('.imgur-upload-submit-btn');
                submitBtn.disabled = false;
                submitBtn.textContent = '确认上传';
            }
        });
    }

    // 将图片链接插入到文本框
    function insertLinkIntoTextarea(textareaElement, imageUrl, fileName) {
        const altText = fileName ? fileName.split('.')[0] : 'image';
        const textToInsert = imageUrl;
        
        const currentValue = textareaElement.value;
        const selectionStart = textareaElement.selectionStart;
        const selectionEnd = textareaElement.selectionEnd;
        
        const newText = currentValue.substring(0, selectionStart) + textToInsert + currentValue.substring(selectionEnd);
        textareaElement.value = newText;
        
        const newCursorPosition = selectionStart + textToInsert.length;
        textareaElement.selectionStart = newCursorPosition;
        textareaElement.selectionEnd = newCursorPosition;
        
        textareaElement.focus();
        textareaElement.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
    }

    // 在页面头部添加上传按钮
    function addUploadButtonToHeader() {
        const replyBox = document.getElementById('reply-box');
        if (!replyBox) return;
        
        const headerCell = replyBox.querySelector('.cell.flex-one-row');
        if (!headerCell) return;
        
        if (headerCell.querySelector('.imgur-upload-btn')) return;
        
        const leftDiv = headerCell.querySelector('div:first-child');
        if (leftDiv) {
            const uploadBtn = document.createElement('a');
            uploadBtn.className = 'imgur-upload-btn';
            uploadBtn.textContent = '上传';
            uploadBtn.href = 'javascript:void(0);';
            uploadBtn.title = '上传图片';
            uploadBtn.style.marginLeft = '10px';
            
            leftDiv.appendChild(uploadBtn);
            
            uploadBtn.addEventListener('click', function(e) {
                e.preventDefault();
                const textarea = document.getElementById('reply_content');
                if (textarea) {
                    createUploadModal(textarea);
                }
            });
        }
    }

    // 查找并添加上传按钮
    function findTextareasAndAddButtons() {
        addUploadButtonToHeader();
        setupMutationObserver();
    }

    // 监听DOM变化
    function setupMutationObserver() {
        const observer = new MutationObserver(function(mutations) {
            let shouldCheck = false;
            mutations.forEach(function(mutation) {
                mutation.addedNodes.forEach(function(node) {
                    if (node.nodeType === Node.ELEMENT_NODE && 
                        (node.id === 'reply-box' || node.querySelector('#reply-box'))) {
                        shouldCheck = true;
                    }
                });
            });
            
            if (shouldCheck) {
                setTimeout(addUploadButtonToHeader, 100);
            }
        });
        
        observer.observe(document.body, { 
            childList: true, 
            subtree: true 
        });
    }

    // 初始化脚本
    function init() {
        addStyle();
        setTimeout(findTextareasAndAddButtons, 100);
    }

    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", init);
    } else {
        init();
    }
})();