Greasy Fork

Greasy Fork is available in English.

图片/视频上传助手(智能命名版)

根据标签内容智能命名文件,支持图片/视频上传到指定 API,含悬浮、菜单、剪贴板上传功能

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         图片/视频上传助手(智能命名版)
// @namespace    https://example.com/
// @version      1.2
// @description  根据标签内容智能命名文件,支持图片/视频上传到指定 API,含悬浮、菜单、剪贴板上传功能
// @author       云空陆
// @license MIT
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @connect      your.domain
// ==/UserScript==

(function () {
    'use strict';

      /*********** 配置区域 ***********/
    const UPLOAD_API = "";  // 上传 API直接填写api地址
    const AUTH_CODE = "";           // 上传认证码或者apitoken(可为空)
    const SERVER_COMPRESS = "false";     // true/false/空(空使用默认 true)
    const UPLOAD_CHANNEL = "";      // telegram/cfr2/s3/空(空使用默认 telegram)
    const AUTO_RETRY = "";          // true/false/空(空使用默认 true)
    const UPLOAD_NAME_TYPE = "";    // default/index/origin/short/空(空使用默认 default)
    const RETURN_FORMAT = "";       // default/full/空(空使用默认 default)
    const UPLOAD_FOLDER = "";       // 相对目录,空使用默认
    /********************************/

    let currentFile = null;

    const uploadBtn = document.createElement("div");
    uploadBtn.innerText = "📤";
    Object.assign(uploadBtn.style, {
        position: "fixed",
        padding: "6px 10px",
        background: "#f13535ff",
        color: "#fff",
        borderRadius: "4px",
        fontSize: "12px",
        cursor: "pointer",
        zIndex: "999999",
        display: "none",
        boxShadow: "0 2px 6px rgba(0,0,0,0.3)",
        transition: "opacity 0.2s",
    });
    document.body.appendChild(uploadBtn);

    let hideUploadTimer = null;

    function getFileSrc(el) {
        return el.getAttribute('data-original') || el.getAttribute('data-src') || el.src || '';
    }

    // 获取节点中可读标题作为文件名
    function getReadableName(el) {
        const nameAttr = el.getAttribute("alt") ||
            el.getAttribute("title") ||
            el.getAttribute("aria-label") ||
            el.getAttribute("data-name") ||
            el.getAttribute("data-title") || "";
        if (nameAttr && nameAttr.trim()) return sanitizeFilename(nameAttr.trim());
        return "";
    }

    // 清理非法文件名字符
    function sanitizeFilename(name) {
        return name.replace(/[\\/:*?"<>|]/g, "_").slice(0, 50);
    }

    // 根据 MIME 类型或 URL 自动获取扩展名
    function guessExtension(mime, url = "") {
        if (mime) {
            if (mime.startsWith("image/")) return mime.split("/")[1];
            if (mime.startsWith("video/")) return mime.split("/")[1];
        }
        const m = url.match(/\.(jpg|jpeg|png|webp|gif|mp4|mov|avi|mkv|webm|svg|bmp|heic|heif)(\?|#|$)/i);
        if (m) return m[1].toLowerCase();
        return "dat";
    }

    // ===== 鼠标悬浮显示按钮 =====
    document.addEventListener("mouseover", (e) => {
        let el = e.target;
        if ((el.tagName === "IMG" || el.tagName === "VIDEO") && getFileSrc(el)) {
            currentFile = el;
            const rect = el.getBoundingClientRect();
            uploadBtn.style.left = `${rect.right - 160}px`;
            uploadBtn.style.top = `${rect.top + 10}px`;
            uploadBtn.style.display = "block";
            uploadBtn.style.opacity = "1";
            clearTimeout(hideUploadTimer);
        }
    });

    document.addEventListener("mouseout", (e) => {
        if (!uploadBtn.contains(e.relatedTarget) && (!currentFile || !currentFile.contains(e.relatedTarget))) {
            clearTimeout(hideUploadTimer);
            hideUploadTimer = setTimeout(() => {
                uploadBtn.style.opacity = "0";
                setTimeout(() => { uploadBtn.style.display = "none"; }, 300);
            }, 500);
        }
    });

    uploadBtn.addEventListener("mouseenter", () => clearTimeout(hideUploadTimer));

    // ===== 上传核心逻辑 =====
    async function uploadFileByUrl(fileUrl, index = 1, total = 1, el = null) {
        if (!fileUrl) { showTip("❌ 未找到文件链接", true); return; }

        showTip(`📤 上传中(${index}/${total})\n${fileUrl}`, false);

        try {
            const resp = await fetch(fileUrl);
            const blob = await resp.blob();

            // 智能生成文件名
            let baseName = el ? getReadableName(el) : "";
            const ext = guessExtension(blob.type, fileUrl);
            if (!baseName) {
                const fromUrl = decodeURIComponent(fileUrl.split("/").pop().split("?")[0]);
                baseName = sanitizeFilename(fromUrl.replace(/\.[a-z0-9]+$/i, "")) || `file_${Date.now()}`;
            }
            const filename = `${baseName}.${ext}`;

            const formData = new FormData();
            formData.append("file", blob, filename);

            const queryParams = new URLSearchParams();
            if (AUTH_CODE) queryParams.append("authCode", AUTH_CODE);
            if (SERVER_COMPRESS) queryParams.append("serverCompress", SERVER_COMPRESS);
            if (UPLOAD_CHANNEL) queryParams.append("uploadChannel", UPLOAD_CHANNEL);
            if (AUTO_RETRY) queryParams.append("autoRetry", AUTO_RETRY);
            if (UPLOAD_NAME_TYPE) queryParams.append("uploadNameType", UPLOAD_NAME_TYPE);
            if (RETURN_FORMAT) queryParams.append("returnFormat", RETURN_FORMAT);
            if (UPLOAD_FOLDER) queryParams.append("uploadFolder", UPLOAD_FOLDER);

            GM_xmlhttpRequest({
                method: "POST",
                url: `${UPLOAD_API}?${queryParams.toString()}`,
                data: formData,
                responseType: "json",
                onload: function (res) {
                    let result;
                    try { result = res.response || JSON.parse(res.responseText); }
                    catch (e) { showTip("❌ 上传响应解析失败", true); return; }

                    if (Array.isArray(result) && result[0]?.src) {
                        const fullUrl = `${UPLOAD_API.replace(/\/upload$/, "")}${result[0].src}`;
                        showTip(`✅ 上传成功(${index}/${total})\n${filename}\n链接: ${fullUrl}`, true, fullUrl);
                        console.log("上传成功:", fullUrl);
                    } else {
                        showTip(`❌ 上传失败(${index}/${total}): 未返回有效链接`, true);
                        console.error(result);
                    }
                },
                onerror: function () { showTip("❌ 网络请求失败", true); }
            });
        } catch (err) {
            console.error(err);
            showTip("❌ 文件加载或处理失败", true);
        }
    }

    uploadBtn.addEventListener("click", async () => {
        if (!currentFile) return;
        const url = getFileSrc(currentFile);
        await uploadFileByUrl(url, 1, 1, currentFile);
    });

    // ===== 提示框 =====
    let tipDiv = null, autoCloseTimer = null;
    function showTip(msg, showControls = false, copyText = "") {
        if (!tipDiv) {
            tipDiv = document.createElement("div");
            Object.assign(tipDiv.style, {
                position: "fixed", top: "20px", right: "20px",
                background: "rgba(0,0,0,0.55)", color: "#fff",
                padding: "10px 15px", borderRadius: "6px",
                zIndex: "999999", fontSize: "13px",
                whiteSpace: "pre-wrap", wordBreak: "break-word",
                maxWidth: "320px", maxHeight: "250px", overflowY: "auto"
            });
            document.body.appendChild(tipDiv);
        }
        tipDiv.innerHTML = "";
        const textNode = document.createElement("div");
        textNode.innerText = msg;
        tipDiv.appendChild(textNode);

        if (showControls) {
            const btnContainer = document.createElement("div");
            Object.assign(btnContainer.style, { marginTop: "6px", textAlign: "right" });

            if (copyText) {
                const copyBtn = document.createElement("button");
                copyBtn.innerText = "复制链接";
                Object.assign(copyBtn.style, {
                    marginRight: "5px",
                    cursor: "pointer",
                    color: "#fff",
                    background: "#f13535ff",
                    border: "none",
                    borderRadius: "4px",
                    padding: "3px 8px",
                    fontSize: "12px"
                });
                copyBtn.onclick = () => {
                    navigator.clipboard.writeText(copyText).then(() => {
                        copyBtn.innerText = "已复制";
                        setTimeout(() => copyBtn.innerText = "复制链接", 1500);
                    });
                };
                btnContainer.appendChild(copyBtn);
            }

            const closeBtn = document.createElement("button");
            closeBtn.innerText = "关闭";
            Object.assign(closeBtn.style, {
                cursor: "pointer",
                color: "#fff",
                background: "#f13535ff",
                border: "none",
                borderRadius: "4px",
                padding: "3px 8px",
                fontSize: "12px"
            });
            closeBtn.onclick = () => { tipDiv.style.display = "none"; clearTimeout(autoCloseTimer); };
            btnContainer.appendChild(closeBtn);
            tipDiv.appendChild(btnContainer);
        }

        tipDiv.style.display = "block";
        clearTimeout(autoCloseTimer);
        autoCloseTimer = setTimeout(() => { tipDiv.style.display = "none"; }, 10000);
    }

    // ===== 菜单上传 =====
    GM_registerMenuCommand("📥 手动输入链接上传", async () => {
        const input = prompt("请输入文件链接(可输入多个链接,用换行或逗号分隔):");
        if (!input) return;
        const urls = input.split(/\n|,|;|\s+/).map(u => u.trim()).filter(Boolean);
        const total = urls.length;
        for (let i = 0; i < total; i++) {
            await uploadFileByUrl(urls[i], i + 1, total);
            await new Promise(r => setTimeout(r, 1200));
        }
        showTip(`✅ 全部上传完成,共 ${total} 个文件`, true);
    });

    // ===== 剪贴板上传按钮 =====
    const clipBtn = document.createElement("div");
    clipBtn.innerText = "📋剪贴图床";
    Object.assign(clipBtn.style, {
        position: "fixed",
        right: localStorage.getItem("clipBtn_right") || "20px",
        bottom: localStorage.getItem("clipBtn_bottom") || "80px",
        background: "#f13535ff",
        color: "#fff",
        padding: "8px 12px",
        borderRadius: "20px",
        fontSize: "13px",
        cursor: "pointer",
        boxShadow: "0 2px 6px rgba(0,0,0,0.3)",
        zIndex: "999999",
        opacity: "0.9",
        userSelect: "none",
    });
    document.body.appendChild(clipBtn);

    // ===== 拖拽逻辑 =====
    let isDragging = false, offsetX = 0, offsetY = 0;
    clipBtn.addEventListener("mousedown", (e) => {
        isDragging = true;
        offsetX = e.clientX - clipBtn.getBoundingClientRect().left;
        offsetY = e.clientY - clipBtn.getBoundingClientRect().top;
        clipBtn.style.transition = "none";
        e.preventDefault();
    });
    document.addEventListener("mousemove", (e) => {
        if (!isDragging) return;
        let left = e.clientX - offsetX;
        let top = e.clientY - offsetY;
        left = Math.max(0, Math.min(window.innerWidth - clipBtn.offsetWidth, left));
        top = Math.max(0, Math.min(window.innerHeight - clipBtn.offsetHeight, top));
        clipBtn.style.left = left + "px";
        clipBtn.style.top = top + "px";
        clipBtn.style.right = "auto";
        clipBtn.style.bottom = "auto";
    });
    document.addEventListener("mouseup", () => {
        if (isDragging) {
            isDragging = false;
            clipBtn.style.transition = "opacity 0.2s";
            localStorage.setItem("clipBtn_left", clipBtn.style.left);
            localStorage.setItem("clipBtn_top", clipBtn.style.top);
        }
    });

    // ===== 剪贴板上传逻辑 =====
    clipBtn.addEventListener("click", async () => {
        if (isDragging) return;
        try {
            const items = await navigator.clipboard.read();
            for (const item of items) {
                for (const type of item.types) {
                    if (type.startsWith("image/") || type.startsWith("video/")) {
                        const blob = await item.getType(type);
                        const blobUrl = URL.createObjectURL(blob);
                        showTip("📋 检测到文件,正在上传...", false);
                        await uploadFileByUrl(blobUrl);
                        return;
                    }
                }
            }
            const text = await navigator.clipboard.readText();
            if (/^https?:\/\/[^\s]+/i.test(text.trim())) {
                showTip("📋 检测到文件链接,正在上传...", false);
                await uploadFileByUrl(text.trim());
            } else {
                showTip("⚠️ 剪贴板中未检测到文件或链接", true);
            }
        } catch (err) {
            console.error(err);
            showTip("❌ 无法访问剪贴板,请检查浏览器权限", true);
        }
    });

})();