NS 简单图床快捷上传

在 Nodeseek.com 的发送评论按钮旁添加图片上传功能,支持粘贴/拖拽上传,提示在右下角

目前為 2025-03-10 提交的版本,檢視 最新版本

// ==UserScript==
// @name         NS 简单图床快捷上传
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  在 Nodeseek.com 的发送评论按钮旁添加图片上传功能,支持粘贴/拖拽上传,提示在右下角
// @author       BreezeZhang
// @match        https://*.nodeseek.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        unsafeWindow
// @connect      nodeseek.com
// @license      GPL-3.0
// ==/UserScript==

(function() {
    'use strict';

    let EASYIMAGE_API_URL = GM_getValue('EASYIMAGE_API_URL', '');
    let EASYIMAGE_TOKEN = GM_getValue('EASYIMAGE_TOKEN', '');

    if (!EASYIMAGE_API_URL || !EASYIMAGE_TOKEN) {
        showConfigModal();
        return;
    }

    initScript();

    // 提取域名(仅用于日志和提示)
    function extractDomain(url) {
        try {
            const urlObj = new URL(url);
            return urlObj.hostname;
        } catch (e) {
            console.error('无效的 API URL:', url, e);
            return '';
        }
    }

    function showConfigModal(isUpdate = false) {
        const modal = document.createElement('div');
        modal.style.cssText = '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:1000;';

        const modalContent = document.createElement('div');
        modalContent.style.cssText = 'background-color:white;padding:20px;border-radius:5px;width:300px;box-shadow:0 0 10px rgba(0,0,0,0.5);';

        const title = document.createElement('h3');
        title.textContent = isUpdate ? '更新配置' : '初始配置';
        title.style.cssText = 'margin-bottom:15px;text-align:center';

        const apiLabel = document.createElement('label');
        apiLabel.textContent = 'EasyImages2.0 API URL: ';
        apiLabel.style.cssText = 'display:block;margin-bottom:5px';
        const apiInput = document.createElement('input');
        apiInput.type = 'text';
        apiInput.value = EASYIMAGE_API_URL || 'https://example.com/api/index.php';
        apiInput.style.cssText = 'width:100%;padding:5px;margin-bottom:10px';

        const tokenLabel = document.createElement('label');
        tokenLabel.textContent = 'EasyImages2.0 Token: ';
        tokenLabel.style.cssText = 'display:block;margin-bottom:5px';
        const tokenInput = document.createElement('input');
        tokenInput.type = 'text';
        tokenInput.value = EASYIMAGE_TOKEN || '';
        tokenInput.style.cssText = 'width:100%;padding:5px;margin-bottom:10px';

        const buttonContainer = document.createElement('div');
        buttonContainer.style.cssText = 'text-align:right';

        const confirmBtn = document.createElement('button');
        confirmBtn.textContent = '保存';
        confirmBtn.style.cssText = 'padding:5px 15px;background-color:#4CAF50;color:white;border:none;border-radius:3px;cursor:pointer;margin-right:10px';
        confirmBtn.addEventListener('click', () => {
            const apiUrl = apiInput.value.trim();
            const token = tokenInput.value.trim();
            if (!apiUrl || !token) {
                alert('API URL 和 Token 不能为空!');
                return;
            }
            EASYIMAGE_API_URL = apiUrl;
            EASYIMAGE_TOKEN = token;
            GM_setValue('EASYIMAGE_API_URL', EASYIMAGE_API_URL);
            GM_setValue('EASYIMAGE_TOKEN', EASYIMAGE_TOKEN);
            document.body.removeChild(modal);
            if (isUpdate) alert('配置更新成功!');
            initScript();
        });

        const cancelBtn = document.createElement('button');
        cancelBtn.textContent = '取消';
        cancelBtn.style.cssText = 'padding:5px 15px;background-color:#f44336;color:white;border:none;border-radius:3px;cursor:pointer';
        cancelBtn.addEventListener('click', () => {
            document.body.removeChild(modal);
            if (!isUpdate) alert('配置未完成,脚本将无法运行!');
        });

        buttonContainer.appendChild(confirmBtn);
        buttonContainer.appendChild(cancelBtn);
        modalContent.appendChild(title);
        modalContent.appendChild(apiLabel);
        modalContent.appendChild(apiInput);
        modalContent.appendChild(tokenLabel);
        modalContent.appendChild(tokenInput);
        modalContent.appendChild(buttonContainer);
        modal.appendChild(modalContent);
        document.body.appendChild(modal);

        modal.addEventListener('click', (e) => {
            if (e.target === modal) return;
            e.stopPropagation();
        });
    }

    function initScript() {
        createUploadButton();
        setupPasteAndDrop();
        addPlaceholder();

        const observer = new MutationObserver(() => {
            createUploadButton();
            setupPasteAndDrop();
            addPlaceholder();
        });

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

    function createUploadButton() {
        const topicSelect = document.querySelector('.topic-select');
        if (!topicSelect || topicSelect.querySelector('.easyimage-upload-container')) return;

        const submitBtn = topicSelect.querySelector('.submit.btn');
        if (!submitBtn) return;

        const container = document.createElement('div');
        container.className = 'easyimage-upload-container';
        container.style.cssText = 'margin:5px 0;display:inline-block';

        const uploadButton = document.createElement('button');
        uploadButton.textContent = '📷 上传图片';
        uploadButton.style.cssText = 'padding:5px 10px;background-color:#4CAF50;color:white;border:none;border-radius:3px;cursor:pointer;margin-right:10px';

        const updateButton = document.createElement('button');
        updateButton.textContent = '⚙️ 更新配置';
        updateButton.style.cssText = 'padding:5px 10px;background-color:#2196F3;color:white;border:none;border-radius:3px;cursor:pointer;margin-right:10px';

        const fileInput = document.createElement('input');
        fileInput.type = 'file';
        fileInput.accept = 'image/*';
        fileInput.style.display = 'none';

        container.appendChild(uploadButton);
        container.appendChild(updateButton);
        container.appendChild(fileInput);

        submitBtn.parentElement.insertBefore(container, submitBtn);

        uploadButton.addEventListener('click', () => fileInput.click());

        fileInput.addEventListener('change', () => {
            if (fileInput.files.length === 0) return;
            const file = fileInput.files[0];
            uploadButton.textContent = '正在上传...';
            uploadButton.disabled = true;
            uploadImage(file, (success, result) => {
                uploadButton.textContent = '📷 上传图片';
                uploadButton.disabled = false;
                if (success) insertMarkdown(result);
                else alert('上传失败:' + result);
            });
        });

        updateButton.addEventListener('click', () => showConfigModal(true));
    }

    function setupPasteAndDrop() {
        const editor = document.querySelector('.CodeMirror');
        if (!editor) return;

        const cm = editor.CodeMirror;
        if (!cm) return;

        if (editor.dataset.pasteDropSetup) return;
        editor.dataset.pasteDropSetup = 'true';

        cm.on('paste', (cmInstance, event) => {
            const items = (event.clipboardData || window.clipboardData).items;
            for (let i = 0; i < items.length; i++) {
                if (items[i].type.indexOf('image') === 0) {
                    event.preventDefault();
                    const file = items[i].getAsFile();
                    uploadImage(file, (success, result) => {
                        if (success) insertMarkdown(result);
                        else alert('上传失败:' + result);
                    });
                    break;
                }
            }
        });

        let uploadInProgress = false;

        editor.addEventListener('dragover', (event) => {
            event.preventDefault();
            event.dataTransfer.dropEffect = 'copy';
        });

        editor.addEventListener('drop', (event) => {
            event.preventDefault();
            event.stopPropagation();

            if (uploadInProgress) {
                console.log('Drop event skipped: upload in progress');
                return;
            }

            const files = event.dataTransfer.files;
            if (files.length > 0 && files[0].type.indexOf('image') === 0) {
                const file = files[0];
                console.log('Starting upload for file:', file.name);
                uploadInProgress = true;
                uploadImage(file, (success, result) => {
                    uploadInProgress = false;
                    if (success) {
                        insertMarkdown(result);
                        console.log('Upload completed for:', file.name);
                    } else {
                        alert('上传失败:' + result);
                        console.log('Upload failed for:', file.name, result);
                    }
                });
            } else {
                console.log('No image file dropped');
            }
        });
    }

    function addPlaceholder() {
        const editor = document.querySelector('.CodeMirror');
        if (!editor || editor.querySelector('.easyimage-placeholder')) return;

        const placeholder = document.createElement('div');
        placeholder.className = 'easyimage-placeholder';
        placeholder.textContent = '拖拽或粘贴图片可以上传图片';
        placeholder.style.cssText = 'position:absolute;bottom:10px;right:10px;color:#aaa;font-size:12px;pointer-events:none;z-index:1';

        editor.appendChild(placeholder);

        const cm = editor.CodeMirror;
        if (cm) {
            cm.on('change', () => {
                placeholder.style.display = cm.getValue().trim() ? 'none' : 'block';
            });
        }
    }

    function uploadImage(file, callback) {
        const fileKey = `${file.name}-${file.size}-${file.lastModified}`;
        if (window.uploadedFiles && window.uploadedFiles[fileKey]) {
            console.log('File already uploaded:', fileKey);
            return callback(true, window.uploadedFiles[fileKey]);
        }

        const apiDomain = extractDomain(EASYIMAGE_API_URL);
        console.log('Uploading to:', EASYIMAGE_API_URL, 'Domain:', apiDomain, 'with token:', EASYIMAGE_TOKEN);

        const formData = new FormData();
        formData.append('image', file);
        formData.append('token', EASYIMAGE_TOKEN);

        // 使用 unsafeWindow.fetch 发起请求,绕过 Tampermonkey 的 @connect 限制
        unsafeWindow.fetch(EASYIMAGE_API_URL, {
            method: 'POST',
            body: formData,
            timeout: 10000 // 10 秒超时(fetch 本身不支持 timeout,需手动实现)
        })
        .then(response => {
            if (!response.ok) {
                throw new Error(`HTTP error! Status: ${response.status}`);
            }
            return response.json();
        })
        .then(result => {
            console.log('Response:', result);
            if (result.result === 'success' && result.code === 200) {
                if (!window.uploadedFiles) window.uploadedFiles = {};
                window.uploadedFiles[fileKey] = result.url;
                callback(true, result.url);
            } else {
                callback(false, result.message || '服务器返回错误');
            }
        })
        .catch(error => {
            console.error('Fetch error:', error);
            callback(false, '网络错误:' + error.message);
        });
    }

    function insertMarkdown(url) {
        const markdown = `![image](${url})`;
        const editor = document.querySelector('.CodeMirror');
        if (editor) {
            const cm = editor.CodeMirror;
            if (cm) {
                cm.replaceRange(markdown, cm.getCursor());
                cm.focus();
                showSuccessMessage();
            }
        }
    }

    function showSuccessMessage() {
        const topicSelect = document.querySelector('.topic-select');
        const successMsg = document.createElement('div');
        successMsg.textContent = '🎉 图片上传成功!';
        successMsg.style.cssText = 'color:#4CAF50;margin:5px 0';
        topicSelect.insertBefore(successMsg, topicSelect.firstChild);
        setTimeout(() => successMsg.remove(), 2000);
    }
})();