搜索
缓存时间07 现在时间07 缓存数据 有些束缚,是我们自找的;有些压力,是我们自给的;有些痛苦,是我们自愿的。没有如影相随的不幸,只有死不放手的执着。早上好!
查看: 90|回复: 6

分享一个粘贴图片自动上传图床的油猴脚本(适用奶昔论坛)

[复制链接]
发表于 4 天前 | 显示全部楼层 |阅读模式

马上注册,免受广告困扰,轻松兑换eSIM!

您需要 登录 才可以下载或查看,没有账号?注册

×
但是这个脚本不支持hostloc,然后由于是油猴脚本都是js.所以用GPT帮忙改了下. 现在能支持hostloc了. 其它类似hostloc 的discuz bbs 理论也能用.你们自己添加// @match 匹配url 页面试试. 我只测试了hostloc.

下一次更新 希望看能不能搞上imgur免费图床.个人还是觉得imgur非常稳定的.
// ==UserScript==
// @name         奶昔论坛 编辑器增强
// @namespace    https://forum.naixi.net/
// @version      0.1.00
// @description  为 奶昔论坛 编辑器增加图片上传功能
// @author       Zhiyang
// @match        *://forum.naixi.net/*
// @icon         https://forum.naixi.net/static/image/common/logo.svg
// @grant        GM_xmlhttpRequest
// @license      MPL-2.0 License
// @supportURL   https://forum.naixi.net/thread-2295-1-1.html
// @homepageURL  https://forum.naixi.net/thread-2295-1-1.html
// ==/UserScript==

/**
 *
 *
 * 当前版本更新日志
 * 0.0.11 - 2024.12.18          !!!更新前注意备份您的配置!!!
 * - 新增 支持 HOSTLOC
 * 0.0.11 - 2024.10.05          !!!更新前注意备份您的配置!!!
 * - 新增 支持 0-RTT/telegraph 项目
 */

