// ==UserScript== // @name ImgBB Uploader v11 — Universal (i forget it Forver NO UPDATE) // @namespace https://imgbb.com/ // @version 11.0 // @description 任意网站粘贴/拖拽/选择上传图片到ImgBB。即时预览+自动插入。 // @author v11 // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_setClipboard // @connect imgbb.com // @run-at document-idle // @downloadURL https://update.greasyfork.icu/scripts/567315/ImgBB%20Uploader%20v11%20%E2%80%94%20Universal%20%28i%20forget%20it%20Forver%20NO%20UPDATE%29.user.js // @updateURL https://update.greasyfork.icu/scripts/567315/ImgBB%20Uploader%20v11%20%E2%80%94%20Universal%20%28i%20forget%20it%20Forver%20NO%20UPDATE%29.meta.js // ==/UserScript== (function() { 'use strict'; const API = "https://imgbb.com/json"; const HOME = "https://imgbb.com/"; const HDR = { Origin: "https://imgbb.com", Referer: "https://imgbb.com/", "X-Requested-With": "XMLHttpRequest" }; const MAX_JOBS = 8, MAX_RETRY = 2, RETRY_MS = 1500; const FMT = { MD:'md', BB:'bb', HTML:'html', URL:'url' }; // ===== Token ===== let _token = null, _tokenP = null; function token() { if (_token) return Promise.resolve(_token); if (_tokenP) return _tokenP; return _tokenP = new Promise((ok, no) => { GM_xmlhttpRequest({ method:"GET", url:HOME, onload(r) { const m = r.responseText.match(/auth_token\s*=\s*"([a-f0-9]+)"/); m ? (_token=m[1], ok(m[1])) : no("token失败"); }, onerror:()=>no("网络错误"), onloadend:()=>{_tokenP=null;} }); }); } // ===== 并发队列 ===== const Q = { p:[], r:0 }; function enq(fn) { return new Promise((ok,no)=>{ Q.p.push({fn,ok,no}); flQ(); }); } function flQ() { while(Q.r{Q.r--;flQ();}); } } // ===== 上传 ===== function upload(file, tk) { return new Promise((ok, no) => { const fd = new FormData(); fd.append("source",file); fd.append("type","file"); fd.append("action","upload"); fd.append("timestamp",Date.now().toString()); fd.append("auth_token",tk); GM_xmlhttpRequest({ method:"POST", url:API, data:fd, headers:HDR, onload(r){ try{ const j=JSON.parse(r.responseText); j.status_code===200&&j.image?.url ? ok(j.image.url) : no(j.error?.message||"失败"); }catch(e){no("解析错误");} }, onerror:()=>no("网络错误"), ontimeout:()=>no("超时") }); }); } // ===== 格式 ===== function fmtLink(url, name, f) { switch(f) { case FMT.MD: return `![${name}](${url})`; case FMT.BB: return `[img]${url}[/img]`; case FMT.HTML: return `${name}`; default: return url; } } // ===== 平台检测 ===== function detectFmt() { const h = location.hostname, b = (document.body?.innerHTML||'').slice(0,5000).toLowerCase(); const bb = ['vbulletin','xenforo','mybb','phpbb','discuz','smf','ipboard']; if (bb.some(k=>h.includes(k)) || b.includes('[/img]') || b.includes('bbcode') || document.querySelector('[data-bbcode],.bbcode-toolbar,.sceditor-toolbar')) return FMT.BB; if (['github.com','gitlab.com','stackoverflow.com','reddit.com','v2ex.com', 'hackmd.io','yuque.com','jianshu.com','segmentfault.com','juejin.cn', 'zhihu.com','csdn.net','greasyfork.org','gitee.com','notion.so'] .some(s=>h.includes(s))) return FMT.MD; if (document.querySelector('.CodeMirror,.monaco-editor,.EasyMDEContainer,.bytemd')) return FMT.MD; return FMT.MD; } let curFmt = detectFmt(); let stats = { total:0, done:0, fail:0 }; // ===== 输入目标探测(统计学加权策略) ===== // 按出现概率从高到低探测 function findTarget() { const el = document.activeElement; // Case 1 (~40%): textarea / input 有焦点 if (el && el.tagName === 'TEXTAREA' && !el.readOnly && !el.disabled) return { t:'ta', el }; if (el && el.tagName === 'INPUT' && el.type === 'text') return { t:'ta', el }; // Case 2 (~25%): contentEditable 有焦点 if (el && el.isContentEditable && el.tagName !== 'BODY') return { t:'ce', el }; // Case 3 (~15%): CodeMirror const cm = document.querySelector('.CodeMirror'); if (cm?.CodeMirror) return { t:'cm', el:cm }; // Case 4: Monaco const monaco = document.querySelector('.monaco-editor'); if (monaco) { const ta = monaco.querySelector('textarea'); if (ta) return { t:'ta', el:ta }; } // Case 5: 页面上可见的 contentEditable for (const sel of ['[contenteditable="true"]','.ql-editor','.ProseMirror', '.cke_editable','.note-editable','.w-e-text','.tox-edit-area [contenteditable]', '.fr-element','.ck-editor__editable','[role="textbox"][contenteditable]']) { const found = document.querySelector(sel); if (found && found.offsetParent !== null) return { t:'ce', el:found }; } // Case 6: 页面上可见的 textarea for (const ta of document.querySelectorAll('textarea:not([readonly]):not([disabled])')) { if (ta.offsetParent !== null) return { t:'ta', el:ta }; } // Case 7: iframe 内编辑器 for (const iframe of document.querySelectorAll('iframe')) { try { const doc = iframe.contentDocument; if (!doc) continue; if (doc.designMode === 'on' || doc.body?.isContentEditable) return { t:'iframe', el:iframe }; const ta = doc.querySelector('textarea:not([readonly])'); if (ta && ta.offsetParent !== null) return { t:'ta', el:ta }; } catch(e) {} } return null; } // ===== 插入引擎 ===== // textarea 插入(React/Vue 兼容) function insertTA(el, text) { el.focus(); const s = el.selectionStart||0, e = el.selectionEnd||0; const pre = el.value.substring(0,s); const suf = el.value.substring(e); const nl = (pre.length && !pre.endsWith('\n')) ? '\n' : ''; const ins = nl + text + '\n'; // 方法1: execCommand(最佳兼容性) try { el.setSelectionRange(s, e); if (document.execCommand('insertText', false, ins)) return true; } catch(x) {} // 方法2: 直接改 value + React setter el.value = pre + ins + suf; const pos = s + ins.length; el.setSelectionRange(pos, pos); // React 兼容 const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,'value')?.set || Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')?.set; if (setter) { setter.call(el, el.value); } el.dispatchEvent(new Event('input',{bubbles:true})); el.dispatchEvent(new Event('change',{bubbles:true})); return true; } // contentEditable 插入图片(带预览) function insertCE_img(el, src, alt) { el.focus(); const img = document.createElement('img'); img.src = src; img.alt = alt || 'image'; img.style.cssText = 'max-width:100%;max-height:500px;border-radius:4px;'; img.setAttribute('data-imgbb','1'); const sel = window.getSelection(); if (sel.rangeCount) { const range = sel.getRangeAt(0); range.deleteContents(); range.insertNode(img); range.setStartAfter(img); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); } else { el.appendChild(img); } el.dispatchEvent(new Event('input',{bubbles:true})); return img; } // contentEditable 插入文本 function insertCE_text(el, text) { el.focus(); try { if (document.execCommand('insertText', false, text+'\n')) return true; } catch(x) {} const tn = document.createTextNode(text+'\n'); const sel = window.getSelection(); if (sel.rangeCount) { const r = sel.getRangeAt(0); r.deleteContents(); r.insertNode(tn); r.setStartAfter(tn); r.collapse(true); sel.removeAllRanges(); sel.addRange(r); } else el.appendChild(tn); el.dispatchEvent(new Event('input',{bubbles:true})); return true; } // CodeMirror 插入 function insertCM(cmEl, text) { const cm = cmEl.CodeMirror; if (!cm) return false; const cur = cm.getCursor(); const line = cm.getLine(cur.line); const nl = (line && line.length) ? '\n' : ''; cm.replaceRange(nl + text + '\n', cur); cm.focus(); return true; } // iframe 内插入 function insertIframe(iframe, src, alt) { try { const doc = iframe.contentDocument; const img = doc.createElement('img'); img.src = src; img.alt = alt; img.style.cssText = 'max-width:100%;'; doc.body.appendChild(img); return img; } catch(e) { return null; } } // 剪贴板降级 function clip(text) { try { GM_setClipboard(text,'text'); return; } catch(e) {} try { navigator.clipboard.writeText(text); return; } catch(e) {} const t = document.createElement('textarea'); t.value = text; t.style.cssText='position:fixed;opacity:0'; document.body.appendChild(t); t.select(); document.execCommand('copy'); document.body.removeChild(t); } // ===== 预览 + 上传 + 替换 三阶段流水线 ===== // 阶段1: 创建本地预览占位 function createPreview(blobUrl, filename) { const target = findTarget(); if (!target) return { placeholder:null, target:null }; // 富文本编辑器:插入 img 预览 if (target.t === 'ce') { const img = insertCE_img(target.el, blobUrl, filename); if (img) { // 添加上传中样式 img.style.opacity = '0.6'; img.style.outline = '2px solid #3498db'; img.style.transition = 'all 0.4s ease'; // 添加 spinner const wrap = document.createElement('span'); wrap.style.cssText = 'display:inline-block;position:relative;line-height:0;'; wrap.className = 'imgbb-wrap'; img.parentNode.insertBefore(wrap, img); wrap.appendChild(img); const spin = document.createElement('span'); spin.className = 'imgbb-spin'; wrap.appendChild(spin); return { placeholder:{ type:'ce', wrap, img }, target }; } } // iframe 编辑器 if (target.t === 'iframe') { const img = insertIframe(target.el, blobUrl, filename); if (img) { img.style.opacity = '0.6'; img.style.outline = '2px solid #3498db'; return { placeholder:{ type:'iframe', img }, target }; } } // textarea / CodeMirror: 插入临时占位文本 if (target.t === 'ta') { const placeholder_text = `⏳ 上传中: ${filename}...`; insertTA(target.el, placeholder_text); return { placeholder:{ type:'ta', el:target.el, text:placeholder_text }, target }; } if (target.t === 'cm') { const placeholder_text = `⏳ 上传中: ${filename}...`; insertCM(target.el, placeholder_text); return { placeholder:{ type:'cm', el:target.el, text:placeholder_text }, target }; } return { placeholder:null, target }; } // 阶段2: 上传完成后替换占位 function finalize(ph, url, filename, ok) { if (!ph) return; if (ph.type === 'ce') { const { wrap, img } = ph; // 移除 spinner const spin = wrap.querySelector('.imgbb-spin'); if (spin) spin.remove(); if (ok) { img.src = url; img.style.opacity = '1'; img.style.outline = '2px solid #27ae60'; // 添加 ✓ const badge = document.createElement('span'); badge.className = 'imgbb-ok'; badge.innerHTML = ``; wrap.appendChild(badge); requestAnimationFrame(()=>requestAnimationFrame(()=>badge.classList.add('show'))); setTimeout(()=>{ img.style.outline = 'none'; badge.style.transition='opacity .5s'; badge.style.opacity='0'; setTimeout(()=>{ // 解包 wrapper,只留 img if (wrap.parentNode) { wrap.parentNode.insertBefore(img, wrap); wrap.remove(); } }, 500); }, 2500); } else { img.style.opacity = '0.5'; img.style.outline = '3px solid #e74c3c'; img.title = '上传失败'; const badge = document.createElement('span'); badge.className = 'imgbb-fail'; badge.innerHTML = ``; wrap.appendChild(badge); requestAnimationFrame(()=>requestAnimationFrame(()=>badge.classList.add('show'))); } } if (ph.type === 'iframe' && ph.img) { if (ok) { ph.img.src = url; ph.img.style.opacity = '1'; ph.img.style.outline = 'none'; } else { ph.img.style.outline = '3px solid #e74c3c'; ph.img.title = '上传失败'; } } if (ph.type === 'ta' && ph.el) { const val = ph.el.value; const replacement = ok ? fmtLink(url, filename, curFmt) : `❌ 上传失败: ${filename}`; const newVal = val.replace(ph.text, replacement); if (newVal !== val) { ph.el.value = newVal; // React 兼容 const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,'value')?.set; if (setter) setter.call(ph.el, ph.el.value); ph.el.dispatchEvent(new Event('input',{bubbles:true})); ph.el.dispatchEvent(new Event('change',{bubbles:true})); } } if (ph.type === 'cm' && ph.el?.CodeMirror) { const cm = ph.el.CodeMirror; const val = cm.getValue(); const replacement = ok ? fmtLink(url, filename, curFmt) : `❌ 上传失败: ${filename}`; cm.setValue(val.replace(ph.text, replacement)); } } // ===== 主处理流程 ===== async function processFile(file) { const name = file.name || 'image.png'; const blobUrl = URL.createObjectURL(file); stats.total++; updateProg(); // 阶段1: 预览 const { placeholder } = createPreview(blobUrl, name); // 如果没有找到任何输入目标,标记为纯剪贴板模式 const clipMode = !placeholder; // 阶段2: 上传(含重试) let url = null, lastErr = ''; for (let i = 0; i <= MAX_RETRY; i++) { if (i > 0) await new Promise(r => setTimeout(r, RETRY_MS)); try { const tk = await token(); url = await enq(() => upload(file, tk)); break; } catch(e) { lastErr = String(e); } } URL.revokeObjectURL(blobUrl); // 阶段3: 替换 if (url) { stats.done++; if (placeholder) { finalize(placeholder, url, name, true); } if (clipMode) { // 没有输入目标 → 复制到剪贴板 const text = fmtLink(url, name, curFmt); clip(text); toast(`📋 已复制: ${text.length>60 ? text.slice(0,60)+'…' : text}`); } else { toast(`✅ ${name}`); } } else { stats.fail++; if (placeholder) finalize(placeholder, null, name, false); toast(`❌ 失败: ${name} (${lastErr})`); } updateProg(); } function handleFiles(list) { const imgs = Array.from(list).filter(f => f.type.startsWith('image/')); if (!imgs.length) { toast("⚠️ 没有图片"); return; } toast(`📤 上传 ${imgs.length} 张图片`); imgs.forEach(f => processFile(f)); } // ===== UI ===== function toast(msg) { let t = document.getElementById('iu-toast'); if (!t) { t=document.createElement('div'); t.id='iu-toast'; document.body.appendChild(t); } t.textContent = msg; t.classList.add('show'); clearTimeout(t._t); t._t = setTimeout(()=>t.classList.remove('show'), 2500); } function updateProg() { const p = document.getElementById('iu-prog'); if (!p) return; const {total,done,fail} = stats; const fin = done+fail; if (!total) { p.classList.remove('show'); return; } p.classList.add('show'); const fill = p.querySelector('.pf'); const txt = p.querySelector('.pt'); const badge = document.getElementById('iu-badge'); if (fill) fill.style.width = Math.round(fin/total*100)+'%'; if (fin < total) { if (txt) txt.textContent = `${fin}/${total}`; if (badge) { badge.textContent = total-fin; badge.classList.add('show'); } } else { if (txt) txt.textContent = fail ? `✅${done} ❌${fail}` : `✅ ${done}张完成`; if (badge) badge.classList.remove('show'); setTimeout(()=>{ p.classList.remove('show'); if(fill)fill.style.width='0'; stats={total:0,done:0,fail:0}; }, 3000); } } function updFmt() { document.querySelectorAll('.iu-fb').forEach(b=>b.classList.toggle('on', b.dataset.f===curFmt)); } // ===== 样式 ===== function injectCSS() { GM_addStyle(` /* Toast */ #iu-toast{position:fixed;top:20px;right:20px;z-index:999999;padding:10px 18px;background:#2c3e50;color:#fff; border-radius:6px;font-size:13px;pointer-events:none;opacity:0;transform:translateY(-10px);transition:all .3s; max-width:420px;box-shadow:0 4px 15px rgba(0,0,0,.3);font-family:-apple-system,system-ui,sans-serif} #iu-toast.show{opacity:1;transform:translateY(0)} /* 控制面板 */ #iu-panel{position:fixed;bottom:24px;right:24px;z-index:999998;display:flex;flex-direction:column; align-items:flex-end;gap:8px;font-family:-apple-system,system-ui,sans-serif} /* 格式栏 */ #iu-fmt{background:#2c3e50;border-radius:8px;padding:6px 10px;display:flex;gap:4px; box-shadow:0 4px 15px rgba(0,0,0,.3);opacity:0;transform:translateY(10px);transition:all .3s;pointer-events:none} #iu-fmt.show{opacity:1;transform:translateY(0);pointer-events:auto} .iu-fb{background:0;border:1px solid rgba(255,255,255,.2);color:#aaa;padding:4px 10px;border-radius:4px; cursor:pointer;font-size:11px;font-weight:bold;transition:all .2s} .iu-fb:hover{border-color:#3498db;color:#fff} .iu-fb.on{background:#3498db;border-color:#3498db;color:#fff} /* 主按钮 */ #iu-btn{width:52px;height:52px;border-radius:50%;border:none; background:linear-gradient(135deg,#3498db,#2980b9);color:#fff;cursor:pointer; box-shadow:0 4px 15px rgba(52,152,219,.4);display:flex;align-items:center;justify-content:center; transition:all .3s;position:relative} #iu-btn:hover{transform:scale(1.1);box-shadow:0 6px 20px rgba(52,152,219,.6)} #iu-btn:active{transform:scale(.95)} #iu-btn svg{width:24px;height:24px;fill:#fff} #iu-badge{position:absolute;top:-4px;right:-4px;min-width:18px;height:18px;background:#e74c3c;color:#fff; border-radius:9px;font-size:10px;font-weight:bold;display:none;align-items:center;justify-content:center; padding:0 4px;box-shadow:0 2px 6px rgba(0,0,0,.3)} #iu-badge.show{display:flex} /* 进度 */ #iu-prog{position:fixed;bottom:96px;right:24px;z-index:999998;background:#2c3e50;color:#fff; border-radius:10px;padding:10px 14px;min-width:180px;box-shadow:0 4px 20px rgba(0,0,0,.3); font-size:12px;opacity:0;transform:translateY(10px) scale(.95);transition:all .3s;pointer-events:none} #iu-prog.show{opacity:1;transform:translateY(0) scale(1);pointer-events:auto} .pb{width:100%;height:5px;background:rgba(255,255,255,.15);border-radius:3px;overflow:hidden;margin-bottom:6px} .pf{height:100%;background:linear-gradient(90deg,#27ae60,#2ecc71);border-radius:3px;transition:width .3s;width:0} .pt{opacity:.85} /* 拖拽遮罩 */ #iu-drop{position:fixed;inset:0;background:rgba(52,152,219,.12);border:4px dashed #3498db;z-index:999997; display:none;align-items:center;justify-content:center;pointer-events:none} #iu-drop.show{display:flex} #iu-drop span{background:rgba(52,152,219,.9);color:#fff;padding:18px 36px;border-radius:12px; font-size:18px;font-weight:bold;box-shadow:0 8px 30px rgba(0,0,0,.3)} /* 预览相关 */ .imgbb-wrap{display:inline-block;position:relative;line-height:0} .imgbb-spin{position:absolute;top:6px;right:6px;width:26px;height:26px;background:rgba(52,152,219,.9); border-radius:50%;display:flex;align-items:center;justify-content:center; box-shadow:0 2px 8px rgba(0,0,0,.3);z-index:10;pointer-events:none;animation:iu-fi .3s ease} .imgbb-spin::after{content:'';width:14px;height:14px;border:2.5px solid rgba(255,255,255,.3); border-top-color:#fff;border-radius:50%;animation:iu-sp .7s linear infinite} @keyframes iu-sp{to{transform:rotate(360deg)}} @keyframes iu-fi{from{opacity:0;transform:scale(.3)}to{opacity:1;transform:scale(1)}} .imgbb-ok,.imgbb-fail{position:absolute;top:6px;right:6px;width:26px;height:26px;border-radius:50%; display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(0,0,0,.3); z-index:10;pointer-events:none;opacity:0;transform:scale(.3); transition:all .4s cubic-bezier(.175,.885,.32,1.275)} .imgbb-ok{background:#27ae60} .imgbb-fail{background:#e74c3c} .imgbb-ok.show,.imgbb-fail.show{opacity:1;transform:scale(1)} .imgbb-ok svg,.imgbb-fail svg{width:14px;height:14px;fill:none;stroke:#fff;stroke-width:3; stroke-linecap:round;stroke-linejoin:round} .imgbb-ok svg .ck{stroke-dasharray:24;stroke-dashoffset:24;transition:stroke-dashoffset .4s ease .15s} .imgbb-ok.show svg .ck{stroke-dashoffset:0} #iu-fi{display:none} `); } // ===== DOM ===== function injectDOM() { const fi = document.createElement('input'); fi.type='file'; fi.id='iu-fi'; fi.accept='image/*'; fi.multiple=true; fi.onchange = e => { if(e.target.files.length) handleFiles(e.target.files); e.target.value=''; }; document.body.appendChild(fi); const drop = document.createElement('div'); drop.id='iu-drop'; drop.innerHTML='📤 松开上传图片'; document.body.appendChild(drop); const panel = document.createElement('div'); panel.id='iu-panel'; panel.innerHTML = `
`; document.body.appendChild(panel); const prog = document.createElement('div'); prog.id='iu-prog'; prog.innerHTML='
'; document.body.appendChild(prog); // 事件 document.getElementById('iu-btn').onclick = ()=>fi.click(); document.getElementById('iu-btn').oncontextmenu = e=>{ e.preventDefault(); document.getElementById('iu-fmt').classList.toggle('show'); }; document.querySelectorAll('.iu-fb').forEach(b=>b.onclick=()=>{ curFmt=b.dataset.f; updFmt(); toast(`📝 ${curFmt.toUpperCase()}`); }); updFmt(); } // ===== 事件 ===== function initEvents() { // 粘贴 document.addEventListener('paste', e => { const items = e.clipboardData?.items; if (!items) return; const files = []; for (const it of items) if (it.kind==='file' && it.type.startsWith('image/')) files.push(it.getAsFile()); if (files.length) { e.preventDefault(); handleFiles(files); } }); // 拖拽 let dn = 0; const dr = document.getElementById('iu-drop'); document.addEventListener('dragenter', e => { e.preventDefault(); dn++; if (e.dataTransfer?.types?.includes('Files') && dr) dr.classList.add('show'); }); document.addEventListener('dragleave', e => { e.preventDefault(); dn--; if (dn<=0) { dn=0; if(dr)dr.classList.remove('show'); } }); document.addEventListener('dragover', e => { e.preventDefault(); e.stopPropagation(); }); document.addEventListener('drop', e => { e.preventDefault(); e.stopPropagation(); dn=0; if(dr)dr.classList.remove('show'); if(e.dataTransfer?.files?.length) handleFiles(e.dataTransfer.files); }); // Ctrl+Shift+U document.addEventListener('keydown', e => { if (e.ctrlKey && e.shiftKey && (e.key==='U'||e.key==='u')) { e.preventDefault(); document.getElementById('iu-fi')?.click(); } }); // 焦点变化自动检测格式(节流) let lastD = 0; document.addEventListener('focusin', () => { const now = Date.now(); if (now-lastD < 800) return; lastD = now; const d = detectFmt(); if (d !== curFmt) { curFmt = d; updFmt(); } }); } // ===== 启动 ===== function boot() { if (!document.body) { window.addEventListener('DOMContentLoaded', boot); return; } token(); // 预加载 injectCSS(); injectDOM(); initEvents(); console.log(`[ImgBB v11] 格式:${curFmt} | Ctrl+Shift+U`); } boot(); })();