Universal Image Uploader

Paste/drag/select images, batch upload to Imgur; auto-copy Markdown/HTML/BBCode/link; site button integration with SPA observer; local history.

As of 14.11.2025. See ბოლო ვერსია.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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.4.0
// @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              https://*.v2ex.com/*
// @match              https://*.v2ex.co/*
// @match              https://greasyfork.org/*
// @match              https://www.nodeseek.com/*
// @match              https://www.deepflood.com/*
// @match              https://2libra.com/*
// @match              *://*/*
// @grant              GM_setValue
// @grant              GM_getValue
// @grant              GM_addStyle
// @grant              GM_deleteValue
// @grant              GM_registerMenuCommand
// @grant              GM_setClipboard
// @grant              GM_addValueChangeListener
// @grant              GM.xmlhttpRequest
// @grant              GM_xmlhttpRequest
// @connect            api.imgur.com
// @connect            tikolu.net
// ==/UserScript==

;(function () {
  'use strict'

  // CONFIG: Preset site configuration
  // - Key: site hostname without port; strip leading 'www.'
  // - format: default text format for insertion
  // - host: default image provider ('imgur' | 'tikolu')
  // - proxy: default proxy for non-Imgur links ('none' | 'wsrv.nl')
  // - buttons: site-specific button injection rules
  const CONFIG = {
    // Examples: local preview page and common sites; add/remove as needed
    localhost: {
      format: 'markdown',
      host: 'imgur',
      proxy: 'none',
      buttons: [{ selector: 'textarea', position: 'after', text: '插入图片' }],
    },
    'v2ex.com': {
      format: 'link',
      host: 'imgur',
      proxy: 'none',
      buttons: [
        {
          selector: '#reply-box > div.cell.flex-one-row > div:nth-child(1)',
          position: 'inside',
          text: '<a style="padding-left: 10px;"> + 插入图片</a>',
        },
        {
          selector: '#tab-preview',
          position: 'after',
          text: '<a class="tab-alt"> + 插入图片</a>',
        },
        {
          selector: 'button[onclick^="previewTopicContent"]',
          position: 'before',
          text: `<button type="button" class="super normal button" style="margin-right: 12px;"><li class="fa fa-plus"></li> &nbsp;插入图片</button>`,
        },
      ],
    },
    'greasyfork.org': {
      format: 'markdown',
      host: 'tikolu',
      proxy: 'wsrv.nl',
      buttons: [
        {
          selector: '.comment-screenshot-control',
          position: 'before',
        },
      ],
    },
    'nodeseek.com': {
      format: 'markdown',
      host: 'tikolu',
      proxy: 'wsrv.nl',
      buttons: [
        {
          selector:
            '#editor-body > div.mde-toolbar > .toolbar-item:last-of-type',
          position: 'after',
          text: '插入图片',
        },
      ],
    },
    'deepflood.com': {
      format: 'markdown',
      host: 'tikolu',
      proxy: 'wsrv.nl',
      buttons: [
        {
          selector:
            '#editor-body > div.mde-toolbar > .toolbar-item:last-of-type',
          position: 'after',
          text: '插入图片',
        },
      ],
    },
    '2libra.com': {
      format: 'markdown',
      host: 'tikolu',
      proxy: 'wsrv.nl',
      buttons: [
        {
          selector:
            '.w-md-editor > div.w-md-editor-toolbar > ul:nth-child(1) > li:last-of-type',
          position: 'after',
          text: '插入图片',
        },
      ],
    },
    'github.com': { format: 'markdown', host: 'tikolu', proxy: 'wsrv.nl' },
  }

  // 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',
      host_imgur: 'Imgur',
      host_tikolu: 'Tikolu',
      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',
      btn_delete: 'Delete',
      btn_edit: 'Edit',
      btn_update: 'Update',
      btn_cancel: 'Cancel',
      menu_open_panel: 'Open upload panel',
      menu_select_images: 'Select images',
      menu_settings: 'Settings',
      formats_section_title: 'Custom Formats',
      placeholder_format_name: 'Format name',
      placeholder_format_template: 'Format template',
      example_format_template: 'Example: {name} - {link}',
      btn_add_format: 'Add format',
      formats_col_name: 'Name',
      formats_col_template: 'Format',
      formats_col_ops: 'Actions',
      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',
      proxy_none: 'No proxy',
      proxy_wsrv_nl: 'wsrv.nl',
      error_network: 'Network error',
      error_upload_failed: 'Upload failed',
    },
    'zh-CN': {
      header_title: '通用图片上传助手',
      btn_history: '历史',
      btn_settings: '设置',
      btn_close: '关闭',
      format_markdown: 'Markdown',
      format_html: 'HTML',
      format_bbcode: 'BBCode',
      format_link: '链接',
      host_imgur: 'Imgur',
      host_tikolu: 'Tikolu',
      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: '打开',
      btn_delete: '删除',
      btn_edit: '编辑',
      btn_update: '更新',
      btn_cancel: '取消',
      menu_open_panel: '打开图片上传面板',
      menu_select_images: '选择图片',
      menu_settings: '设置',
      formats_section_title: '自定义格式',
      placeholder_format_name: '格式名称',
      placeholder_format_template: '格式内容',
      example_format_template: '示例:{name} - {link}',
      btn_add_format: '添加格式',
      formats_col_name: '名字',
      formats_col_template: '格式',
      formats_col_ops: '操作',
      history_upload_page_prefix: '上传页面:',
      history_upload_page: '上传页面:{host}',
      btn_history_count: '历史({count})',
      btn_clear_history: '清空',
      default_image_name: '图片',
      proxy_none: '无代理',
      proxy_wsrv_nl: 'wsrv.nl',
      error_network: '网络错误',
      error_upload_failed: '上传失败',
    },
    'zh-TW': {
      header_title: '通用圖片上傳助手',
      btn_history: '歷史',
      btn_settings: '設定',
      btn_close: '關閉',
      format_markdown: 'Markdown',
      format_html: 'HTML',
      format_bbcode: 'BBCode',
      format_link: '連結',
      host_imgur: 'Imgur',
      host_tikolu: 'Tikolu',
      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: '打開',
      btn_delete: '刪除',
      btn_edit: '編輯',
      btn_update: '更新',
      btn_cancel: '取消',
      menu_open_panel: '打開圖片上傳面板',
      menu_select_images: '選擇圖片',
      menu_settings: '設定',
      formats_section_title: '自訂格式',
      placeholder_format_name: '格式名稱',
      placeholder_format_template: '格式內容',
      example_format_template: '範例:{name} - {link}',
      btn_add_format: '新增格式',
      formats_col_name: '名稱',
      formats_col_template: '格式',
      formats_col_ops: '操作',
      history_upload_page_prefix: '上傳頁面:',
      history_upload_page: '上傳頁面:{host}',
      btn_history_count: '歷史({count})',
      btn_clear_history: '清空',
      default_image_name: '圖片',
      proxy_none: '不使用代理',
      proxy_wsrv_nl: 'wsrv.nl',
      error_network: '網路錯誤',
      error_upload_failed: '上傳失敗',
    },
  }

  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 pool (see upload-image.ts)
  const IMGUR_CLIENT_IDS = [
    '3107b9ef8b316f3',
    '442b04f26eefc8a',
    '59cfebe717c09e4',
    '60605aad4a62882',
    '6c65ab1d3f5452a',
    '83e123737849aa9',
    '9311f6be1c10160',
    'c4a4a563f698595',
    '81be04b9e4a08ce',
  ]

  const HISTORY_KEY = 'uiu_history'
  const FORMAT_MAP_KEY = 'uiu_format_map' // legacy
  const BTN_SETTINGS_MAP_KEY = 'uiu_site_btn_settings_map' // legacy
  const HOST_MAP_KEY = 'uiu_host_map' // legacy
  const PROXY_MAP_KEY = 'uiu_proxy_map' // legacy
  const SITE_SETTINGS_MAP_KEY = 'uiu_site_settings_map'
  const CUSTOM_FORMATS_KEY = 'uiu_custom_formats'
  const DEFAULT_FORMAT = 'markdown'
  const DEFAULT_HOST = 'tikolu'
  const DEFAULT_PROXY = 'wsrv.nl'
  // Global allowed value lists
  const ALLOWED_FORMATS = ['markdown', 'html', 'bbcode', 'link']
  const ALLOWED_HOSTS = ['imgur', 'tikolu']
  const ALLOWED_PROXIES = ['none', 'wsrv.nl']
  const ALLOWED_BUTTON_POSITIONS = ['before', 'inside', 'after']
  const DEFAULT_BUTTON_POSITION = 'after'

  // Migrate legacy storage keys from older versions (iu_*) to new (uiu_*) - v0.1 to v0.2
  function migrateLegacyStorage() {
    try {
      const maybeMove = (oldKey, newKey) => {
        const hasNew = GM_getValue(newKey, undefined) !== undefined
        const oldVal = GM_getValue(oldKey, undefined)
        const hasOld = oldVal !== undefined
        if (!hasNew && hasOld) {
          GM_setValue(newKey, oldVal)
          try {
            if (typeof GM_deleteValue === 'function') GM_deleteValue(oldKey)
          } catch {}
        }
      }
      maybeMove('iu_history', HISTORY_KEY)
      maybeMove('iu_format_map', FORMAT_MAP_KEY)
      maybeMove('iu_site_btn_settings_map', BTN_SETTINGS_MAP_KEY)
    } catch {}
  }

  // Run migration early before any reads/writes
  migrateLegacyStorage()

  // Utility: normalize a host string consistently (trim and strip leading 'www.')
  function normalizeHost(h) {
    try {
      h = String(h || '').trim()
      return h.startsWith('www.') ? h.slice(4) : h
    } catch {
      return h
    }
  }

  /**
   * ensureAllowedValue
   * Returns `value` if it is contained in `allowedValues`,
   * otherwise returns `defaultValue` (or `undefined` when omitted).
   *
   * - `allowedValues` may be any array; non-array or empty lists yield `defaultValue`/`undefined`.
   * - Optimizes lookups for larger lists via `Set`.
   * - Does not coerce types; comparison is strict equality against items in `allowedValues`.
   */
  function ensureAllowedValue(value, allowedValues, defaultValue) {
    if (!Array.isArray(allowedValues) || allowedValues.length === 0) {
      return defaultValue
    }
    if (allowedValues.length < 8) {
      return allowedValues.includes(value) ? value : defaultValue
    }
    const set = new Set(allowedValues)
    return set.has(value) ? value : defaultValue
  }

  // Global custom formats: [{ name: string, template: string }]
  function getCustomFormats() {
    try {
      const list = GM_getValue(CUSTOM_FORMATS_KEY, []) || []
      if (!Array.isArray(list)) return []
      return list
        .map((it) => ({
          name: String(it?.name || '').trim(),
          template: String(it?.template || ''),
        }))
        .filter((it) => it.name && it.template)
    } catch {
      return []
    }
  }
  function setCustomFormats(list) {
    try {
      const arr = Array.isArray(list) ? list : []
      const normalized = arr
        .map((it) => ({
          name: String(it?.name || '').trim(),
          template: String(it?.template || ''),
        }))
        .filter((it) => it.name && it.template)
      // de-duplicate by name (last wins)
      const map = new Map()
      normalized.forEach((it) => map.set(it.name, it.template))
      const out = Array.from(map.entries()).map(([name, template]) => ({
        name,
        template,
      }))
      GM_setValue(CUSTOM_FORMATS_KEY, out)
    } catch {}
  }
  function upsertCustomFormat(name, template) {
    try {
      name = String(name || '').trim()
      template = String(template || '')
      if (!name || !template) return
      const list = getCustomFormats()
      const idx = list.findIndex((it) => it.name === name)
      if (idx >= 0) list[idx] = { name, template }
      else list.push({ name, template })
      setCustomFormats(list)
    } catch {}
  }
  function removeCustomFormat(name) {
    try {
      name = String(name || '').trim()
      if (!name) return
      const list = getCustomFormats().filter((it) => it.name !== name)
      setCustomFormats(list)
    } catch {}
  }
  function getAllowedFormats() {
    try {
      return [...ALLOWED_FORMATS, ...getCustomFormats().map((f) => f.name)]
    } catch {
      return [...ALLOWED_FORMATS]
    }
  }
  function ensureAllowedFormat(fmt) {
    return ensureAllowedValue(fmt, getAllowedFormats(), DEFAULT_FORMAT)
  }

  // Migrate existing separate maps (format/host/proxy/buttons) into unified per-domain map - v0.2 to v0.3 and later
  function migrateToUnifiedSiteMap() {
    try {
      const existing = GM_getValue(SITE_SETTINGS_MAP_KEY, undefined)
      const siteMap = existing && typeof existing === 'object' ? existing : {}
      const isEmpty = !siteMap || Object.keys(siteMap).length === 0
      // Only migrate if the unified map is empty to avoid overwriting user settings
      if (!isEmpty) return

      const formatMap = GM_getValue(FORMAT_MAP_KEY, {}) || {}
      const hostMap = GM_getValue(HOST_MAP_KEY, {}) || {}
      const proxyMap = GM_getValue(PROXY_MAP_KEY, {}) || {}
      const btnMap = GM_getValue(BTN_SETTINGS_MAP_KEY, {}) || {}

      const rawKeys = new Set([
        ...Object.keys(formatMap),
        ...Object.keys(hostMap),
        ...Object.keys(proxyMap),
        ...Object.keys(btnMap),
        ...Object.keys(CONFIG || {}),
      ])
      const keys = new Set()
      rawKeys.forEach((k) => keys.add(normalizeHost(k)))

      keys.forEach((key) => {
        if (!key) return
        const preset = CONFIG?.[key] || {}
        const s = siteMap[key] || {}
        // Format
        if (s.format === undefined) {
          const fmt = formatMap[key] ?? preset.format
          const normalizedFormat = ensureAllowedFormat(fmt)
          if (normalizedFormat) s.format = normalizedFormat
        }
        // Host
        if (s.host === undefined) {
          const h = hostMap[key] ?? preset.host
          const normalizedHost = ensureAllowedValue(h, ALLOWED_HOSTS)
          if (normalizedHost) s.host = normalizedHost
        }
        // Proxy (Due to legacy logic, do not persist 'none', convert 'none' to undefined)
        if (s.proxy === undefined) {
          const px = proxyMap[key] ?? preset.proxy
          const resolved = ensureAllowedValue(px, ALLOWED_PROXIES)
          if (resolved && resolved !== 'none') s.proxy = resolved
        }
        // Buttons
        if (s.buttons === undefined) {
          const raw = btnMap[key] ?? preset.buttons ?? preset.button ?? []
          const arr = Array.isArray(raw) ? raw : raw ? [raw] : []
          const list = arr
            .map((c) => {
              const selector = String(c?.selector || '').trim()
              if (!selector) return null
              const p = String(c?.position || '').trim()
              const pos = ensureAllowedValue(
                p,
                ALLOWED_BUTTON_POSITIONS,
                DEFAULT_BUTTON_POSITION
              )
              const text = String(
                c?.text || t('insert_image_button_default')
              ).trim()
              return { selector, position: pos, text }
            })
            .filter(Boolean)
          if (list.length) s.buttons = list
        }
        if (Object.keys(s).length > 0) siteMap[key] = s
      })

      GM_setValue(SITE_SETTINGS_MAP_KEY, siteMap)
      // Optionally clear legacy keys to avoid duplication
      try {
        if (typeof GM_deleteValue === 'function') {
          GM_deleteValue(FORMAT_MAP_KEY)
          GM_deleteValue(HOST_MAP_KEY)
          GM_deleteValue(PROXY_MAP_KEY)
          GM_deleteValue(BTN_SETTINGS_MAP_KEY)
        }
      } catch {}
    } catch {}
  }

  migrateToUnifiedSiteMap()

  // Apply preset config to unified storage (only set missing fields)
  function applyPresetConfig() {
    try {
      const siteMap = GM_getValue(SITE_SETTINGS_MAP_KEY, {}) || {}
      let changed = false
      Object.entries(CONFIG || {}).forEach(([host, preset]) => {
        const key = normalizeHost(host)
        if (!key || typeof preset !== 'object') return
        const s = siteMap[key] || {}
        // format
        if (s.format === undefined && preset.format) {
          const normalizedFormat = ensureAllowedValue(
            preset.format,
            ALLOWED_FORMATS
          )
          if (normalizedFormat) {
            s.format = normalizedFormat
            changed = true
          }
        }
        // host
        if (s.host === undefined && preset.host) {
          const normalizedHost = ensureAllowedValue(preset.host, ALLOWED_HOSTS)
          if (normalizedHost) {
            s.host = normalizedHost
            changed = true
          }
        }
        // proxy
        if (s.proxy === undefined && preset.proxy) {
          const resolved = ensureAllowedValue(preset.proxy, ALLOWED_PROXIES)
          if (resolved) {
            s.proxy = resolved
            changed = true
          }
        }
        // buttons
        if (s.buttons === undefined) {
          const raw = preset.buttons || preset.button || []
          const arr = Array.isArray(raw) ? raw : raw ? [raw] : []
          const list = arr
            .map((c) => {
              const selector = String(c?.selector || '').trim()
              if (!selector) return null
              const p = String(c?.position || '').trim()
              const pos = ensureAllowedValue(
                p,
                ALLOWED_BUTTON_POSITIONS,
                DEFAULT_BUTTON_POSITION
              )
              const text = String(
                c?.text || t('insert_image_button_default')
              ).trim()
              return { selector, position: pos, text }
            })
            .filter(Boolean)
          if (list.length) {
            s.buttons = list
            changed = true
          }
        }
        if (changed) siteMap[key] = s
      })
      if (changed) GM_setValue(SITE_SETTINGS_MAP_KEY, siteMap)
    } catch {}
  }

  // Initialize once at runtime
  applyPresetConfig()

  const SITE_KEY = normalizeHost(location.hostname || '')
  const getSiteSettingsMap = () => GM_getValue(SITE_SETTINGS_MAP_KEY, {})
  const setSiteSettingsMap = (map) => GM_setValue(SITE_SETTINGS_MAP_KEY, map)
  const getCurrentSiteSettings = () => {
    const map = getSiteSettingsMap()
    return map[SITE_KEY] || {}
  }
  const updateCurrentSiteSettings = (updater) => {
    const map = getSiteSettingsMap()
    const key = SITE_KEY
    const current = map[key] || {}
    const partial =
      typeof updater === 'function'
        ? updater({ ...current })
        : { ...(updater || {}) }
    const next = { ...current, ...partial }
    // sanitize format (allow built-ins and user custom formats)
    if (Object.prototype.hasOwnProperty.call(next, 'format')) {
      const resolvedFormat = ensureAllowedFormat(next.format)
      if (resolvedFormat) next.format = resolvedFormat
      else delete next.format
    }
    // sanitize host
    if (Object.prototype.hasOwnProperty.call(next, 'host')) {
      const resolvedHost = ensureAllowedValue(next.host, ALLOWED_HOSTS)
      if (resolvedHost) next.host = resolvedHost
      else delete next.host
    }
    // sanitize proxy
    if (Object.prototype.hasOwnProperty.call(next, 'proxy')) {
      const resolved = ensureAllowedValue(next.proxy, ALLOWED_PROXIES)
      if (resolved) next.proxy = resolved
      else delete next.proxy
    }
    // sanitize buttons (empty or falsy removes the field)
    if (Object.prototype.hasOwnProperty.call(next, 'buttons')) {
      const list = next.buttons
      if (!list || !Array.isArray(list) || list.length === 0) {
        delete next.buttons
      }
    }
    // persist
    if (!next || Object.keys(next).length === 0) {
      if (map[key]) delete map[key]
    } else {
      map[key] = next
    }
    setSiteSettingsMap(map)
  }
  const getFormat = () => {
    const s = getCurrentSiteSettings()
    return s.format || DEFAULT_FORMAT
  }
  const setFormat = (format) => {
    updateCurrentSiteSettings({ format })
  }
  const getHost = () => {
    const s = getCurrentSiteSettings()
    return s.host || DEFAULT_HOST
  }
  const setHost = (host) => {
    updateCurrentSiteSettings({ host })
  }
  const getProxy = () => {
    const s = getCurrentSiteSettings()
    return s.proxy || DEFAULT_PROXY
  }
  const setProxy = (proxy) => {
    updateCurrentSiteSettings({ proxy })
  }
  // Support multiple site button configurations
  const getSiteBtnSettingsList = () => {
    const s = getCurrentSiteSettings()
    const val = s.buttons || []
    return Array.isArray(val) ? val : val?.selector ? [val] : []
  }
  const setSiteBtnSettingsList = (list) => {
    updateCurrentSiteSettings({ buttons: list })
  }
  const addSiteBtnSetting = (cfg) => {
    const selector = (cfg?.selector || '').trim()
    if (!selector) return
    const p = (cfg?.position || '').trim()
    const pos = ensureAllowedValue(
      p,
      ALLOWED_BUTTON_POSITIONS,
      DEFAULT_BUTTON_POSITION
    )
    const text = (cfg?.text || t('insert_image_button_default')).trim()
    const list = getSiteBtnSettingsList()
    list.push({ selector, position: pos, text })
    setSiteBtnSettingsList(list)
  }
  const removeSiteBtnSetting = (index) => {
    const list = getSiteBtnSettingsList()
    if (index >= 0 && index < list.length) {
      list.splice(index, 1)
      setSiteBtnSettingsList(list)
    }
  }
  const updateSiteBtnSetting = (index, cfg) => {
    const list = getSiteBtnSettingsList()
    if (!list || index < 0 || index >= list.length) return
    const selector = (cfg?.selector || '').trim()
    if (!selector) return
    const p = (cfg?.position || '').trim()
    const pos = ensureAllowedValue(
      p,
      ALLOWED_BUTTON_POSITIONS,
      DEFAULT_BUTTON_POSITION
    )
    const text = (cfg?.text || t('insert_image_button_default')).trim()
    list[index] = { selector, position: pos, text }
    setSiteBtnSettingsList(list)
  }
  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
  }

  // Helper: build button position options for a select element
  // selectedValue is optional; defaults to DEFAULT_BUTTON_POSITION when absent/invalid
  const buildPositionOptions = (selectEl, selectedValue) => {
    if (!selectEl) return
    // Avoid Trusted Types violation: clear without using innerHTML
    selectEl.textContent = ''
    const selected = selectedValue
      ? ensureAllowedValue(
          selectedValue,
          ALLOWED_BUTTON_POSITIONS,
          DEFAULT_BUTTON_POSITION
        )
      : DEFAULT_BUTTON_POSITION
    ALLOWED_BUTTON_POSITIONS.forEach((value) => {
      const opt = createEl('option', { value, text: t('pos_' + value) })
      if (value === selected) opt.selected = true
      selectEl.appendChild(opt)
    })
  }

  // Helper: build format options
  const buildFormatOptions = (selectEl, selectedValue) => {
    if (!selectEl) return
    // Avoid Trusted Types violation: clear without using innerHTML
    selectEl.textContent = ''
    const selected = selectedValue
      ? ensureAllowedFormat(selectedValue)
      : DEFAULT_FORMAT
    const builtins = ALLOWED_FORMATS
    const customs = getCustomFormats()
    builtins.forEach((val) => {
      const opt = createEl('option', { value: val, text: t('format_' + val) })
      if (val === selected) opt.selected = true
      selectEl.appendChild(opt)
    })
    customs.forEach((cf) => {
      const opt = createEl('option', { value: cf.name, text: cf.name })
      if (cf.name === selected) opt.selected = true
      selectEl.appendChild(opt)
    })
  }

  // Helper: build host options
  const buildHostOptions = (selectEl, selectedValue) => {
    if (!selectEl) return
    // Avoid Trusted Types violation: clear without using innerHTML
    selectEl.textContent = ''
    const selected = selectedValue
      ? ensureAllowedValue(selectedValue, ALLOWED_HOSTS, DEFAULT_HOST)
      : DEFAULT_HOST
    ALLOWED_HOSTS.forEach((val) => {
      const opt = createEl('option', { value: val, text: t('host_' + val) })
      if (val === selected) opt.selected = true
      selectEl.appendChild(opt)
    })
  }

  // Helper: build proxy options
  const buildProxyOptions = (selectEl, selectedValue) => {
    if (!selectEl) return
    // Avoid Trusted Types violation: clear without using innerHTML
    selectEl.textContent = ''
    const selected = selectedValue
      ? ensureAllowedValue(selectedValue, ALLOWED_PROXIES, DEFAULT_PROXY)
      : DEFAULT_PROXY
    const proxyLabelKey = (val) =>
      val === 'wsrv.nl' ? 'proxy_wsrv_nl' : 'proxy_none'
    ALLOWED_PROXIES.forEach((val) => {
      const opt = createEl('option', {
        value: val,
        text: t(proxyLabelKey(val)),
      })
      if (val === selected) opt.selected = true
      selectEl.appendChild(opt)
    })
  }

  const css = `
  #uiu-panel { position: fixed; right: 16px; bottom: 16px; z-index: 999999; width: 440px; max-height: calc(100vh - 32px); overflow: auto; 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; font-size: 13px; line-height: 1.5; }
  #uiu-panel header { display:flex; align-items:center; justify-content:space-between; padding: 10px 12px; font-weight: 600; font-size: 16px; background-color: unset; box-shadow: unset; transition: unset; }
  #uiu-panel header .uiu-actions { display:flex; gap:8px; }
  #uiu-panel header .uiu-actions button { font-size: 12px; }
  /* Active styles for toggles when sections are open */
  #uiu-panel header.uiu-show-history .uiu-actions .uiu-toggle-history { background:#2563eb; border-color:#1d4ed8; box-shadow: 0 0 0 1px #1d4ed8 inset; color:#fff; }
  #uiu-panel header.uiu-show-settings .uiu-actions .uiu-toggle-settings { background:#2563eb; border-color:#1d4ed8; box-shadow: 0 0 0 1px #1d4ed8 inset; color:#fff; }
  #uiu-panel .uiu-body { padding: 8px 12px; }
  #uiu-panel .uiu-controls { display:flex; align-items:center; gap:8px; flex-wrap: wrap; }
  #uiu-panel select, #uiu-panel button { font-size: 12px; padding: 6px 10px; border-radius: 6px; border: 1px solid #334155; background:#1f2937; color:#fff; }
  #uiu-panel button.uiu-primary { background:#2563eb; border-color:#1d4ed8; }
  #uiu-panel .uiu-list { margin-top:8px; max-height: 140px; overflow-y:auto; overflow-x:hidden; font-size: 12px; }
  #uiu-panel .uiu-list .uiu-item { padding:6px 0; border-bottom: 1px dashed #334155; white-space: normal; word-break: break-word; overflow-wrap: anywhere; }
  #uiu-panel .uiu-history { display:none; margin-top:12px; border-top: 2px solid #475569; padding-top: 8px; }
  #uiu-panel header.uiu-show-history + .uiu-body .uiu-history { display:block; }
  #uiu-panel .uiu-history .uiu-controls > span { font-size: 16px; font-weight: 600;}
  #uiu-panel .uiu-history .uiu-list { max-height: 240px; }
  #uiu-panel .uiu-history .uiu-row { display:flex; align-items:center; justify-content:space-between; gap:8px; padding:6px 0; border-bottom: 1px dashed #334155; }
  #uiu-panel .uiu-history .uiu-row .uiu-ops { display:flex; gap:6px; }
  #uiu-panel .uiu-history .uiu-row .uiu-name { display:block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  #uiu-panel .uiu-hint { font-size: 11px; opacity:.85; margin-top:6px; }
  /* Settings container toggling */
  #uiu-panel .uiu-settings-container { display:none; margin-top:12px; border-top: 2px solid #475569; padding-top: 8px; }
  #uiu-panel header.uiu-show-settings + .uiu-body .uiu-settings-container { display:block; }
  #uiu-panel .uiu-settings .uiu-controls > span { font-size: 16px; font-weight: 600;}
  #uiu-panel .uiu-settings .uiu-settings-list { margin-top:6px; max-height: 240px; overflow-y:auto; overflow-x:hidden; }
  #uiu-panel .uiu-settings .uiu-settings-row { display:flex; align-items:center; justify-content:space-between; gap:8px; padding:6px 0; border-bottom: 1px dashed #334155; font-size: 12px; flex-wrap: nowrap; }
  #uiu-panel .uiu-settings .uiu-settings-row .uiu-settings-item { flex:1; display:flex; align-items:center; gap:6px; min-width:0; }
  #uiu-panel .uiu-settings .uiu-settings-row .uiu-settings-item input[type="text"] { flex:1; min-width:0; }
  #uiu-panel .uiu-settings .uiu-settings-row .uiu-settings-item select { flex:0 0 auto; }
  #uiu-panel .uiu-settings .uiu-settings-row .uiu-ops { display:flex; gap:6px; flex-shrink:0; white-space:nowrap; }
  #uiu-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; }
  #uiu-drop.show { display:flex; }
  .uiu-insert-btn { cursor:pointer; }
  .uiu-insert-btn.uiu-default { font-size: 12px; padding: 4px 8px; border-radius: 6px; border: 1px solid #334155; background:#1f2937; color:#fff; cursor:pointer; }
  /* Hover effects for all buttons */
  #uiu-panel button { transition: background-color .12s ease, box-shadow .12s ease, transform .06s ease, opacity .12s ease, border-color .12s ease; }
  #uiu-panel button:hover { background:#334155; border-color:#475569; box-shadow: 0 0 0 1px #475569 inset; transform: translateY(-0.5px); }
  #uiu-panel button.uiu-primary:hover { background:#1d4ed8; border-color:#1e40af; }
  #uiu-panel button:active { transform: translateY(0); }
  /* Disabled style for proxy selector */
  #uiu-panel select:disabled { opacity:.55; cursor:not-allowed; filter: grayscale(80%); background:#111827; color:#9ca3af; border-color:#475569; }
  /* Custom Formats layout */
  #uiu-panel .uiu-formats { margin-top:12px; border-top: 2px solid #475569; padding-top: 8px; }
  #uiu-panel .uiu-formats .uiu-controls > span { font-size: 16px; font-weight: 600; }
  #uiu-panel .uiu-formats .uiu-formats-list { margin-top:6px; max-height: 200px; overflow-y:auto; overflow-x:hidden; }
  #uiu-panel .uiu-formats .uiu-formats-row { display:grid; grid-template-columns: 1fr 2fr 180px; align-items:center; gap:8px; padding:6px 0; border-bottom: 1px dashed #334155; }
  #uiu-panel .uiu-formats .uiu-formats-row .uiu-ops { display:flex; gap:6px; justify-content:flex-end; }
  #uiu-panel .uiu-formats .uiu-formats-row:not(.uiu-editing) .uiu-fmt-name, #uiu-panel .uiu-formats .uiu-formats-row:not(.uiu-editing) .uiu-fmt-template { display:block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  #uiu-panel .uiu-formats .uiu-formats-row.uiu-editing .uiu-fmt-name, #uiu-panel .uiu-formats .uiu-formats-row.uiu-editing .uiu-fmt-template { overflow: visible; text-overflow: clip; white-space: normal; }
  #uiu-panel .uiu-formats .uiu-form-add { display:grid; grid-template-columns: 1fr 2fr 180px; align-items:center; gap:8px; }
  #uiu-panel .uiu-formats .uiu-formats-row input[type="text"] { width:100%; }
  #uiu-panel .uiu-formats .uiu-form-add input[type="text"] { width:100%; }
  #uiu-panel .uiu-formats .uiu-form-add button { justify-self: end; }
  #uiu-panel .uiu-formats .uiu-formats-header { font-weight: 600; color:#e5e7eb; }
  #uiu-panel .uiu-formats .uiu-form-add .uiu-fmt-name, #uiu-panel .uiu-formats .uiu-form-add .uiu-fmt-template { display:block; min-width:0; }
  #uiu-panel .uiu-formats .uiu-format-example-row { padding-top:4px; border-bottom: none; }
  #uiu-panel .uiu-formats .uiu-format-example-row .uiu-fmt-template { font-size:12px; color:#cbd5e1; white-space: normal; overflow: visible; text-overflow: clip; }
  `
  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)
    // Custom format support: if fmt matches a user-defined template name
    try {
      const custom = getCustomFormats().find((cf) => cf.name === fmt)
      if (custom) {
        return tpl(custom.template, { link, name: alt })
      }
    } catch {}
    switch (fmt) {
      case 'html':
        return `<img src="${link}" alt="${alt}" />`
      case 'bbcode':
        return `[img]${link}[/img]`
      case 'link':
        return link
      default:
        return `![${alt}](${link})`
    }
  }

  function isImgurUrl(url) {
    try {
      const u = new URL(url)
      const h = u.hostname.toLowerCase()
      return h.includes('imgur.com')
    } catch {
      return false
    }
  }

  function applyProxy(url, providerKey) {
    try {
      const px = getProxy()
      if (px === 'none') return url
      const provider = providerKey || getHost()
      if (provider === 'imgur' || isImgurUrl(url)) return url
      if (px === 'wsrv.nl') {
        return `https://wsrv.nl/?url=${encodeURIComponent(url)}`
      }
      return url
    } catch {
      return url
    }
  }

  async function uploadToImgur(file) {
    // Shuffle Client-ID list to ensure a different ID on each retry
    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(t('error_network'))
          continue
        }
        const data = await res.json()
        if (data?.success && data?.data?.link) {
          return data.data.link
        }
        lastError = new Error(t('error_upload_failed'))
      } catch (e) {
        lastError = e
      }
    }

    throw lastError || new Error(t('error_upload_failed'))
  }

  async function uploadToTikolu(file) {
    // 8mb size limit (server also checks)
    if (Math.floor(file.size / 1000) > 8000) {
      throw new Error('8mb limit')
    }
    const formData = new FormData()
    formData.append('upload', true)
    formData.append('file', file)
    return new Promise((resolve, reject) => {
      const req =
        typeof GM !== 'undefined' && GM?.xmlHttpRequest
          ? GM.xmlHttpRequest
          : typeof GM_xmlhttpRequest !== 'undefined'
            ? GM_xmlhttpRequest
            : null
      if (!req) {
        reject(new Error('GM.xmlHttpRequest unavailable'))
        return
      }
      try {
        req({
          method: 'POST',
          url: 'https://tikolu.net/i/',
          data: formData,
          responseType: 'json',
          onload: (res) => {
            try {
              const data = res.response ?? JSON.parse(res.responseText || '{}')
              if (data?.status === 'uploaded' && data?.id) {
                resolve(`https://tikolu.net/i/${data.id}`)
              } else {
                reject(new Error(t('error_upload_failed')))
              }
            } catch (e) {
              reject(e)
            }
          },
          onerror: () => reject(new Error(t('error_network'))),
          ontimeout: () => reject(new Error(t('error_network'))),
        })
      } catch (e) {
        reject(e)
      }
    })
  }

  async function uploadImage(file) {
    const host = getHost()
    if (host === 'tikolu') return uploadToTikolu(file)
    // Default
    return uploadToImgur(file)
  }

  // Track last visited editable element to support insertion after focus is lost
  let lastEditableEl = null
  // Helper: get deepest active element across Shadow DOM and same-origin iframes
  function getDeepActiveElement() {
    let el = document.activeElement
    try {
      // Traverse into open shadow roots
      while (el && el.shadowRoot && el.shadowRoot.activeElement) {
        el = el.shadowRoot.activeElement
      }
      // Traverse into same-origin iframes
      while (
        el &&
        el.tagName === 'IFRAME' &&
        el.contentDocument &&
        el.contentDocument.activeElement
      ) {
        el = el.contentDocument.activeElement
      }
    } catch {}
    return el
  }
  // Helper: check if node is inside our UI panel (including its Shadow DOM)
  function isInsideUIPanel(node) {
    try {
      const host = document.getElementById('uiu-panel')
      if (!host || !node) return false
      if (host === node) return true
      if (host.contains(node)) return true
      const root = host.shadowRoot
      return root ? root.contains(node) : false
    } catch {}
    return false
  }
  function isTextInput(el) {
    if (!(el instanceof HTMLInputElement)) return false
    const type = (el.type || '').toLowerCase()
    return (
      type === 'text' ||
      type === 'search' ||
      type === 'url' ||
      type === 'email' ||
      type === 'tel'
    )
  }
  function isEditable(el) {
    return (
      el instanceof HTMLTextAreaElement ||
      isTextInput(el) ||
      (el instanceof HTMLElement && el.isContentEditable)
    )
  }
  document.addEventListener(
    'focusin',
    (e) => {
      // Use deep active element to handle Shadow DOM editors
      const deepTarget =
        getDeepActiveElement() ||
        (typeof e.composedPath === 'function' ? e.composedPath()[0] : e.target)
      if (
        deepTarget &&
        isEditable(deepTarget) &&
        !isInsideUIPanel(deepTarget)
      ) {
        lastEditableEl = deepTarget
      }
    },
    true
  )

  function insertIntoFocused(text) {
    let el = getDeepActiveElement()
    // Fallback to last editable target if current focus is not usable (or inside our panel)
    if (!isEditable(el) || isInsideUIPanel(el)) {
      el = lastEditableEl
      try {
        if (el && typeof el.focus === 'function') el.focus()
      } catch {}
    }
    if (!isEditable(el) || isInsideUIPanel(el)) return false
    try {
      if (el instanceof HTMLTextAreaElement || isTextInput(el)) {
        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) {
        // Ensure caret is inside the element, fallback to end
        try {
          const sel = window.getSelection()
          if (sel) {
            const range = document.createRange()
            range.selectNodeContents(el)
            range.collapse(false)
            sel.removeAllRanges()
            sel.addRange(range)
          }
        } catch {}
        document.execCommand('insertText', false, text)
        return true
      }
    } catch {}
    return false
  }

  function copyAndInsert(text) {
    try {
      GM_setClipboard(text)
    } catch {}
    insertIntoFocused(`\n${text}\n`)
  }

  function createPanel() {
    const panel = createEl('div', { id: 'uiu-panel' })
    // Attach Shadow DOM and inject scoped styles (convert '#uiu-panel' selectors to ':host')
    const root = panel.attachShadow({ mode: 'open' })
    try {
      const styleEl = document.createElement('style')
      styleEl.textContent = css.replace(/#uiu-panel\b/g, ':host')
      root.appendChild(styleEl)
    } catch {}
    const header = createEl('header')
    header.appendChild(createEl('span', { text: t('header_title') }))
    const actions = createEl('div', { class: 'uiu-actions' })
    const toggleHistoryBtn = createEl('button', {
      text: t('btn_history'),
      class: 'uiu-toggle-history',
    })
    toggleHistoryBtn.addEventListener('click', () => {
      header.classList.toggle('uiu-show-history')
      renderHistory()
      try {
        toggleHistoryBtn.setAttribute(
          'aria-pressed',
          header.classList.contains('uiu-show-history') ? 'true' : 'false'
        )
      } catch {}
    })
    const settingsBtn = createEl('button', {
      text: t('btn_settings'),
      class: 'uiu-toggle-settings',
    })
    settingsBtn.addEventListener('click', () => {
      header.classList.toggle('uiu-show-settings')
      try {
        refreshSettingsUI()
      } catch {}
      try {
        settingsBtn.setAttribute(
          'aria-pressed',
          header.classList.contains('uiu-show-settings') ? 'true' : 'false'
        )
      } 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: 'uiu-body' })
    const controls = createEl('div', { class: 'uiu-controls' })

    const format = getFormat()
    const formatSel = createEl('select')
    buildFormatOptions(formatSel, format)
    formatSel.addEventListener('change', () => setFormat(formatSel.value))

    const host = getHost()
    const hostSel = createEl('select')
    buildHostOptions(hostSel, host)
    hostSel.addEventListener('change', () => {
      setHost(hostSel.value)
      updateProxyState()
    })

    const proxy = getProxy()
    const proxySel = createEl('select')
    buildProxyOptions(proxySel, proxy)
    function updateProxyState() {
      const currentHost = hostSel.value
      if (currentHost === 'imgur') {
        proxySel.value = 'none'
        proxySel.disabled = true
        setProxy('none')
        try {
          renderHistory()
        } catch {}
      } else {
        proxySel.disabled = false
      }
    }
    updateProxyState()
    proxySel.addEventListener('change', () => {
      setProxy(proxySel.value)
      try {
        renderHistory()
      } catch {}
    })

    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()
    }

    const selectBtn = createEl('button', {
      class: 'uiu-primary',
      text: t('btn_select_images'),
    })
    selectBtn.addEventListener('click', openFilePicker)

    const progressEl = createEl('span', {
      class: 'uiu-progress',
      text: t('progress_initial'),
    })

    controls.appendChild(formatSel)
    controls.appendChild(hostSel)
    controls.appendChild(proxySel)
    controls.appendChild(selectBtn)
    controls.appendChild(progressEl)
    body.appendChild(controls)

    const list = createEl('div', { class: 'uiu-list' })
    body.appendChild(list)

    const hint = createEl('div', {
      class: 'uiu-hint',
      text: t('hint_text'),
    })
    body.appendChild(hint)

    const history = createEl('div', { class: 'uiu-history' })
    body.appendChild(history)

    // Parent container that groups Site Button Settings and Custom Formats
    const settingsContainer = createEl('div', {
      class: 'uiu-settings-container',
    })
    body.appendChild(settingsContainer)

    const settings = createEl('div', { class: 'uiu-settings' })
    const settingsHeader = createEl('div', { class: 'uiu-controls' })
    settingsHeader.appendChild(
      createEl('span', { text: t('settings_section_title') })
    )
    settings.appendChild(settingsHeader)
    const settingsForm = createEl('div', { class: 'uiu-controls' })
    const selInput = createEl('input', {
      type: 'text',
      placeholder: t('placeholder_css_selector'),
    })
    const posSel = createEl('select')
    buildPositionOptions(posSel)
    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', () => {
      addSiteBtnSetting({
        selector: selInput.value,
        position: posSel.value,
        text: textInput.value,
      })

      selInput.value = ''
      buildPositionOptions(posSel)
      textInput.value = t('insert_image_button_default')
      renderSettingsList()

      document.querySelectorAll('.uiu-insert-btn').forEach((el) => el.remove())
      applySiteButtons()
      try {
        restartSiteButtonObserver()
      } catch {}
    })
    const removeBtn = createEl('button', { text: t('btn_remove_button_temp') })
    removeBtn.addEventListener('click', () => {
      document.querySelectorAll('.uiu-insert-btn').forEach((el) => el.remove())
      try {
        if (siteBtnObserver) siteBtnObserver.disconnect()
      } catch {}
    })
    const clearBtn = createEl('button', { text: t('btn_clear_settings') })
    clearBtn.addEventListener('click', () => {
      setSiteBtnSettingsList([])
      renderSettingsList()
      document.querySelectorAll('.uiu-insert-btn').forEach((el) => el.remove())
      try {
        if (siteBtnObserver) siteBtnObserver.disconnect()
      } catch {}
    })
    const settingsList = createEl('div', { class: 'uiu-settings-list' })
    settings.appendChild(settingsList)
    settingsForm.appendChild(selInput)
    settingsForm.appendChild(posSel)
    settingsForm.appendChild(textInput)
    settingsForm.appendChild(saveBtn)
    settingsForm.appendChild(removeBtn)
    settingsForm.appendChild(clearBtn)
    settings.appendChild(settingsForm)
    settingsContainer.appendChild(settings)

    // Custom Formats section (below Site Button Settings)
    const formats = createEl('div', { class: 'uiu-formats' })
    const formatsHeader = createEl('div', { class: 'uiu-controls' })
    formatsHeader.appendChild(
      createEl('span', { text: t('formats_section_title') })
    )
    formats.appendChild(formatsHeader)
    // Column headers: Name | Format | Actions
    const formatsColsHeader = createEl('div', {
      class: 'uiu-formats-row uiu-formats-header',
    })
    formatsColsHeader.appendChild(
      createEl('span', { class: 'uiu-fmt-name', text: t('formats_col_name') })
    )
    formatsColsHeader.appendChild(
      createEl('span', {
        class: 'uiu-fmt-template',
        text: t('formats_col_template'),
      })
    )
    formatsColsHeader.appendChild(
      createEl('span', { class: 'uiu-ops', text: t('formats_col_ops') })
    )
    formats.appendChild(formatsColsHeader)
    const formatsForm = createEl('div', { class: 'uiu-controls uiu-form-add' })
    const fnameInput = createEl('input', {
      type: 'text',
      placeholder: t('placeholder_format_name'),
    })
    const ftemplateInput = createEl('input', {
      type: 'text',
      placeholder: t('placeholder_format_template'),
    })
    const addFmtBtn = createEl('button', { text: t('btn_add_format') })
    addFmtBtn.addEventListener('click', () => {
      const name = (fnameInput.value || '').trim()
      const tplStr = String(ftemplateInput.value || '')
      if (!name || !tplStr) return
      upsertCustomFormat(name, tplStr)
      fnameInput.value = ''
      ftemplateInput.value = ''
      renderFormatsList()
      try {
        buildFormatOptions(formatSel, getFormat())
      } catch {}
    })
    // Wrap inputs with the same column containers as list rows for alignment
    const addNameCol = createEl('span', { class: 'uiu-fmt-name' })
    addNameCol.appendChild(fnameInput)
    const addTplCol = createEl('span', { class: 'uiu-fmt-template' })
    addTplCol.appendChild(ftemplateInput)
    formatsForm.appendChild(addNameCol)
    formatsForm.appendChild(addTplCol)
    formatsForm.appendChild(addFmtBtn)
    const formatsList = createEl('div', { class: 'uiu-formats-list' })
    formats.appendChild(formatsList)
    formats.appendChild(formatsForm)
    // Example row: align under Format column using same grid
    const formatsExampleRow = createEl('div', {
      class: 'uiu-formats-row uiu-format-example-row',
    })
    formatsExampleRow.appendChild(
      createEl('span', { class: 'uiu-fmt-name', text: '' })
    )
    formatsExampleRow.appendChild(
      createEl('span', {
        class: 'uiu-fmt-template',
        text: t('example_format_template'),
      })
    )
    formatsExampleRow.appendChild(
      createEl('span', { class: 'uiu-ops', text: '' })
    )
    formats.appendChild(formatsExampleRow)
    settingsContainer.appendChild(formats)

    function renderFormatsList() {
      formatsList.textContent = ''
      const list = getCustomFormats()
      list.forEach((cf) => {
        const row = createEl('div', { class: 'uiu-formats-row' })
        const nameEl = createEl('span', {
          class: 'uiu-fmt-name',
          text: cf.name,
        })
        const tplEl = createEl('span', {
          class: 'uiu-fmt-template',
          text: cf.template,
        })
        const editBtn = createEl('button', { text: t('btn_edit') })
        editBtn.addEventListener('click', () => {
          row.textContent = ''
          row.classList.add('uiu-editing')
          const colName = createEl('span', {
            class: 'uiu-settings-item uiu-fmt-name',
          })
          const eName = createEl('input', { type: 'text' })
          eName.value = cf.name
          const colTpl = createEl('span', {
            class: 'uiu-settings-item uiu-fmt-template',
          })
          const eTpl = createEl('input', { type: 'text' })
          eTpl.value = cf.template
          colName.appendChild(eName)
          colTpl.appendChild(eTpl)
          const ops = createEl('span', { class: 'uiu-ops' })
          const updateBtn = createEl('button', { text: t('btn_update') })
          updateBtn.addEventListener('click', () => {
            const newName = (eName.value || '').trim()
            const newTpl = String(eTpl.value || '')
            if (!newName || !newTpl) return
            if (newName !== cf.name) removeCustomFormat(cf.name)
            upsertCustomFormat(newName, newTpl)
            // Update current format selection if renamed
            try {
              if (getFormat() === cf.name) setFormat(newName)
            } catch {}
            renderFormatsList()
            try {
              buildFormatOptions(formatSel, getFormat())
            } catch {}
          })
          const cancelBtn = createEl('button', { text: t('btn_cancel') })
          cancelBtn.addEventListener('click', () => {
            renderFormatsList()
          })
          ops.appendChild(updateBtn)
          ops.appendChild(cancelBtn)
          row.appendChild(colName)
          row.appendChild(colTpl)
          row.appendChild(ops)
        })
        const delBtn = createEl('button', { text: t('btn_delete') })
        delBtn.addEventListener('click', () => {
          removeCustomFormat(cf.name)
          // Reset site format if current selection removed
          try {
            if (getFormat() === cf.name) setFormat(DEFAULT_FORMAT)
          } catch {}
          renderFormatsList()
          try {
            buildFormatOptions(formatSel, getFormat())
          } catch {}
        })
        const ops = createEl('span', { class: 'uiu-ops' })
        ops.appendChild(editBtn)
        ops.appendChild(delBtn)
        row.appendChild(nameEl)
        row.appendChild(tplEl)
        row.appendChild(ops)
        formatsList.appendChild(row)
      })
    }

    function renderSettingsList() {
      // Avoid Trusted Types violation: clear without using innerHTML
      settingsList.textContent = ''
      const listData = getSiteBtnSettingsList()
      listData.forEach((cfg, idx) => {
        const row = createEl('div', { class: 'uiu-settings-row' })
        const info = createEl('span', {
          class: 'uiu-settings-item',
          text: `${cfg.selector} [${cfg.position || DEFAULT_BUTTON_POSITION}] - ${cfg.text || t('insert_image_button_default')}`,
        })
        const editBtn = createEl('button', { text: t('btn_edit') })
        editBtn.addEventListener('click', () => {
          // Avoid Trusted Types violation: clear without using innerHTML
          row.textContent = ''
          row.classList.add('uiu-editing')
          const fields = createEl('span', { class: 'uiu-settings-item' })
          const eSel = createEl('input', { type: 'text' })
          eSel.value = cfg.selector || ''
          const ePos = createEl('select')
          buildPositionOptions(ePos, cfg.position)
          const eText = createEl('input', { type: 'text' })
          eText.value = cfg.text || t('insert_image_button_default')
          fields.appendChild(eSel)
          fields.appendChild(ePos)
          fields.appendChild(eText)
          const ops = createEl('span', { class: 'uiu-ops' })
          const updateBtn = createEl('button', { text: t('btn_update') })
          updateBtn.addEventListener('click', () => {
            updateSiteBtnSetting(idx, {
              selector: eSel.value,
              position: ePos.value,
              text: eText.value,
            })
            renderSettingsList()
            document
              .querySelectorAll('.uiu-insert-btn')
              .forEach((el) => el.remove())
            applySiteButtons()
            try {
              restartSiteButtonObserver()
            } catch {}
          })
          const cancelBtn = createEl('button', { text: t('btn_cancel') })
          cancelBtn.addEventListener('click', () => {
            renderSettingsList()
          })
          ops.appendChild(updateBtn)
          ops.appendChild(cancelBtn)
          row.appendChild(fields)
          row.appendChild(ops)
        })
        const delBtn = createEl('button', { text: t('btn_delete') })
        delBtn.addEventListener('click', () => {
          removeSiteBtnSetting(idx)
          renderSettingsList()
          document
            .querySelectorAll('.uiu-insert-btn')
            .forEach((el) => el.remove())
          applySiteButtons()
          try {
            restartSiteButtonObserver()
          } catch {}
        })
        row.appendChild(info)
        const ops = createEl('span', { class: 'uiu-ops' })
        ops.appendChild(editBtn)
        ops.appendChild(delBtn)
        row.appendChild(ops)
        settingsList.appendChild(row)
      })
    }

    function refreshSettingsUI() {
      selInput.value = ''
      buildPositionOptions(posSel)
      textInput.value = t('insert_image_button_default')
      renderSettingsList()
      try {
        fnameInput.value = ''
        ftemplateInput.value = ''
        renderFormatsList()
      } catch {}
    }

    // Render into Shadow DOM root
    root.appendChild(header)
    root.appendChild(body)
    document.body.appendChild(panel)

    // initialize pressed state
    try {
      toggleHistoryBtn.setAttribute('aria-pressed', 'false')
      settingsBtn.setAttribute('aria-pressed', 'false')
    } catch {}

    panel.style.display = 'none'

    function applySingle(cfg) {
      if (!cfg?.selector) return
      let targets
      try {
        targets = document.querySelectorAll(cfg.selector)
      } catch (e) {
        return
      }
      if (!targets || !targets.length) return
      const posRaw = (cfg.position || '').trim()
      const pos =
        posRaw === 'before'
          ? 'before'
          : posRaw === 'inside'
            ? 'inside'
            : 'after'
      const content = (cfg.text || t('insert_image_button_default')).trim()
      Array.from(targets).forEach((target) => {
        const exists =
          pos === 'inside'
            ? !!target.querySelector('.uiu-insert-btn')
            : pos === 'before'
              ? !!(
                  target.previousElementSibling &&
                  target.previousElementSibling.classList?.contains(
                    'uiu-insert-btn'
                  )
                )
              : !!(
                  target.nextElementSibling &&
                  target.nextElementSibling.classList?.contains(
                    'uiu-insert-btn'
                  )
                )
        if (exists) return
        let btn
        try {
          // Parse HTML without using innerHTML to comply with Trusted Types
          const range = document.createRange()
          const ctx = document.createElement('div')
          range.selectNodeContents(ctx)
          const frag = range.createContextualFragment(content)
          if (frag && frag.childElementCount === 1) {
            btn = frag.firstElementChild
          }
        } catch {}
        if (!btn) {
          btn = createEl('button', {
            class: 'uiu-insert-btn uiu-default',
            text: content,
          })
        } else {
          btn.classList.add('uiu-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)
        }
      })
    }
    function applySiteButtons() {
      const list = getSiteBtnSettingsList()
      list.forEach((cfg) => {
        try {
          applySingle(cfg)
        } catch {}
      })
    }
    applySiteButtons()

    let siteBtnObserver
    function restartSiteButtonObserver() {
      try {
        if (siteBtnObserver) siteBtnObserver.disconnect()
      } catch {}
      const list = getSiteBtnSettingsList()
      if (!list.length) {
        siteBtnObserver = null
        return
      }
      const checkAndInsertAll = () => {
        list.forEach((cfg) => {
          try {
            applySingle(cfg)
          } catch {}
        })
      }
      checkAndInsertAll()
      siteBtnObserver = new MutationObserver(() => checkAndInsertAll())
      siteBtnObserver.observe(document.body || document.documentElement, {
        childList: true,
        subtree: true,
      })
    }
    restartSiteButtonObserver()

    const drop = createEl('div', { id: 'uiu-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 uploadImage(item.file)
          const fmt = getFormat()
          const out = formatText(
            applyProxy(link, getHost()),
            item.file.name,
            fmt
          )
          copyAndInsert(out)
          addToHistory({
            link,
            name: item.file.name,
            ts: Date.now(),
            pageUrl: location.href,
            provider: getHost(),
          })
          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) => {
      const dt = e.dataTransfer
      const types = dt?.types ? Array.from(dt.types) : []
      const hasFileType =
        types.includes('Files') || dt?.types?.contains?.('Files')
      const hasFileItem = dt?.items
        ? Array.from(dt.items).some((it) => it.kind === 'file')
        : false
      if (hasFileType || hasFileItem) {
        drop.classList.add('show')
        e.preventDefault()
      } else {
        drop.classList.remove('show')
      }
    })
    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() {
      // Avoid Trusted Types violation: clear without using innerHTML
      history.textContent = ''
      const header = createEl('div', { class: 'uiu-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: 'uiu-list' })
      const items = loadHistory()
      items.forEach((it) => {
        const row = createEl('div', { class: 'uiu-row' })

        const preview = createEl('img', {
          src: applyProxy(
            it.link,
            it.provider || (isImgurUrl(it.link) ? 'imgur' : 'other')
          ),
          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: 'uiu-name',
            text: it.name || it.link,
            title: it.name || it.link,
          })
        )
        try {
          const providerKey = it.provider || 'imgur'
          const providerText = t('host_' + providerKey)
          info.appendChild(
            createEl('span', {
              text: providerText,
              style:
                'font-size:11px;color:#cbd5e1;border:1px solid #334155;border-radius:4px;padding:1px 6px;width:fit-content;',
            })
          )
        } catch {}
        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: 'uiu-ops' })
        const copyBtn = createEl('button', { text: t('btn_copy') })
        copyBtn.addEventListener('click', () => {
          const fmt = getFormat()
          const proxied = applyProxy(
            it.link,
            it.provider || (isImgurUrl(it.link) ? 'imgur' : 'other')
          )
          const out = formatText(
            proxied,
            it.name || t('default_image_name'),
            fmt
          )
          copyAndInsert(out)
        })
        const openBtn = createEl('button', { text: t('btn_open') })
        openBtn.addEventListener('click', () => {
          const url = applyProxy(
            it.link,
            it.provider || (isImgurUrl(it.link) ? 'imgur' : 'other')
          )
          window.open(url, '_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'
      try {
        toggleHistoryBtn.setAttribute(
          'aria-pressed',
          header.classList.contains('uiu-show-history') ? 'true' : 'false'
        )
        settingsBtn.setAttribute(
          'aria-pressed',
          header.classList.contains('uiu-show-settings') ? 'true' : 'false'
        )
      } catch {}
    })
    GM_registerMenuCommand(t('menu_select_images'), () => {
      panel.style.display = 'block'
      openFilePicker()
    })
    GM_registerMenuCommand(t('menu_settings'), () => {
      panel.style.display = 'block'
      header.classList.add('uiu-show-settings')
      try {
        refreshSettingsUI()
      } catch {}
      try {
        settingsBtn.setAttribute('aria-pressed', 'true')
        toggleHistoryBtn.setAttribute(
          'aria-pressed',
          header.classList.contains('uiu-show-history') ? 'true' : 'false'
        )
      } catch {}
    })

    return { handleFiles }
  }

  if (!document.getElementById('uiu-panel')) {
    const { handleFiles } = createPanel()

    window.addEventListener('iu:uploadFiles', (e) => {
      const files = e.detail?.files
      if (files?.length) handleFiles(files)
    })
  }
})()