(function () {
    'use strict';

    // 图床配置, 默认提供的是这位大佬的 https://www.nodeseek.com/post-38305-1 , 这个图床上传限制 5p / IP / 小时
    // 当 type 为 LskyPro 时以下所有配置项均有用, 为 Chevereto 时 url 和 token 有用, 为 Telegraph / EasyImages 时只有 url 有用
    // Telegraph 官网 https://telegra.ph/ 在大陆被阻断, 可以使用 https://github.com/cf-pages/Telegraph-Image 提供的服务或者自己部署
    // Telegraph2 by @Xiefengshang 使用的是 https://github.com/0-RTT/telegraph 项目(个人考虑到其缓存做的更好所以使用)
    // EasyImages 官网 https://png.cm/ 限制单 ip 每天上传 3 张, 项目地址 https://github.com/icret/EasyImages2.0, 这个图床真烂, 两套接口不统一下, 文档也不写几句话
    const imgHost = {
        type: "Telegraph2", // 图床类型, 支持 LskyPro / Telegraph / Telegraph2 / Chevereto / EasyImages
        url: "https://pic.j8.work", // 图床地址, 带上协议头
        token: null, // 图床 token, 可选, 不填则为游客上传, LskyPro 在 /user/tokens 生成, Chevereto 必填, 在 /settings/api 生成, EasyImages 填写则使用后端接口上传, 不填写则使用前端接口上传
        storageId: null, // 图床存储策略ID, 可选项, 不填则为默认策略, 普通用户可在上传页抓包得到, 管理员可以在后台看到
    };
    const mdImgName = 0; // 0(非 Telegraph): 使用图床返回的原始名称, 其他值则名称固定为该值
    const submitByKey = true; // 是否按下 Ctrl+Enter 后触发发帖动作

    // 页面加载完毕后载入功能
    window.addEventListener('load', initEditorEnhancer, false);

    function initEditorEnhancer() {
        // 监听粘贴事件
        document.addEventListener('paste', (event) => handlePasteEvt(event));

        // 给编辑器绑定拖拽事件
        var dropZone = document.getElementById('code-mirror-editor') || document.getElementById('postbox');
        // 阻止默认行为
        dropZone.addEventListener('dragover', function (e) {
            e.preventDefault();
            e.stopPropagation();
            e.dataTransfer.dropEffect = 'copy'; // 显示为复制图标
        });

        // 处理文件拖放
        dropZone.addEventListener('drop', function (e) {
            e.preventDefault();
            e.stopPropagation();

            log('正在处理拖放内容...');
            let imageFiles = [];
            for (let file of e.dataTransfer.files) {
                if (/^image\//i.test(file.type)) { // 确保只处理图片文件
                    imageFiles.push(file);
                    log(`拖放的文件名: ${file.name}`);
                }
            }
            log(`拖放的图片数量: ${imageFiles.length}`);
            if (imageFiles.length === 0) {
                log('你拖放的内容好像没有图片哦', 'red');
                return;
            }

            // 调整uploadImage函数以接受File对象数组而不是DataTransferItemList
            uploadImage(imageFiles.map(file => {
                return {
                    kind: 'file',
                    type: file.type,
                    getAsFile: () => file
                };
            }));
        });

        // 修改图片按钮的行为
        // 图片按钮
        let checkExist = setInterval(function () {
            const oldElement = document.querySelector('.toolbar-item.i-icon.i-icon-pic[title="图片"]');
            if (oldElement) {
                clearInterval(checkExist);
                const newElement = oldElement.cloneNode(true);
                oldElement.parentNode.replaceChild(newElement, oldElement);
                newElement.addEventListener('click', handleImgBtnClick);
            }
        }, 200);

        // 监听 Ctrl+Enter 快捷键
        if (submitByKey)
            document.addEventListener('keydown', function (event) {
                if (event.ctrlKey && event.key === 'Enter') {
                    // 获取按钮元素
                    const button = document.querySelector('.submit.btn');
                    // 触发点击事件
                    button.click();

                }
            });

    }

    // 粘贴事件处理
    function handlePasteEvt(event) {
        log('正在处理粘贴内容...');
        const items = (event.clipboardData || event.originalEvent.clipboardData).items;
        if (items.length === 0) {
            log('你粘贴的内容好像没有图片哦', 'red');
            return;
        }
        uploadImage(items)
    }

    // 图片按钮点击事件处理
    function handleImgBtnClick() {
        // 创建一个隐藏的文件输入元素
        const input = document.createElement('input');
        input.type = 'file';
        input.multiple = true; // 允许多选文件
        input.accept = 'image/*'; // 仅接受图片文件

        // 当文件被选择后的处理
        input.onchange = e => {
            const files = e.target.files; // 获取用户选择的文件列表
            if (files.length) {
                const items = [...files].map(file => ({
                    kind: 'file',
                    type: file.type,
                    getAsFile: () => file
                }));

                uploadImage(items);
            }
        };

        // 触发文件输入框的点击事件,打开文件选择窗口
        input.click();
    }

    // 处理并上传图片
    async function uploadImage(items) {
        let imageFiles = [];

        for (let item of items) {
            if (item.kind === 'file' && item.type.indexOf('image/') !== -1) {
                let blob = item.getAsFile();
                imageFiles.push(blob);
            }
        }

        if (imageFiles.length > 0) {
            event.preventDefault();
            for (let i = 0; i < imageFiles.length; i++) {
                if (imageFiles.length > 1)
                    log(`上传第 ${i + 1} / ${imageFiles.length} 张图片...`);
                else
                    log(`上传图片...`);
                let file = imageFiles[i];
                let formData = new FormData();
                formData.append('file', file);
                if (imgHost.type === 'LskyPro') {
                    if (imgHost.storageId) formData.append('strategy_id', imgHost.storageId);
                    await uploadToLsky(formData);
                } else if (imgHost.type === 'Telegraph') {
                    await uploadToTelegraph(formData);
                } else if (imgHost.type === 'Telegraph2') {
                    await uploadToTelegraph2(formData);
                } else if (imgHost.type === 'Chevereto') {
                    await uploadToChevereto(file);
                } else if (imgHost.type === 'EasyImages') {
                    await uploadToEasyImages(file);
                } else {
                    log(`暂不支持的图床类型: ${imgHost.type}, 取消上传`, 'red');
                    return;
                }
            }

        } else {
            log('你粘贴的内容好像没有图片哦', 'red');
        }
    }

    async function uploadToLsky(formData) {
        return new Promise((resolve, reject) => {
            let headers = {
                'Accept': 'application/json'
            };
            if (imgHost.token)
                headers['Authorization'] = `Bearer ${imgHost.token}`;

            GM_xmlhttpRequest({
                method: 'POST',
                url: `${imgHost.url}/api/v1/upload`,
                headers: headers,
                data: formData,
                onload: (rsp) => {
                    let rspJson = JSON.parse(rsp.responseText);
                    if (rsp.status !== 200) {
                        log(`图片上传失败: ${rsp.status} ${rsp.statusText}`, 'red');
                        reject(rspJson.message);
                    }
                    if (rspJson.status === true) {
                        // 图片上传成功
                        if (rspJson?.data?.links?.markdown)
                            insertToEditor(mdImgName === 0 ? rspJson.data.links.markdown : `![${mdImgName}](${rspJson.data.links.url})`);
                        else {
                            log('图片上传成功, 但接口返回有误, 原始返回已粘贴到编辑器', 'red');
                            insertToEditor(`图片上传成功, 但接口返回有误: ${JSON.stringify(rspJson)})`);
                        }
                    } else
                        log(`图片上传失败: ${rspJson.message}`, 'red');
                    resolve();
                },
                onerror: (error) => {
                    log(`图片上传失败: ${error.status} ${error.statusText}`, 'red');
                    reject(error);
                }
            });
        });
    }

    async function uploadToTelegraph(formData) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'POST',
                url: `${imgHost.url}/upload`,
                data: formData,
                onload: (rsp) => {
                    let rspJson = JSON.parse(rsp.responseText);
                    rspJson = rspJson[0];
                    if (rsp.status !== 200) {
                        log(`图片上传失败: ${rsp.status} ${rsp.statusText}`, 'red');
                        reject(rspJson.message);
                    }
                    if (rspJson) {
                        // 图片上传成功
                        if (rspJson?.src)
                            insertToEditor(`![${mdImgName}](${imgHost.url}${rspJson.src})`);
                        else {
                            log('图片上传成功, 但接口返回有误, 原始返回已粘贴到编辑器', 'red');
                            insertToEditor(`图片上传成功, 但接口返回有误: ${JSON.stringify(rspJson)})`);
                        }
                    } else
                        log(`图片上传失败: ${JSON.stringify(rspJson)}`, 'red');
                    resolve();
                },
                onerror: (error) => {
                    log(`图片上传失败: ${error.status} ${error.statusText}`, 'red');
                    reject(error);
                }
            });
        });
    }

    async function uploadToTelegraph2(formData) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'POST',
                url: `${imgHost.url}/upload`,
                data: formData,
                onload: (rsp) => {
                    let rspJson = JSON.parse(rsp.responseText);

                    if (rsp.status !== 200 || !rspJson || !rspJson.data) {
                        log(`图片上传失败: ${rsp.status} ${rsp.statusText}`, 'red');
                        reject(rspJson?.message || '图片上传失败,缺少 data 字段');
                    }

                    const result = rspJson.data;

                    // 图片上传成功
                    if (result) {

                        insertToCursor(`${result}`, `${mdImgName}`);
                        log('图片上传成功', 'green');
                        resolve(result);
                    } else {
                        log('图片上传成功, 但接口返回有误, 原始返回已粘贴到编辑器', 'red');

                        insertToCursor(null, null, `图片上传成功, 但接口返回有误: ${JSON.stringify(rspJson)}`)
                        resolve();
                    }
                },
                onerror: (error) => {
                    log(`图片上传失败: ${error.status} ${error.statusText}`, 'red');
                    reject(error);
                }
            });
        });
    }

    async function uploadToChevereto(file) {
        return new Promise((resolve, reject) => {
            let headers = {
                'Accept': 'application/json'
            };
            if (!imgHost.token) {
                log('Chevereto 图床需配置 token', 'red');
                reject('Chevereto 图床需要 token, 请填写 token 后重试');
                return;
            }
            headers['X-API-Key'] = imgHost.token;
            let formData = new FormData();
            formData.append('source', file);

            GM_xmlhttpRequest({
                method: 'POST',
                url: `${imgHost.url}/api/1/upload`,
                headers: headers,
                data: formData,
                onload: (rsp) => {
                    let rspJson = JSON.parse(rsp.responseText);
                    if (rsp.status !== 200) {
                        log(`图片上传失败: ${rsp.status} ${rsp.statusText}`, 'red');
                        reject(rspJson?.success?.message || rspJson?.error?.message);
                    }
                    if (rspJson.status_code === 200) {
                        // 图片上传成功
                        let imgUrl = rspJson.image.url || rspJson.image.url_viewer || rspJson.image.url_short;
                        if (imgUrl)
                            insertToEditor(mdImgName === 0 ? `![${rspJson.image.filename}](${imgUrl})` : `![${mdImgName}](${imgUrl})`);
                        else {
                            log('图片上传成功, 但接口返回有误, 原始返回已粘贴到编辑器', 'red');
                            insertToEditor(`图片上传成功, 但接口返回有误: ${JSON.stringify(rspJson)})`);
                        }
                    } else
                        log(`图片上传失败: ${rspJson?.success?.message || rspJson?.error?.message}`, 'red');
                    resolve();
                },
                onerror: (error) => {
                    log(`图片上传失败: ${error.status} ${error.statusText}`, 'red');
                    reject(error);
                }
            });
        });
    }

    async function uploadToEasyImages(file) {
        return new Promise((resolve, reject) => {
            let url = imgHost.url;
            let formData = new FormData();
            if (imgHost.token) {
                // 带token, 使用后端接口上传
                url += '/api/index.php'
                formData.append('token', imgHost.token);
                formData.append('image', file);
            } else {
                // 不带token, 使用前端接口上传
                url += '/app/upload.php'
                formData.append('file', file);
                // 十位时间戳作为sign
                formData.append('sign', Math.floor(Date.now() / 1000));
            }


            GM_xmlhttpRequest({
                method: 'POST',
                url: url,
                data: formData,
                onload: (rsp) => {
                    let rspJson = JSON.parse(rsp.responseText);
                    if (rsp.status !== 200) {
                        log(`图片上传失败: ${rsp.status} ${rsp.statusText}`, 'red');
                        reject(rspJson.result);
                    }
                    if (rspJson.code === 200) {
                        // 图片上传成功
                        if (rspJson?.url)
                            insertToEditor(`![${(mdImgName === 0 ? rspJson.srcName : mdImgName)}](${rspJson.url})`);
                        else {
                            log('图片上传成功, 但接口返回有误, 原始返回已粘贴到编辑器', 'red');
                            insertToEditor(`图片上传成功, 但接口返回有误: ${JSON.stringify(rspJson)})`);
                        }
                    } else
                        log(`图片上传失败: ${JSON.stringify(rspJson)}`, 'red');
                    resolve();
                },
                onerror: (error) => {
                    log(`图片上传失败: ${error.status} ${error.statusText}`, 'red');
                    reject(error);
                }
            });
        });
    }

    function insertToCursor(sourceUrl, sourceName, errorString) {
        const codeMirrorElement = document.querySelector('.CodeMirror');
        const discuzEditorTextarea = document.querySelector('#e_textarea');


        if (codeMirrorElement) { // nodeseek 使用markdown语法
            const fullLink = `![${sourceName}](${sourceUrl})`
            const codeMirrorInstance = codeMirrorElement.CodeMirror;
            if (codeMirrorInstance) {
                const cursor = codeMirrorInstance.getCursor();

                codeMirrorInstance.replaceRange(`\n${fullLink} \n`, cursor);
            }
        } else if (discuzEditorTextarea) { //discuz 使用 BBS code语法
            const fullLink = `[img]${sourceUrl}[/img]`
            const cursorPosition = discuzEditorTextarea.selectionStart; // 获取光标位置
            const textBefore = discuzEditorTextarea.value.substring(0, cursorPosition); // 光标前的文本
            const textAfter = discuzEditorTextarea.value.substring(cursorPosition); // 光标后的文本

            const markdownLink = fullLink; // 你要插入的内容
            discuzEditorTextarea.value = `${textBefore}${markdownLink}${textAfter}`; // 拼接插入的文本

            // 更新光标位置到插入文本的末尾
            discuzEditorTextarea.selectionStart = discuzEditorTextarea.selectionEnd = cursorPosition + markdownLink.length;

            // 让光标保持在文本框中
            discuzEditorTextarea.focus();
        } else {
            log('出现错误: 未找到编辑栏', 'red');
        }

    }

    function insertToEditor(fullLink, sourceName, sourceUrl) {

        const codeMirrorElement = document.querySelector('.CodeMirror');

        if (codeMirrorElement) { // nodeseek 使用markdown语法
            // nodeseek 还是用老逻辑 完整链接
            const codeMirrorInstance = codeMirrorElement.CodeMirror;
            if (codeMirrorInstance) {
                const cursor = codeMirrorInstance.getCursor();
                codeMirrorInstance.replaceRange(`\n${fullLink} \n`, cursor);
            }
        }
        if (fullLink.startsWith('!['))
            log('图片已插入到编辑器~', 'green');
    }

    // 在编辑器打印日志
    function log(message, color = '') {
        if (!document.getElementById('editor-enhance-logs')) {
            initEditorLogDiv();
        }
        const logDiv = document.getElementById('editor-enhance-logs');
        logDiv.innerHTML = `<div${color ? ` style="color: ${color};` : ''}">   ${message} </div>`;

        console.log(`[NodeSeek-Editor-Enhance] ${message}`);
    }

    // 初始化显示日志的容器
    function initEditorLogDiv() {
        const logDiv = document.createElement('div');
        logDiv.id = 'editor-enhance-logs';
        logDiv.innerHTML = '';
        document.body.appendChild(logDiv);

        const editorToolbarDiv = document.querySelector('.mde-toolbar') || document.querySelector('#e_controls');
        editorToolbarDiv.appendChild(logDiv);
    }

})();
爱生活,爱奶昔~
发表于 4 天前 | 显示全部楼层
好东西啊,试试看哈
[发帖际遇]: bxn21583 乐于助人,奖励 5 金币. 幸运榜 / 衰神榜
爱生活,爱奶昔~
回复 支持 反对

使用道具 举报

发表于 3 天前 | 显示全部楼层
好东西啊,试试看哈
爱生活,爱奶昔~
回复 支持 反对

使用道具 举报

发表于 3 天前 | 显示全部楼层
以后传图片岂不是很方便
爱生活,爱奶昔~
回复 支持 反对

使用道具 举报

发表于 3 天前 | 显示全部楼层
好东西啊,试试看哈
[发帖际遇]: 一个袋子砸在了 naxi2233 头上,naxi2233 赚了 5 点数. 幸运榜 / 衰神榜
爱生活,爱奶昔~
回复 支持 反对

使用道具 举报

发表于 3 天前 | 显示全部楼层
好像拖放或者粘贴都无效哈
爱生活,爱奶昔~
回复 支持 反对

使用道具 举报

bin
发表于 3 天前 | 显示全部楼层
直接粘贴确实方便
爱生活,爱奶昔~
回复 支持 反对

使用道具 举报

Powered by Nyarime. Licensed

GMT+8, 2024-12-23 07:31 , Processed in 0.020566 second(s), 9 queries , Gzip On, Redis On
发帖际遇 ·手机版 ·小黑屋 ·RSS ·奶昔网

登录切换风格
快速回复 返回顶部 返回列表