Image Uploader to Markdown to CloudFlare-ImgBed

适配于 CloudFlare-ImgBed 的粘贴上传并生成markdown的脚本, CloudFlare-ImgBed : https://github.com/MarSeventh/CloudFlare-ImgBed

  1. // ==UserScript==
  2. // @name Image Uploader to Markdown to CloudFlare-ImgBed
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.4.1-beta
  5. // @description 适配于 CloudFlare-ImgBed 的粘贴上传并生成markdown的脚本, CloudFlare-ImgBed : https://github.com/MarSeventh/CloudFlare-ImgBed
  6. // @author calg
  7. // @match *://*/*
  8. // @exclude *://*.jpg
  9. // @exclude *://*.jpeg
  10. // @exclude *://*.png
  11. // @exclude *://*.gif
  12. // @exclude *://*.webp
  13. // @exclude *://*.pdf
  14. // @grant GM_xmlhttpRequest
  15. // @grant GM_addStyle
  16. // @grant GM_getValue
  17. // @grant GM_setValue
  18. // @grant GM_registerMenuCommand
  19. // @grant GM_info
  20. // @grant GM_unregisterMenuCommand
  21. // @grant GM_log
  22. // @license MIT
  23. // @icon https://raw.githubusercontent.com/MarSeventh/CloudFlare-ImgBed/refs/heads/main/logo.png
  24. // ==/UserScript==
  25.  
  26. (function() {
  27. 'use strict';
  28.  
  29. // 防止重复注入
  30. if (window.imageUploaderInitialized) {
  31. return;
  32. }
  33. window.imageUploaderInitialized = true;
  34.  
  35. // 创建一个唯一的命名空间
  36. const SCRIPT_NAMESPACE = 'image_uploader_' + GM_info.script.version.replace(/\./g, '_');
  37.  
  38. // 存储菜单命令ID
  39. let menuCommandId = null;
  40.  
  41. // 检查是否已经存在事件监听器
  42. function hasEventListener(element, eventName) {
  43. const key = `${SCRIPT_NAMESPACE}_${eventName}`;
  44. return element[key] === true;
  45. }
  46.  
  47. // 标记事件监听器已添加
  48. function markEventListener(element, eventName) {
  49. const key = `${SCRIPT_NAMESPACE}_${eventName}`;
  50. element[key] = true;
  51. }
  52.  
  53. // 默认配置信息
  54. const DEFAULT_CONFIG = {
  55. AUTH_CODE: 'AUTH_CODE', // 替换为你的认证码
  56. SERVER_URL: 'https://SERVER_URL', // 替换为实际的服务器地址
  57. UPLOAD_PARAMS: {
  58. serverCompress: true,
  59. uploadChannel: 'telegram', // 可选 telegram 和 cfr2
  60. autoRetry: true,
  61. uploadNameType: 'index', // 可选值为[default, index, origin, short]
  62. returnFormat: 'full',
  63. uploadFolder: 'apiupload' // 指定上传目录,用相对路径表示,例如上传到img/test目录需填img/test
  64. },
  65. NOTIFICATION_DURATION: 3000, // 通知显示时间(毫秒)
  66. MARKDOWN_TEMPLATE: '![{filename}]({url})', // Markdown 模板
  67. AUTO_COPY_URL: false, // 是否自动复制URL到剪贴板
  68. ALLOWED_HOSTS: ['*'], // 允许在哪些网站上运行,* 表示所有网站
  69. MAX_FILE_SIZE: 5 * 1024 * 1024 // 最大文件大小(5MB)
  70. };
  71.  
  72. // 获取用户配置并确保所有必需的字段都存在
  73. const userConfig = GM_getValue('userConfig', {});
  74. let CONFIG = {};
  75.  
  76. // 深度合并配置
  77. function mergeConfig(target, source) {
  78. for (const key in source) {
  79. if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
  80. target[key] = target[key] || {};
  81. mergeConfig(target[key], source[key]);
  82. } else {
  83. target[key] = source[key];
  84. }
  85. }
  86. return target;
  87. }
  88.  
  89. // 确保所有默认配置项都存在
  90. CONFIG = mergeConfig({...DEFAULT_CONFIG}, userConfig);
  91.  
  92. // 验证配置的完整性
  93. function validateConfig() {
  94. if (!Array.isArray(CONFIG.ALLOWED_HOSTS)) {
  95. CONFIG.ALLOWED_HOSTS = DEFAULT_CONFIG.ALLOWED_HOSTS;
  96. }
  97. if (typeof CONFIG.NOTIFICATION_DURATION !== 'number') {
  98. CONFIG.NOTIFICATION_DURATION = DEFAULT_CONFIG.NOTIFICATION_DURATION;
  99. }
  100. if (typeof CONFIG.MAX_FILE_SIZE !== 'number') {
  101. CONFIG.MAX_FILE_SIZE = DEFAULT_CONFIG.MAX_FILE_SIZE;
  102. }
  103. if (typeof CONFIG.MARKDOWN_TEMPLATE !== 'string') {
  104. CONFIG.MARKDOWN_TEMPLATE = DEFAULT_CONFIG.MARKDOWN_TEMPLATE;
  105. }
  106. if (typeof CONFIG.AUTO_COPY_URL !== 'boolean') {
  107. CONFIG.AUTO_COPY_URL = DEFAULT_CONFIG.AUTO_COPY_URL;
  108. }
  109. }
  110.  
  111. validateConfig();
  112.  
  113. // 添加通知样式
  114. GM_addStyle(`
  115. .img-upload-notification {
  116. position: fixed;
  117. top: 20px;
  118. right: 20px;
  119. padding: 15px 20px;
  120. border-radius: 5px;
  121. z-index: 9999;
  122. max-width: 300px;
  123. font-size: 14px;
  124. box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  125. transition: all 0.3s ease;
  126. opacity: 0;
  127. transform: translateX(20px);
  128. }
  129. .img-upload-notification.show {
  130. opacity: 1;
  131. transform: translateX(0);
  132. }
  133. .img-upload-success {
  134. background-color: #4caf50;
  135. color: white;
  136. }
  137. .img-upload-error {
  138. background-color: #f44336;
  139. color: white;
  140. }
  141. .img-upload-info {
  142. background-color: #2196F3;
  143. color: white;
  144. }
  145. .img-upload-close {
  146. float: right;
  147. margin-left: 10px;
  148. cursor: pointer;
  149. opacity: 0.8;
  150. }
  151. .img-upload-close:hover {
  152. opacity: 1;
  153. }
  154.  
  155. .img-upload-modal {
  156. position: fixed;
  157. top: 50%;
  158. left: 50%;
  159. transform: translate(-50%, -50%);
  160. background: white;
  161. padding: 20px;
  162. border-radius: 8px;
  163. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  164. z-index: 10000;
  165. max-width: 600px;
  166. width: 90%;
  167. max-height: 80vh;
  168. overflow-y: auto;
  169. }
  170. .img-upload-modal-overlay {
  171. position: fixed;
  172. top: 0;
  173. left: 0;
  174. right: 0;
  175. bottom: 0;
  176. background: rgba(0, 0, 0, 0.5);
  177. z-index: 9999;
  178. }
  179. .img-upload-modal h2 {
  180. margin: 0 0 20px;
  181. color: #333;
  182. font-size: 18px;
  183. }
  184. .img-upload-form-group {
  185. margin-bottom: 20px;
  186. }
  187. .img-upload-form-group label {
  188. display: block;
  189. margin-bottom: 8px;
  190. color: #333;
  191. font-weight: 500;
  192. }
  193. .img-upload-help-text {
  194. margin-top: 4px;
  195. color: #666;
  196. font-size: 12px;
  197. }
  198. .img-upload-form-group input[type="text"],
  199. .img-upload-form-group input[type="number"],
  200. .img-upload-form-group textarea {
  201. width: 100%;
  202. padding: 8px;
  203. border: 1px solid #ddd;
  204. border-radius: 4px;
  205. font-size: 14px;
  206. box-sizing: border-box;
  207. }
  208. .img-upload-form-group textarea {
  209. min-height: 100px;
  210. font-family: monospace;
  211. }
  212. .img-upload-form-group input[type="checkbox"] {
  213. margin-right: 8px;
  214. }
  215. .img-upload-buttons {
  216. display: flex;
  217. justify-content: flex-end;
  218. gap: 10px;
  219. margin-top: 20px;
  220. }
  221. .img-upload-button {
  222. padding: 8px 16px;
  223. border: none;
  224. border-radius: 4px;
  225. cursor: pointer;
  226. font-size: 14px;
  227. transition: background-color 0.2s;
  228. }
  229. .img-upload-button-primary {
  230. background: #2196F3;
  231. color: white;
  232. }
  233. .img-upload-button-secondary {
  234. background: #e0e0e0;
  235. color: #333;
  236. }
  237. .img-upload-button:hover {
  238. opacity: 0.9;
  239. }
  240. .img-upload-error {
  241. color: #ffffff;
  242. font-size: 12px;
  243. margin-top: 4px;
  244. }
  245. .img-upload-info-icon {
  246. display: inline-block;
  247. width: 16px;
  248. height: 16px;
  249. background: #2196F3;
  250. color: white;
  251. border-radius: 50%;
  252. text-align: center;
  253. line-height: 16px;
  254. font-size: 12px;
  255. margin-left: 4px;
  256. cursor: help;
  257. }
  258. .img-upload-form-group select {
  259. width: 100%;
  260. padding: 8px;
  261. border: 1px solid #ddd;
  262. border-radius: 4px;
  263. font-size: 14px;
  264. background-color: white;
  265. }
  266. .img-upload-input-group {
  267. display: flex;
  268. align-items: center;
  269. }
  270. .img-upload-input-group input {
  271. flex: 1;
  272. border-top-right-radius: 0;
  273. border-bottom-right-radius: 0;
  274. }
  275. .img-upload-input-group-text {
  276. padding: 8px 12px;
  277. background: #f5f5f5;
  278. border: 1px solid #ddd;
  279. border-left: none;
  280. border-radius: 0 4px 4px 0;
  281. color: #666;
  282. }
  283. .img-upload-checkbox-label {
  284. display: flex !important;
  285. align-items: center;
  286. font-weight: normal !important;
  287. }
  288. .img-upload-checkbox-label input {
  289. margin-right: 8px;
  290. }
  291.  
  292. .img-upload-dropzone {
  293. display: none;
  294. position: fixed;
  295. top: 0;
  296. left: 0;
  297. width: 100%;
  298. height: 100%;
  299. background: rgba(33, 150, 243, 0.2);
  300. border: 3px dashed #2196F3;
  301. z-index: 9998;
  302. box-sizing: border-box;
  303. }
  304. .img-upload-dropzone.active {
  305. display: block;
  306. }
  307. .img-upload-dropzone-message {
  308. position: absolute;
  309. top: 50%;
  310. left: 50%;
  311. transform: translate(-50%, -50%);
  312. background: white;
  313. padding: 20px 40px;
  314. border-radius: 8px;
  315. font-size: 18px;
  316. color: #2196F3;
  317. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  318. }
  319. `);
  320.  
  321. // 添加日志函数
  322. function log(message, type = 'info') {
  323. const prefix = '[Image Uploader]';
  324. switch(type) {
  325. case 'error':
  326. console.error(`${prefix} ${message}`);
  327. break;
  328. case 'warn':
  329. console.warn(`${prefix} ⚠️ ${message}`);
  330. break;
  331. case 'success':
  332. console.log(`${prefix} ${message}`);
  333. break;
  334. default:
  335. console.log(`${prefix} ℹ️ ${message}`);
  336. }
  337. }
  338.  
  339. // 显示通知的函数
  340. function showNotification(message, type = 'info') {
  341. const notification = document.createElement('div');
  342. notification.className = `img-upload-notification img-upload-${type}`;
  343.  
  344. const closeBtn = document.createElement('span');
  345. closeBtn.className = 'img-upload-close';
  346. closeBtn.textContent = '✕';
  347. closeBtn.onclick = () => removeNotification(notification);
  348.  
  349. const messageSpan = document.createElement('span');
  350. messageSpan.textContent = message;
  351.  
  352. notification.appendChild(closeBtn);
  353. notification.appendChild(messageSpan);
  354. document.body.appendChild(notification);
  355.  
  356. // 添加显示动画
  357. setTimeout(() => notification.classList.add('show'), 10);
  358.  
  359. // 自动消失
  360. const timeout = setTimeout(() => removeNotification(notification), CONFIG.NOTIFICATION_DURATION);
  361.  
  362. // 鼠标悬停时暂停消失
  363. notification.addEventListener('mouseenter', () => clearTimeout(timeout));
  364. notification.addEventListener('mouseleave', () => setTimeout(() => removeNotification(notification), 1000));
  365. }
  366.  
  367. // 移除通知
  368. function removeNotification(notification) {
  369. notification.classList.remove('show');
  370. setTimeout(() => {
  371. if (notification.parentNode) {
  372. notification.parentNode.removeChild(notification);
  373. }
  374. }, 300);
  375. }
  376.  
  377. // 复制文本到剪贴板
  378. function copyToClipboard(text) {
  379. const textarea = document.createElement('textarea');
  380. textarea.value = text;
  381. textarea.style.position = 'fixed';
  382. textarea.style.opacity = '0';
  383. document.body.appendChild(textarea);
  384. textarea.select();
  385. try {
  386. document.execCommand('copy');
  387. showNotification('链接已复制到剪贴板!', 'success');
  388. } catch (err) {
  389. showNotification('复制失败:' + err.message, 'error');
  390. }
  391. document.body.removeChild(textarea);
  392. }
  393.  
  394. // 检查文件大小
  395. function checkFileSize(file) {
  396. if (file.size > CONFIG.MAX_FILE_SIZE) {
  397. showNotification(`文件大小超过限制(${Math.round(CONFIG.MAX_FILE_SIZE/1024/1024)}MB)`, 'error');
  398. return false;
  399. }
  400. return true;
  401. }
  402.  
  403. // 检查当前网站是否允许上传
  404. function isAllowedHost() {
  405. const currentHost = window.location.hostname;
  406. log(`检查当前域名是否允许: ${currentHost}`);
  407.  
  408. // 如果允许所有网站
  409. if (CONFIG.ALLOWED_HOSTS.includes('*')) {
  410. log('允许所有网站');
  411. return true;
  412. }
  413.  
  414. // 清理和标准化域名列表
  415. const allowedHosts = CONFIG.ALLOWED_HOSTS.map(host => {
  416. // 移除协议前缀
  417. host = host.replace(/^https?:\/\//, '');
  418. // 移除路径和查询参数
  419. host = host.split('/')[0];
  420. // 移除端口号
  421. host = host.split(':')[0];
  422. return host.toLowerCase().trim();
  423. });
  424.  
  425. log(`允许的域名列表: ${JSON.stringify(allowedHosts, null, 2)}`);
  426.  
  427. // 检查当前域名是否在允许列表中
  428. const isAllowed = allowedHosts.some(host => {
  429. // 完全匹配
  430. if (host === currentHost) {
  431. log(`域名完全匹配: ${host}`);
  432. return true;
  433. }
  434. // 通配符匹配(例如 *.example.com)
  435. if (host.startsWith('*.') && currentHost.endsWith(host.slice(1))) {
  436. log(`域名通配符匹配: ${host}`);
  437. return true;
  438. }
  439. return false;
  440. });
  441.  
  442. if (!isAllowed) {
  443. log(`当前域名 ${currentHost} 不在允许列表中`, 'warn');
  444. }
  445.  
  446. return isAllowed;
  447. }
  448.  
  449. // 修改事件监听器添加方式
  450. function addPasteListener() {
  451. if (hasEventListener(document, 'paste')) {
  452. return;
  453. }
  454.  
  455. document.addEventListener('paste', async function(event) {
  456. if (!isAllowedHost()) return;
  457.  
  458. const activeElement = document.activeElement;
  459. if (!activeElement || !['INPUT', 'TEXTAREA'].includes(activeElement.tagName)) {
  460. return;
  461. }
  462.  
  463. const items = event.clipboardData.items;
  464. let hasImage = false;
  465.  
  466. for (let item of items) {
  467. if (item.type.startsWith('image/')) {
  468. hasImage = true;
  469. event.preventDefault();
  470. const blob = item.getAsFile();
  471.  
  472. if (!checkFileSize(blob)) {
  473. return;
  474. }
  475.  
  476. showNotification('正在上传图片,请稍候...', 'info');
  477. await uploadImage(blob, activeElement);
  478. break;
  479. }
  480. }
  481.  
  482. if (!hasImage) {
  483. return;
  484. }
  485. });
  486.  
  487. markEventListener(document, 'paste');
  488. }
  489.  
  490. // 上传图片
  491. async function uploadImage(blob, targetElement) {
  492. const formData = new FormData();
  493. const filename = `pasted-image-${Date.now()}.png`;
  494. formData.append('file', blob, filename);
  495.  
  496. log(`开始上传图片: ${filename} (${(blob.size / 1024).toFixed(2)}KB)`);
  497. log(`上传参数: ${JSON.stringify(CONFIG.UPLOAD_PARAMS, null, 2)}`);
  498.  
  499. const queryParams = new URLSearchParams({
  500. authCode: CONFIG.AUTH_CODE,
  501. ...CONFIG.UPLOAD_PARAMS
  502. }).toString();
  503.  
  504. try {
  505. log(`正在发送请求到: ${CONFIG.SERVER_URL}/upload`);
  506. GM_xmlhttpRequest({
  507. method: 'POST',
  508. url: `${CONFIG.SERVER_URL}/upload?${queryParams}`,
  509. data: formData,
  510. onload: function(response) {
  511. if (response.status === 200) {
  512. try {
  513. const result = JSON.parse(response.responseText);
  514. log(`服务器响应: ${JSON.stringify(result, null, 2)}`);
  515.  
  516. if (result && result.length > 0) {
  517. const imageUrl = result[0].src;
  518. log(`上传成功,图片URL: ${imageUrl}`, 'success');
  519. insertMarkdownImage(imageUrl, targetElement, filename);
  520. showNotification('图片上传成功!', 'success');
  521.  
  522. if (CONFIG.AUTO_COPY_URL) {
  523. log('自动复制URL到剪贴板');
  524. copyToClipboard(imageUrl);
  525. }
  526. } else {
  527. const errorMsg = '上传成功但未获取到图片链接,请检查服务器响应';
  528. log(errorMsg, 'error');
  529. showNotification(errorMsg, 'error');
  530. }
  531. } catch (e) {
  532. const errorMsg = `解析服务器响应失败:${e.message}`;
  533. log(errorMsg, 'error');
  534. log(`原始响应: ${response.responseText}`, 'error');
  535. showNotification(errorMsg, 'error');
  536. }
  537. } else {
  538. let errorMsg = '上传失败';
  539. try {
  540. const errorResponse = JSON.parse(response.responseText);
  541. errorMsg += ':' + (errorResponse.message || response.statusText);
  542. log(`上传失败: ${JSON.stringify(errorResponse, null, 2)}`, 'error');
  543. } catch (e) {
  544. errorMsg += `(状态码:${response.status})`;
  545. log(`上传失败: 状态码 ${response.status}`, 'error');
  546. log(`原始响应: ${response.responseText}`, 'error');
  547. }
  548. showNotification(errorMsg, 'error');
  549. }
  550. },
  551. onerror: function(error) {
  552. const errorMsg = '网络错误:无法连接到图床服务器';
  553. log(`${errorMsg}: ${error}`, 'error');
  554. showNotification(errorMsg, 'error');
  555. }
  556. });
  557. } catch (error) {
  558. const errorMsg = `上传过程发生错误:${error.message}`;
  559. log(errorMsg, 'error');
  560. showNotification(errorMsg, 'error');
  561. }
  562. }
  563.  
  564. // 在输入框中插入 Markdown 格式的图片链接
  565. function insertMarkdownImage(imageUrl, element, filename) {
  566. const markdownImage = CONFIG.MARKDOWN_TEMPLATE
  567. .replace('{url}', imageUrl)
  568. .replace('{filename}', filename.replace(/\.[^/.]+$/, '')); // 移除文件扩展名
  569.  
  570. const start = element.selectionStart;
  571. const end = element.selectionEnd;
  572. const text = element.value;
  573.  
  574. element.value = text.substring(0, start) + markdownImage + text.substring(end);
  575. element.selectionStart = element.selectionEnd = start + markdownImage.length;
  576. element.focus();
  577. }
  578.  
  579. // 创建配置界面
  580. function createConfigModal() {
  581. const overlay = document.createElement('div');
  582. overlay.className = 'img-upload-modal-overlay';
  583.  
  584. const modal = document.createElement('div');
  585. modal.className = 'img-upload-modal';
  586.  
  587. const content = `
  588. <h2>图床上传配置</h2>
  589. <form id="img-upload-config-form">
  590. <div class="img-upload-form-group">
  591. <label>认证码</label>
  592. <input type="text" name="AUTH_CODE" value="${CONFIG.AUTH_CODE}" required>
  593. <div class="img-upload-help-text">用于验证上传请求的密钥</div>
  594. </div>
  595. <div class="img-upload-form-group">
  596. <label>服务器地址</label>
  597. <input type="text" name="SERVER_URL" value="${CONFIG.SERVER_URL}" required>
  598. <div class="img-upload-help-text">图床服务器的URL地址</div>
  599. </div>
  600. <div class="img-upload-form-group">
  601. <label>上传通道</label>
  602. <select name="uploadChannel">
  603. <option value="cfr2" ${CONFIG.UPLOAD_PARAMS.uploadChannel === 'cfr2' ? 'selected' : ''}>CloudFlare R2</option>
  604. <option value="telegram" ${CONFIG.UPLOAD_PARAMS.uploadChannel === 'telegram' ? 'selected' : ''}>Telegram</option>
  605. </select>
  606. <div class="img-upload-help-text">选择图片上传的存储通道</div>
  607. </div>
  608. <div class="img-upload-form-group">
  609. <label>文件命名方式</label>
  610. <select name="uploadNameType">
  611. <option value="default" ${CONFIG.UPLOAD_PARAMS.uploadNameType === 'default' ? 'selected' : ''}>默认(前缀_原名)</option>
  612. <option value="index" ${CONFIG.UPLOAD_PARAMS.uploadNameType === 'index' ? 'selected' : ''}>仅前缀</option>
  613. <option value="origin" ${CONFIG.UPLOAD_PARAMS.uploadNameType === 'origin' ? 'selected' : ''}>仅原名</option>
  614. <option value="short" ${CONFIG.UPLOAD_PARAMS.uploadNameType === 'short' ? 'selected' : ''}>短链接</option>
  615. </select>
  616. <div class="img-upload-help-text">选择上传后的文件命名方式</div>
  617. </div>
  618. <div class="img-upload-form-group">
  619. <label>上传目录</label>
  620. <input type="text" name="uploadFolder" value="${CONFIG.UPLOAD_PARAMS.uploadFolder}">
  621. <div class="img-upload-help-text">指定上传目录,使用相对路径,例如:img/test</div>
  622. </div>
  623. <div class="img-upload-form-group">
  624. <label>通知显示时间</label>
  625. <input type="number" name="NOTIFICATION_DURATION" value="${CONFIG.NOTIFICATION_DURATION}" min="1000" step="500">
  626. <div class="img-upload-help-text">通知消息显示的时间(毫秒)</div>
  627. </div>
  628. <div class="img-upload-form-group">
  629. <label>Markdown模板</label>
  630. <input type="text" name="MARKDOWN_TEMPLATE" value="${CONFIG.MARKDOWN_TEMPLATE}">
  631. <div class="img-upload-help-text">支持 {filename} {url} 两个变量</div>
  632. </div>
  633. <div class="img-upload-form-group">
  634. <label>允许的网站</label>
  635. <input type="text" name="ALLOWED_HOSTS" value="${CONFIG.ALLOWED_HOSTS.join(',')}">
  636. <div class="img-upload-help-text">输入域名,用逗号分隔。例如:nodeseek.com, *.example.com。使用 * 表示允许所有网站。无需输入 http:// 或 https://</div>
  637. </div>
  638. <div class="img-upload-form-group">
  639. <label>最大文件大小</label>
  640. <div class="img-upload-input-group">
  641. <input type="number" name="MAX_FILE_SIZE" value="${CONFIG.MAX_FILE_SIZE / 1024 / 1024}" min="1" step="1">
  642. <span class="img-upload-input-group-text">MB</span>
  643. </div>
  644. </div>
  645. <div class="img-upload-form-group">
  646. <label class="img-upload-checkbox-label">
  647. <input type="checkbox" name="AUTO_COPY_URL" ${CONFIG.AUTO_COPY_URL ? 'checked' : ''}>
  648. 自动复制URL到剪贴板
  649. </label>
  650. </div>
  651. <div class="img-upload-buttons">
  652. <button type="button" class="img-upload-button img-upload-button-secondary" id="img-upload-cancel">取消</button>
  653. <button type="button" class="img-upload-button img-upload-button-secondary" id="img-upload-reset">重置默认值</button>
  654. <button type="submit" class="img-upload-button img-upload-button-primary">保存</button>
  655. </div>
  656. </form>
  657. `;
  658.  
  659. modal.innerHTML = content;
  660. document.body.appendChild(overlay);
  661. document.body.appendChild(modal);
  662.  
  663. // 事件处理
  664. const form = modal.querySelector('#img-upload-config-form');
  665. const cancelBtn = modal.querySelector('#img-upload-cancel');
  666. const resetBtn = modal.querySelector('#img-upload-reset');
  667.  
  668. function closeModal() {
  669. document.body.removeChild(overlay);
  670. document.body.removeChild(modal);
  671. }
  672.  
  673. overlay.addEventListener('click', closeModal);
  674. cancelBtn.addEventListener('click', closeModal);
  675.  
  676. resetBtn.addEventListener('click', () => {
  677. if (confirm('确定要重置所有配置到默认值吗?')) {
  678. CONFIG = {...DEFAULT_CONFIG};
  679. GM_setValue('userConfig', {});
  680. showNotification('配置已重置为默认值!', 'success');
  681. closeModal();
  682. }
  683. });
  684.  
  685. form.addEventListener('submit', (e) => {
  686. e.preventDefault();
  687. try {
  688. const formData = new FormData(form);
  689. const newConfig = {
  690. AUTH_CODE: formData.get('AUTH_CODE'),
  691. SERVER_URL: formData.get('SERVER_URL'),
  692. UPLOAD_PARAMS: {
  693. ...DEFAULT_CONFIG.UPLOAD_PARAMS,
  694. uploadChannel: formData.get('uploadChannel'),
  695. uploadNameType: formData.get('uploadNameType'),
  696. uploadFolder: formData.get('uploadFolder')
  697. },
  698. NOTIFICATION_DURATION: parseInt(formData.get('NOTIFICATION_DURATION')),
  699. MARKDOWN_TEMPLATE: formData.get('MARKDOWN_TEMPLATE'),
  700. ALLOWED_HOSTS: formData.get('ALLOWED_HOSTS')
  701. .split(',')
  702. .map(h => {
  703. // 清理域名格式
  704. h = h.replace(/^https?:\/\//, '');
  705. h = h.split('/')[0];
  706. h = h.split(':')[0];
  707. return h.toLowerCase().trim();
  708. })
  709. .filter(h => h), // 移除空值
  710. MAX_FILE_SIZE: parseFloat(formData.get('MAX_FILE_SIZE')) * 1024 * 1024,
  711. AUTO_COPY_URL: formData.get('AUTO_COPY_URL') === 'on'
  712. };
  713.  
  714. CONFIG = mergeConfig({...DEFAULT_CONFIG}, newConfig);
  715. GM_setValue('userConfig', CONFIG);
  716. showNotification('配置已更新!', 'success');
  717. closeModal();
  718. } catch (error) {
  719. showNotification('配置格式错误:' + error.message, 'error');
  720. }
  721. });
  722.  
  723. // 防止点击模态框时关闭
  724. modal.addEventListener('click', (e) => e.stopPropagation());
  725. }
  726.  
  727. // 修改注册配置菜单函数
  728. function registerMenuCommands() {
  729. // 如果已经存在菜单,先注销
  730. if (menuCommandId) {
  731. try {
  732. GM_unregisterMenuCommand(menuCommandId);
  733. } catch (e) {
  734. console.log('Unregister menu failed:', e);
  735. }
  736. }
  737.  
  738. // 注册新菜单
  739. try {
  740. menuCommandId = GM_registerMenuCommand('配置图床参数', createConfigModal);
  741. } catch (e) {
  742. console.log('Register menu failed:', e);
  743. // 如果注册失败,尝试延迟重试
  744. setTimeout(registerMenuCommands, 1000);
  745. }
  746. }
  747.  
  748. // 创建拖拽区域
  749. function createDropZone() {
  750. const dropZone = document.createElement('div');
  751. dropZone.className = 'img-upload-dropzone';
  752.  
  753. const message = document.createElement('div');
  754. message.className = 'img-upload-dropzone-message';
  755. message.textContent = '释放鼠标上传图片';
  756.  
  757. dropZone.appendChild(message);
  758. document.body.appendChild(dropZone);
  759. return dropZone;
  760. }
  761.  
  762. // 修改拖拽事件监听器添加方式
  763. function handleDragAndDrop() {
  764. if (hasEventListener(document, 'drag')) {
  765. return;
  766. }
  767.  
  768. const dropZone = createDropZone();
  769. let activeElement = null;
  770.  
  771. // 处理拖拽文件
  772. async function handleFiles(files, targetElement) {
  773. for (const file of files) {
  774. if (file.type.startsWith('image/')) {
  775. if (!checkFileSize(file)) {
  776. continue;
  777. }
  778. showNotification('正在上传图片,请稍候...', 'info');
  779. await uploadImage(file, targetElement);
  780. } else {
  781. showNotification('只能上传图片文件', 'error');
  782. }
  783. }
  784. }
  785.  
  786. // 监听拖拽事件
  787. document.addEventListener('dragenter', (e) => {
  788. e.preventDefault();
  789. activeElement = document.activeElement;
  790. if (activeElement && ['INPUT', 'TEXTAREA'].includes(activeElement.tagName)) {
  791. dropZone.classList.add('active');
  792. }
  793. });
  794.  
  795. document.addEventListener('dragover', (e) => {
  796. e.preventDefault();
  797. });
  798.  
  799. document.addEventListener('dragleave', (e) => {
  800. e.preventDefault();
  801. const rect = document.documentElement.getBoundingClientRect();
  802. if (e.clientX <= rect.left || e.clientX >= rect.right ||
  803. e.clientY <= rect.top || e.clientY >= rect.bottom) {
  804. dropZone.classList.remove('active');
  805. }
  806. });
  807.  
  808. document.addEventListener('drop', async (e) => {
  809. e.preventDefault();
  810. dropZone.classList.remove('active');
  811.  
  812. if (!isAllowedHost()) return;
  813.  
  814. if (activeElement && ['INPUT', 'TEXTAREA'].includes(activeElement.tagName)) {
  815. const files = Array.from(e.dataTransfer.files);
  816. await handleFiles(files, activeElement);
  817. }
  818. });
  819.  
  820. markEventListener(document, 'drag');
  821. }
  822.  
  823. // 修改初始化函数
  824. function init() {
  825. // 检查是否已经初始化
  826. if (document[SCRIPT_NAMESPACE]) {
  827. log('脚本已经初始化,跳过');
  828. return;
  829. }
  830. document[SCRIPT_NAMESPACE] = true;
  831.  
  832. log(`初始化图片上传脚本 v${GM_info.script.version}`);
  833. log(`当前配置: ${JSON.stringify(CONFIG, null, 2)}`);
  834.  
  835. // 清理可能存在的旧实例
  836. cleanup();
  837.  
  838. // 初始化功能
  839. addPasteListener();
  840. handleDragAndDrop();
  841.  
  842. // 确保菜单注册成功
  843. const registerMenu = () => {
  844. if (!menuCommandId) {
  845. log('注册配置菜单');
  846. registerMenuCommands();
  847. }
  848. };
  849.  
  850. // 立即注册一次
  851. registerMenu();
  852.  
  853. // 在不同的时机尝试注册菜单
  854. window.addEventListener('load', registerMenu);
  855. document.addEventListener('readystatechange', registerMenu);
  856.  
  857. // 定期检查菜单是否存在
  858. setInterval(registerMenu, 5000);
  859.  
  860. log('初始化完成');
  861. }
  862.  
  863. // 添加清理函数
  864. function cleanup() {
  865. // 移除可能存在的旧的拖拽区域
  866. const oldDropZones = document.querySelectorAll('.img-upload-dropzone');
  867. oldDropZones.forEach(zone => zone.remove());
  868.  
  869. // 移除可能存在的旧的通知
  870. const oldNotifications = document.querySelectorAll('.img-upload-notification');
  871. oldNotifications.forEach(notification => notification.remove());
  872.  
  873. // 注销可能存在的旧菜单
  874. if (menuCommandId) {
  875. try {
  876. GM_unregisterMenuCommand(menuCommandId);
  877. menuCommandId = null;
  878. } catch (e) {
  879. console.log('Cleanup menu failed:', e);
  880. }
  881. }
  882. }
  883.  
  884. // 在页面 DOM 加载完成后初始化
  885. if (document.readyState === 'loading') {
  886. document.addEventListener('DOMContentLoaded', init);
  887. } else {
  888. init();
  889. }
  890. })();