// ==UserScript== // @name Universal Image Uploader // @name:zh-CN 通用图片上传助手 // @name:zh-TW 通用圖片上傳助手 // @namespace https://github.com/utags // @homepageURL https://github.com/utags/userscripts#readme // @supportURL https://github.com/utags/userscripts/issues // @version 0.0.2 // @description Paste/drag/select images, batch upload to Imgur; auto-copy Markdown/HTML/BBCode/link; site button integration with SPA observer; local history. // @description:zh-CN 通用图片上传与插入:支持粘贴/拖拽/选择,批量上传至 Imgur;自动复制 Markdown/HTML/BBCode/链接;可为各站点插入按钮并适配 SPA;保存本地历史。 // @description:zh-TW 通用圖片上傳與插入:支援貼上/拖曳/選擇,批次上傳至 Imgur;自動複製 Markdown/HTML/BBCode/連結;可為各站點插入按鈕並適配 SPA;保存本地歷史。 // @author Pipecraft // @license MIT // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCIgZmlsbD0ibm9uZSI+PHJlY3QgeD0iOCIgeT0iOCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4IiByeD0iMTAiIHN0cm9rZT0iIzFmMjkzNyIgc3Ryb2tlLXdpZHRoPSI0Ii8+PHBhdGggZD0iTTMyIDIwbC0xMiAxMmg3djE4aDEwVjMyaDdsLTEyLTEyeiIgZmlsbD0iIzFmMjkzNyIvPjwvc3ZnPg== // @noframes // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_setClipboard // @grant GM_addValueChangeListener // @connect api.imgur.com // ==/UserScript== ;(function () { 'use strict' // I18N: language detection and translations const I18N = { en: { header_title: 'Universal Image Uploader', btn_history: 'History', btn_settings: 'Settings', btn_close: 'Close', format_markdown: 'Markdown', format_html: 'HTML', format_bbcode: 'BBCode', format_link: 'Link', btn_select_images: 'Select Images', progress_initial: 'Done 0/0', progress_done: 'Done {done}/{total}', hint_text: 'Paste or drag images onto the page, or click Select to batch upload', settings_section_title: 'Site Button Settings', placeholder_css_selector: 'CSS Selector', pos_before: 'Before', pos_after: 'After', pos_inside: 'Inside', placeholder_button_content: 'Button content (HTML allowed)', insert_image_button_default: 'Insert image', btn_save_and_insert: 'Save & Insert', btn_remove_button_temp: 'Remove button (temporary)', btn_clear_settings: 'Clear settings', drop_overlay: 'Release to upload images', log_uploading: 'Uploading: ', log_success: '✅ Success: ', log_failed: '❌ Failed: ', btn_copy: 'Copy', btn_open: 'Open', menu_open_panel: 'Open upload panel', menu_select_images: 'Select images', menu_settings: 'Settings', history_upload_page_prefix: 'Upload page: ', history_upload_page: 'Upload page: {host}', btn_history_count: 'History ({count})', btn_clear_history: 'Clear', default_image_name: 'image', }, 'zh-CN': { header_title: '通用图片上传助手', btn_history: '历史', btn_settings: '设置', btn_close: '关闭', format_markdown: 'Markdown', format_html: 'HTML', format_bbcode: 'BBCode', format_link: '链接', btn_select_images: '选择图片', progress_initial: '完成 0/0', progress_done: '完成 {done}/{total}', hint_text: '支持粘贴图片、拖拽图片到页面或点击选择图片进行批量上传', settings_section_title: '站点按钮设置', placeholder_css_selector: 'CSS 选择器', pos_before: '之前', pos_after: '之后', pos_inside: '里面', placeholder_button_content: '按钮内容(可为 HTML)', insert_image_button_default: '插入图片', btn_save_and_insert: '保存并插入', btn_remove_button_temp: '移除按钮(临时)', btn_clear_settings: '清空设置', drop_overlay: '释放以上传图片', log_uploading: '上传中:', log_success: '✅ 成功:', log_failed: '❌ 失败:', btn_copy: '复制', btn_open: '打开', menu_open_panel: '打开图片上传面板', menu_select_images: '选择图片', menu_settings: '设置', history_upload_page_prefix: '上传页面:', history_upload_page: '上传页面:{host}', btn_history_count: '历史({count})', btn_clear_history: '清空', default_image_name: '图片', }, 'zh-TW': { header_title: '通用圖片上傳助手', btn_history: '歷史', btn_settings: '設定', btn_close: '關閉', format_markdown: 'Markdown', format_html: 'HTML', format_bbcode: 'BBCode', format_link: '連結', btn_select_images: '選擇圖片', progress_initial: '完成 0/0', progress_done: '完成 {done}/{total}', hint_text: '支援貼上、拖曳圖片到頁面或點擊選擇檔案進行批次上傳', settings_section_title: '站點按鈕設定', placeholder_css_selector: 'CSS 選擇器', pos_before: '之前', pos_after: '之後', pos_inside: '裡面', placeholder_button_content: '按鈕內容(可為 HTML)', insert_image_button_default: '插入圖片', btn_save_and_insert: '保存並插入', btn_remove_button_temp: '移除按鈕(暫時)', btn_clear_settings: '清空設定', drop_overlay: '放開以上傳圖片', log_uploading: '上傳中:', log_success: '✅ 成功:', log_failed: '❌ 失敗:', btn_copy: '複製', btn_open: '打開', menu_open_panel: '打開圖片上傳面板', menu_select_images: '選擇圖片', menu_settings: '設定', history_upload_page_prefix: '上傳頁面:', history_upload_page: '上傳頁面:{host}', btn_history_count: '歷史({count})', btn_clear_history: '清空', default_image_name: '圖片', }, } function detectLanguage() { try { const browserLang = ( navigator.language || navigator.userLanguage || 'en' ).toLowerCase() const supported = Object.keys(I18N) if (supported.includes(browserLang)) return browserLang const base = browserLang.split('-')[0] const match = supported.find((l) => l.startsWith(base + '-')) return match || 'en' } catch { return 'en' } } const USER_LANG = detectLanguage() function t(key) { return (I18N[USER_LANG] && I18N[USER_LANG][key]) || I18N.en[key] || key } function tpl(str, params) { return String(str).replace(/\{(\w+)\}/g, (_, k) => `${params?.[k] ?? ''}`) } // Imgur Client ID 池(参考 upload-image.ts) const IMGUR_CLIENT_IDS = [ '3107b9ef8b316f3', '442b04f26eefc8a', '59cfebe717c09e4', '60605aad4a62882', '6c65ab1d3f5452a', '83e123737849aa9', '9311f6be1c10160', 'c4a4a563f698595', '81be04b9e4a08ce', ] const HISTORY_KEY = 'iu_history' const FORMAT_MAP_KEY = 'iu_format_map' const DEFAULT_FORMAT = 'markdown' const siteKey = () => { let h = location.hostname || '' return h.startsWith('www.') ? h.slice(4) : h } const getFormat = () => { const map = GM_getValue(FORMAT_MAP_KEY, {}) return map[siteKey()] || DEFAULT_FORMAT } const setFormat = (fmt) => { const map = GM_getValue(FORMAT_MAP_KEY, {}) map[siteKey()] = fmt GM_setValue(FORMAT_MAP_KEY, map) } // 站点按钮设置(选择器/位置/文字) const BTN_SETTINGS_MAP_KEY = 'iu_site_btn_settings_map' const getSiteBtnSettings = () => { const map = GM_getValue(BTN_SETTINGS_MAP_KEY, {}) return map[siteKey()] || null } const setSiteBtnSettings = (cfg) => { const map = GM_getValue(BTN_SETTINGS_MAP_KEY, {}) const key = siteKey() const selector = (cfg?.selector || '').trim() if (!selector) { delete map[key] } else { // 规范化插入位置(英文):'before' | 'after' | 'inside' const p = (cfg?.position || '').trim() const pos = p === 'before' ? 'before' : p === 'inside' ? 'inside' : 'after' map[key] = { selector, position: pos, text: cfg?.text || '插入图片', } } GM_setValue(BTN_SETTINGS_MAP_KEY, map) } const MAX_HISTORY = 50 const createEl = (tag, attrs = {}, children = []) => { const el = document.createElement(tag) Object.entries(attrs).forEach(([k, v]) => { if (k === 'text') el.textContent = v else if (k === 'class') el.className = v else el.setAttribute(k, v) }) children.forEach((c) => el.appendChild(c)) return el } const css = ` #iu-panel { position: fixed; right: 16px; bottom: 16px; z-index: 999999; width: 440px; background: #111827cc; color: #fff; backdrop-filter: blur(6px); border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,.25); font-family: system-ui, -apple-system, Segoe UI, Roboto; } #iu-panel header { display:flex; align-items:center; justify-content:space-between; padding: 10px 12px; font-weight: 600; } #iu-panel header .actions { display:flex; gap:8px; } #iu-panel .body { padding: 8px 12px; } #iu-panel .controls { display:flex; align-items:center; gap:8px; flex-wrap: wrap; } #iu-panel select, #iu-panel button { font-size: 12px; padding: 6px 10px; border-radius: 6px; border: 1px solid #334155; background:#1f2937; color:#fff; } #iu-panel button.primary { background:#2563eb; border-color:#1d4ed8; } #iu-panel .progress { font-size: 12px; opacity:.9; } #iu-panel .list { margin-top:8px; max-height: 140px; overflow-y:auto; overflow-x:hidden; font-size: 12px; } #iu-panel .list .item { padding:6px 0; border-bottom: 1px dashed #334155; white-space: normal; word-break: break-word; overflow-wrap: anywhere; } #iu-panel .history { display:none; margin-top:8px; } #iu-panel.show-history .history { display:block; } #iu-panel .history .list { max-height: 240px; } #iu-panel .history .row { display:flex; align-items:center; justify-content:space-between; gap:8px; padding:6px 0; border-bottom: 1px dashed #334155; } #iu-panel .history .row .ops { display:flex; gap:6px; } #iu-panel .history .row .name { display:block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #iu-panel .hint { font-size: 11px; opacity:.85; margin-top:6px; } #iu-drop { position: fixed; inset: 0; background: rgba(37,99,235,.12); border: 2px dashed #2563eb; display:none; align-items:center; justify-content:center; z-index: 999998; color:#2563eb; font-size: 18px; font-weight: 600; } #iu-drop.show { display:flex; } .iu-insert-btn { font-size: 12px; padding: 4px 8px; border-radius: 6px; border: 1px solid #334155; background:#1f2937; color:#fff; cursor:pointer; } #iu-panel .settings { display:none; margin-top:8px; } #iu-panel.show-settings .settings { display:block; } ` GM_addStyle(css) function loadHistory() { return GM_getValue(HISTORY_KEY, []) } function saveHistory(list) { GM_setValue(HISTORY_KEY, list.slice(0, MAX_HISTORY)) } function addToHistory(entry) { const list = loadHistory() list.unshift(entry) saveHistory(list) } function basename(name) { const n = (name || '').trim() if (!n) return t('default_image_name') return n.replace(/\.[^.]+$/, '') } function formatText(link, name, fmt) { const alt = basename(name) switch (fmt) { case 'html': return `${alt}` case 'bbcode': return `[img]${link}[/img]` case 'link': return link default: return `![${alt}](${link})` } } async function uploadToImgur(file) { // 随机打乱 Client-ID 列表,保证每次失败后更换不同 ID const ids = [...IMGUR_CLIENT_IDS] for (let i = ids.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)) ;[ids[i], ids[j]] = [ids[j], ids[i]] } let lastError for (const id of ids) { const formData = new FormData() formData.append('image', file) try { const res = await fetch('https://api.imgur.com/3/upload', { method: 'POST', headers: { Authorization: `Client-ID ${id}` }, body: formData, }) if (!res.ok) { lastError = new Error('网络错误') continue } const data = await res.json() if (data?.success && data?.data?.link) { return data.data.link } lastError = new Error('上传失败') } catch (e) { lastError = e } } throw lastError || new Error('上传失败') } function insertIntoFocused(text) { const el = document.activeElement if (!el) return false try { if ( el instanceof HTMLTextAreaElement || (el instanceof HTMLInputElement && el.type === 'text') ) { const start = el.selectionStart ?? el.value.length const end = el.selectionEnd ?? el.value.length const v = el.value el.value = v.slice(0, start) + text + v.slice(end) el.dispatchEvent(new Event('input', { bubbles: true })) return true } if (el instanceof HTMLElement && el.isContentEditable) { document.execCommand('insertText', false, text) return true } } catch {} return false } function copyAndInsert(text) { try { GM_setClipboard(text) } catch {} insertIntoFocused(text) } function createPanel() { const panel = createEl('div', { id: 'iu-panel' }) const header = createEl('header') header.appendChild(createEl('span', { text: t('header_title') })) const actions = createEl('div', { class: 'actions' }) const toggleHistoryBtn = createEl('button', { text: t('btn_history') }) toggleHistoryBtn.addEventListener('click', () => { panel.classList.toggle('show-history') renderHistory() }) const settingsBtn = createEl('button', { text: t('btn_settings') }) settingsBtn.addEventListener('click', () => { panel.classList.toggle('show-settings') try { refreshSettingsUI() } catch {} }) const closeBtn = createEl('button', { text: t('btn_close') }) closeBtn.addEventListener('click', () => { panel.style.display = 'none' }) actions.appendChild(toggleHistoryBtn) actions.appendChild(settingsBtn) actions.appendChild(closeBtn) header.appendChild(actions) const body = createEl('div', { class: 'body' }) const controls = createEl('div', { class: 'controls' }) const format = getFormat() const formatSel = createEl('select') ;[ ['markdown', t('format_markdown')], ['html', t('format_html')], ['bbcode', t('format_bbcode')], ['link', t('format_link')], ].forEach(([val, label]) => { const opt = createEl('option', { value: val, text: label }) if (val === format) opt.selected = true formatSel.appendChild(opt) }) formatSel.addEventListener('change', () => setFormat(formatSel.value)) // 新增:抽取文件选择逻辑为函数,供按钮与菜单复用 function openFilePicker() { const input = createEl('input', { type: 'file', accept: 'image/*', multiple: 'true', style: 'display:none', }) input.addEventListener('change', () => { if (input.files?.length) handleFiles(Array.from(input.files)) }) input.click() } // 选择图片按钮(调用统一的 openFilePicker) const selectBtn = createEl('button', { class: 'primary', text: t('btn_select_images'), }) selectBtn.addEventListener('click', openFilePicker) const progressEl = createEl('span', { class: 'progress', text: t('progress_initial'), }) controls.appendChild(formatSel) controls.appendChild(selectBtn) controls.appendChild(progressEl) body.appendChild(controls) const list = createEl('div', { class: 'list' }) body.appendChild(list) const hint = createEl('div', { class: 'hint', text: t('hint_text'), }) body.appendChild(hint) const history = createEl('div', { class: 'history' }) body.appendChild(history) // 设置面板:站点“插入图片”按钮配置 const settings = createEl('div', { class: 'settings' }) const settingsHeader = createEl('div', { class: 'controls' }) settingsHeader.appendChild( createEl('span', { text: t('settings_section_title') }) ) settings.appendChild(settingsHeader) const settingsForm = createEl('div', { class: 'controls' }) const selInput = createEl('input', { type: 'text', placeholder: t('placeholder_css_selector'), }) const posSel = createEl('select') ;[ { value: 'before', text: t('pos_before') }, { value: 'after', text: t('pos_after') }, { value: 'inside', text: t('pos_inside') }, ].forEach(({ value, text }) => { const opt = createEl('option', { value, text }) if (value === 'after') opt.selected = true posSel.appendChild(opt) }) const textInput = createEl('input', { type: 'text', placeholder: t('placeholder_button_content'), }) textInput.value = t('insert_image_button_default') const saveBtn = createEl('button', { text: t('btn_save_and_insert') }) saveBtn.addEventListener('click', () => { setSiteBtnSettings({ selector: selInput.value, position: posSel.value, text: textInput.value, }) document.querySelectorAll('.iu-insert-btn').forEach((el) => el.remove()) applySiteButton() try { restartSiteButtonObserver() } catch {} }) const removeBtn = createEl('button', { text: t('btn_remove_button_temp') }) removeBtn.addEventListener('click', () => { document.querySelectorAll('.iu-insert-btn').forEach((el) => el.remove()) try { if (siteBtnObserver) siteBtnObserver.disconnect() } catch {} }) const clearBtn = createEl('button', { text: t('btn_clear_settings') }) clearBtn.addEventListener('click', () => { setSiteBtnSettings({ selector: '' }) document.querySelectorAll('.iu-insert-btn').forEach((el) => el.remove()) try { if (siteBtnObserver) siteBtnObserver.disconnect() } catch {} }) settingsForm.appendChild(selInput) settingsForm.appendChild(posSel) settingsForm.appendChild(textInput) settingsForm.appendChild(saveBtn) settingsForm.appendChild(removeBtn) settingsForm.appendChild(clearBtn) settings.appendChild(settingsForm) body.appendChild(settings) function refreshSettingsUI() { const cur = getSiteBtnSettings() || { selector: '', position: 'after', text: '插入图片', } selInput.value = cur.selector || '' Array.from(posSel.options).forEach((opt) => { opt.selected = opt.value === (cur.position || 'after') }) textInput.value = cur.text || '插入图片' } panel.appendChild(header) panel.appendChild(body) document.body.appendChild(panel) // 默认隐藏面板,避免初始显示 panel.style.display = 'none' // 根据站点设置,在指定位置插入“插入图片”按钮 function applySiteButton() { const cfg = getSiteBtnSettings() if (!cfg?.selector) return let target try { target = document.querySelector(cfg.selector) } catch (e) { return } if (!target) return const posRaw = (cfg.position || '').trim() const pos = posRaw === 'before' ? 'before' : posRaw === 'inside' ? 'inside' : 'after' const exists = pos === 'inside' ? !!target.querySelector('.iu-insert-btn') : pos === 'before' ? !!( target.previousElementSibling && target.previousElementSibling.classList?.contains( 'iu-insert-btn' ) ) : !!( target.nextElementSibling && target.nextElementSibling.classList?.contains('iu-insert-btn') ) if (exists) return let btn const content = (cfg.text || '插入图片').trim() try { const t = document.createElement('template') t.innerHTML = content if (t.content && t.content.childElementCount === 1) { btn = t.content.firstElementChild } } catch {} if (!btn) { btn = createEl('button', { class: 'iu-insert-btn', text: content }) } else { btn.classList.add('iu-insert-btn') } btn.addEventListener('click', (event) => { panel.style.display = 'block' event.preventDefault() try { openFilePicker() } catch {} }) if (pos === 'before') { target.insertAdjacentElement('beforebegin', btn) } else if (pos === 'inside') { target.insertAdjacentElement('beforeend', btn) } else { target.insertAdjacentElement('afterend', btn) } } applySiteButton() // SPA/延迟渲染:观察 DOM,目标出现即插入按钮 let siteBtnObserver function restartSiteButtonObserver() { try { if (siteBtnObserver) siteBtnObserver.disconnect() } catch {} const cfg = getSiteBtnSettings() if (!cfg?.selector) { siteBtnObserver = null return } let inserted = false const checkAndInsert = () => { if (inserted) return let target try { target = document.querySelector(cfg.selector) } catch (e) { return } if (!target) return const posRaw = (cfg.position || '').trim() const pos = posRaw === 'before' ? 'before' : posRaw === 'inside' ? 'inside' : 'after' const exists = pos === 'inside' ? !!target.querySelector('.iu-insert-btn') : pos === 'before' ? !!( target.previousElementSibling && target.previousElementSibling.classList?.contains( 'iu-insert-btn' ) ) : !!( target.nextElementSibling && target.nextElementSibling.classList?.contains('iu-insert-btn') ) if (!exists) { applySiteButton() } const existsAfter = pos === 'inside' ? !!target.querySelector('.iu-insert-btn') : pos === 'before' ? !!( target.previousElementSibling && target.previousElementSibling.classList?.contains( 'iu-insert-btn' ) ) : !!( target.nextElementSibling && target.nextElementSibling.classList?.contains('iu-insert-btn') ) if (existsAfter) { inserted = true try { siteBtnObserver.disconnect() } catch {} } } checkAndInsert() siteBtnObserver = new MutationObserver(() => checkAndInsert()) siteBtnObserver.observe(document.body || document.documentElement, { childList: true, subtree: true, }) } restartSiteButtonObserver() // Drop 覆盖层 const drop = createEl('div', { id: 'iu-drop', text: t('drop_overlay') }) document.body.appendChild(drop) // 队列与并发 const queue = [] let running = 0 let done = 0 let total = 0 const CONCURRENCY = 3 function updateProgress() { progressEl.textContent = tpl(t('progress_done'), { done, total }) } function addLog(text) { list.prepend(createEl('div', { class: 'item', text })) } async function processQueue() { while (running < CONCURRENCY && queue.length) { const item = queue.shift() running++ addLog(`${t('log_uploading')}${item.file.name}`) try { const link = await uploadToImgur(item.file) const fmt = getFormat() const out = formatText(link, item.file.name, fmt) copyAndInsert(out) addToHistory({ link, name: item.file.name, ts: Date.now(), pageUrl: location.href, }) addLog(`${t('log_success')}${item.file.name} → ${link}`) } catch (e) { addLog(`${t('log_failed')}${item.file.name}(${e?.message || e})`) } finally { running-- done++ updateProgress() } } } function handleFiles(files) { const imgs = files.filter((f) => f.type.includes('image')) if (!imgs.length) return total += imgs.length updateProgress() imgs.forEach((file) => queue.push({ file })) processQueue() } // 粘贴图片 document.addEventListener( 'paste', (event) => { const items = event.clipboardData?.items if (!items) return const imageItem = Array.from(items).find((i) => i.type.includes('image') ) const file = imageItem?.getAsFile() if (file) handleFiles([file]) }, true ) // 拖拽上传 document.addEventListener('dragover', (e) => { drop.classList.add('show') e.preventDefault() }) document.addEventListener('dragleave', () => drop.classList.remove('show')) document.addEventListener('drop', (event) => { drop.classList.remove('show') event.preventDefault() const files = event.dataTransfer?.files if (files?.length) handleFiles(Array.from(files)) }) function renderHistory() { history.innerHTML = '' const header = createEl('div', { class: 'controls' }) header.appendChild( createEl('span', { text: tpl(t('btn_history_count'), { count: loadHistory().length }), }) ) const clearBtn = createEl('button', { text: t('btn_clear_history') }) clearBtn.addEventListener('click', () => { saveHistory([]) renderHistory() }) header.appendChild(clearBtn) history.appendChild(header) const listWrap = createEl('div', { class: 'list' }) const items = loadHistory() items.forEach((it) => { const row = createEl('div', { class: 'row' }) // 预览图片 const preview = createEl('img', { src: it.link, style: 'width:48px;height:48px;object-fit:cover;border-radius:4px;border:1px solid #334155;', }) row.appendChild(preview) // 信息栏:名称与来源网址 const info = createEl('div', { style: 'flex:1;min-width:0;display:flex;flex-direction:column;gap:4px;padding:0 8px;', }) info.appendChild( createEl('span', { class: 'name', text: it.name || it.link, title: it.name || it.link, }) ) if (it.pageUrl) { let host = it.pageUrl try { host = new URL(it.pageUrl).hostname } catch {} const pageLink = createEl('a', { href: it.pageUrl, text: tpl(t('history_upload_page'), { host }), target: '_blank', rel: 'noopener noreferrer', style: 'color:#93c5fd;text-decoration:none;font-size:11px;', }) info.appendChild(pageLink) } row.appendChild(info) const ops = createEl('div', { class: 'ops' }) const copyBtn = createEl('button', { text: t('btn_copy') }) copyBtn.addEventListener('click', () => { const fmt = getFormat() const out = formatText( it.link, it.name || t('default_image_name'), fmt ) copyAndInsert(out) }) const openBtn = createEl('button', { text: t('btn_open') }) openBtn.addEventListener('click', () => window.open(it.link, '_blank')) ops.appendChild(copyBtn) ops.appendChild(openBtn) row.appendChild(ops) listWrap.appendChild(row) }) history.appendChild(listWrap) } // 监听历史记录的变化,实时刷新列表 try { if (typeof GM_addValueChangeListener === 'function') { GM_addValueChangeListener( HISTORY_KEY, function (name, oldValue, newValue, remote) { renderHistory() } ) } } catch {} GM_registerMenuCommand(t('menu_open_panel'), () => { panel.style.display = 'block' }) GM_registerMenuCommand(t('menu_select_images'), () => { panel.style.display = 'block' openFilePicker() }) GM_registerMenuCommand(t('menu_settings'), () => { panel.style.display = 'block' panel.classList.add('show-settings') try { refreshSettingsUI() } catch {} }) return { handleFiles } } // 初始化 if (!document.getElementById('iu-panel')) { const { handleFiles } = createPanel() // 支持通过菜单外部触发(例如其他脚本集成) window.addEventListener('iu:uploadFiles', (e) => { const files = e.detail?.files if (files?.length) handleFiles(files) }) } })()