V2ex Better Comment

在 V2ex 评论中支持--自定义表情/快速上传图片

  1. // ==UserScript==
  2. // @name V2ex Better Comment
  3. // @namespace http://tampermonkey.net/1436051
  4. // @version 1.0
  5. // @description 在 V2ex 评论中支持--自定义表情/快速上传图片
  6. // @author Dogxi <dogxi.me>
  7. // @match https://www.v2ex.com/t/*
  8. // @match https://v2ex.com/t/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=v2ex.com
  10. // @grant GM_xmlhttpRequest
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // @connect api.imgur.com
  14. // @license MIT
  15. // ==/UserScript==
  16.  
  17. (function () {
  18. "use strict";
  19.  
  20. const IMGUR_CLIENT_ID_KEY = "imgurClientId";
  21. const EMOJI_CONFIG_KEY = "emojiConfig";
  22. let CLIENT_ID = GM_getValue(IMGUR_CLIENT_ID_KEY, null);
  23.  
  24. // 默认表情配置
  25. const DEFAULT_EMOJI_CONFIG = {
  26. 颜文字: {
  27. type: "text",
  28. container: [
  29. { icon: "OωO", text: "呆" },
  30. { icon: "|´・ω・)ノ", text: "Hi" },
  31. { icon: "ヾ(≧∇≦*)ゝ", text: "开心" },
  32. { icon: "(☆ω☆)", text: "星星眼" },
  33. { icon: "(╯‵□′)╯︵┴─┴", text: "掀桌" },
  34. { icon: " ̄﹃ ̄", text: "流口水" },
  35. { icon: "(/ω\)", text: "捂脸" },
  36. { icon: "∠( ᐛ 」∠)_", text: "给跪" },
  37. { icon: "(๑•̀ㅁ•́ฅ)", text: "Hi" },
  38. { icon: "→_→", text: "斜眼" },
  39. { icon: "୧(๑•̀⌄•́๑)૭", text: "加油" },
  40. { icon: "٩(ˊᗜˋ*)و", text: "有木有WiFi" },
  41. { icon: "(ノ°ο°)ノ", text: "前方高能预警" },
  42. { icon: "(´இ皿இ`)", text: "我从未见过如此厚颜无耻之人" },
  43. { icon: "⌇●﹏●⌇", text: "吓死宝宝惹" },
  44. { icon: "(ฅ´ω`ฅ)", text: "已阅留爪" },
  45. { icon: "(╯°A°)╯︵○○○", text: "去吧大师球" },
  46. { icon: "φ( ̄∇ ̄o)", text: "太萌惹" },
  47. { icon: 'ヾ(´・ ・`。)ノ"', text: "咦咦咦" },
  48. { icon: "( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃", text: "气呼呼" },
  49. { icon: "(ó﹏ò。)", text: "我受到了惊吓" },
  50. { icon: "Σ(っ °Д °;)っ", text: "什么鬼" },
  51. { icon: '( ,,´・ω・)ノ"(´っω・`。)', text: "摸摸头" },
  52. { icon: "╮(╯▽╰)╭ ", text: "无奈" },
  53. { icon: "o(*////▽////*)q ", text: "脸红" },
  54. { icon: ">﹏<", text: "" },
  55. { icon: '( ๑´•ω•) "(ㆆᴗㆆ)', text: "" },
  56. ],
  57. },
  58. Emoji: {
  59. type: "emoji",
  60. container: [
  61. { icon: "😂", text: "" },
  62. { icon: "😀", text: "" },
  63. { icon: "😅", text: "" },
  64. { icon: "😊", text: "" },
  65. { icon: "🙂", text: "" },
  66. { icon: "🙃", text: "" },
  67. { icon: "😌", text: "" },
  68. { icon: "😍", text: "" },
  69. { icon: "😘 ", text: "" },
  70. { icon: "😜", text: "" },
  71. { icon: "😝", text: "" },
  72. { icon: "😏", text: "" },
  73. { icon: "😒", text: "" },
  74. { icon: "🙄", text: "" },
  75. { icon: "😳", text: "" },
  76. { icon: "😡", text: "" },
  77. { icon: "😔", text: "" },
  78. { icon: "😫", text: "" },
  79. { icon: "😱", text: "" },
  80. { icon: "😭", text: "" },
  81. { icon: "💩", text: "" },
  82. { icon: "👻", text: "" },
  83. { icon: "🙌", text: "" },
  84. { icon: "🖕", text: "" },
  85. { icon: "👍", text: "" },
  86. { icon: "👫", text: "" },
  87. { icon: "👬", text: "" },
  88. { icon: "👭", text: "" },
  89. { icon: "🌚", text: "" },
  90. { icon: "🌝", text: "" },
  91. { icon: "🙈", text: "" },
  92. { icon: "💊", text: "" },
  93. { icon: "😶", text: "" },
  94. { icon: "🙏", text: "" },
  95. { icon: "🍦", text: "" },
  96. { icon: "🍉", text: "" },
  97. { icon: "😣", text: "" },
  98. ],
  99. },
  100. 收藏表情: {
  101. type: "sticker",
  102. container: [
  103. { icon: "https://i.imgur.com/2by85Ui.jpeg", text: "小冒蜜" },
  104. { icon: "https://i.imgur.com/HCEidtT.jpeg", text: "老鼠玩手机" },
  105. { icon: "https://i.imgur.com/6W0VDcT.gif", text: "猫脸蹭墙" },
  106. ],
  107. },
  108. };
  109.  
  110. const STYLE = `
  111. .imgur-upload-btn, .emoji-btn {
  112. background: none;
  113. border: none;
  114. color: #778087;
  115. cursor: pointer;
  116. font-size: 13px;
  117. padding: 0;
  118. margin-left: 15px;
  119. text-decoration: none;
  120. transition: color 0.2s ease;
  121. }
  122. .imgur-upload-btn:hover, .emoji-btn:hover {
  123. color: #4d5256;
  124. text-decoration: underline;
  125. }
  126. .hidden {
  127. display: none !important;
  128. }
  129. .imgur-upload-modal, .emoji-modal {
  130. position: fixed;
  131. top: 0;
  132. left: 0;
  133. width: 100%;
  134. height: 100%;
  135. background-color: rgba(0, 0, 0, 0.5);
  136. display: flex;
  137. justify-content: center;
  138. align-items: center;
  139. z-index: 9999;
  140. }
  141. .imgur-upload-modal-content {
  142. background-color: #fff;
  143. padding: 20px;
  144. border-radius: 3px;
  145. box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
  146. max-width: 450px;
  147. width: 90%;
  148. position: relative;
  149. font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  150. }
  151. .emoji-modal-content {
  152. background-color: #fff;
  153. border-radius: 8px;
  154. box-shadow: 0 2px 20px rgba(0, 0, 0, 0.15);
  155. width: 90%;
  156. max-width: 500px;
  157. max-height: 70vh;
  158. position: relative;
  159. font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  160. display: flex;
  161. flex-direction: column;
  162. }
  163. .imgur-upload-modal-header, .emoji-modal-header {
  164. display: flex;
  165. justify-content: space-between;
  166. align-items: center;
  167. margin-bottom: 15px;
  168. padding-bottom: 10px;
  169. border-bottom: 1px solid #e2e2e2;
  170. }
  171. .emoji-modal-header {
  172. padding: 15px 20px 10px;
  173. margin-bottom: 0;
  174. }
  175. .imgur-upload-modal-title, .emoji-modal-title {
  176. font-size: 15px;
  177. font-weight: normal;
  178. color: #000;
  179. }
  180. .imgur-upload-modal-close, .emoji-modal-close {
  181. cursor: pointer;
  182. font-size: 18px;
  183. color: #ccc;
  184. width: 20px;
  185. height: 20px;
  186. display: flex;
  187. align-items: center;
  188. justify-content: center;
  189. transition: color 0.2s ease;
  190. }
  191. .imgur-upload-modal-close:hover, .emoji-modal-close:hover {
  192. color: #999;
  193. }
  194. .emoji-content {
  195. padding: 15px 20px;
  196. flex: 1;
  197. overflow-y: auto;
  198. min-height: 200px;
  199. }
  200. .emoji-grid {
  201. display: flex;
  202. flex-wrap: wrap;
  203. gap: 6px;
  204. margin-bottom: 15px;
  205. }
  206. .emoji-item {
  207. display: flex;
  208. align-items: center;
  209. justify-content: center;
  210. min-width: 35px;
  211. height: 35px;
  212. padding: 4px 6px;
  213. cursor: pointer;
  214. border-radius: 4px;
  215. transition: background-color 0.2s ease;
  216. font-size: 16px;
  217. text-align: center;
  218. border: 1px solid transparent;
  219. background: none;
  220. position: relative;
  221. word-break: keep-all;
  222. white-space: nowrap;
  223. }
  224. .emoji-item:hover {
  225. background-color: #f0f0f0;
  226. border-color: #ddd;
  227. }
  228. .emoji-item.text {
  229. border: 1px solid #d0d0d0;
  230. background-color: #f8f8f8;
  231. font-family: "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Helvetica Neue", Helvetica, Arial, sans-serif;
  232. box-shadow: 0 1px 2px rgba(0,0,0,0.05);
  233. }
  234. .emoji-item.text:hover {
  235. background-color: #eeeeee;
  236. border-color: #bbb;
  237. box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  238. }
  239. .emoji-item.image {
  240. background-size: contain;
  241. background-repeat: no-repeat;
  242. background-position: center;
  243. width: 35px;
  244. min-width: 35px;
  245. }
  246. .emoji-item.image.large {
  247. width: 60px;
  248. min-width: 60px;
  249. height: 60px;
  250. }
  251. .emoji-item .delete-btn {
  252. position: absolute;
  253. top: -4px;
  254. right: -4px;
  255. width: 14px;
  256. height: 14px;
  257. background: #ff4444;
  258. color: white;
  259. border: none;
  260. border-radius: 50%;
  261. font-size: 9px;
  262. cursor: pointer;
  263. display: none;
  264. align-items: center;
  265. justify-content: center;
  266. line-height: 1;
  267. }
  268. .emoji-item:hover .delete-btn {
  269. display: flex;
  270. }
  271. .emoji-tabs {
  272. display: flex;
  273. border-top: 1px solid #e2e2e2;
  274. background-color: #f9f9f9;
  275. overflow-x: auto;
  276. }
  277. .emoji-tab {
  278. flex: 1;
  279. padding: 12px 16px;
  280. text-align: center;
  281. cursor: pointer;
  282. font-size: 12px;
  283. color: #666;
  284. border: none;
  285. background: none;
  286. transition: all 0.2s ease;
  287. border-right: 1px solid #e2e2e2;
  288. white-space: nowrap;
  289. }
  290. .emoji-tab:last-child {
  291. border-right: none;
  292. }
  293. .emoji-tab.active {
  294. background-color: #fff;
  295. color: #333;
  296. border-top: 2px solid #778087;
  297. }
  298. .emoji-tab:hover:not(.active) {
  299. background-color: #f0f0f0;
  300. }
  301. .emoji-config-panel {
  302. padding: 20px;
  303. height: calc(100% - 0px);
  304. box-sizing: border-box;
  305. display: flex;
  306. flex-direction: column;
  307. width: 100%;
  308. }
  309. .emoji-config-title {
  310. font-size: 14px;
  311. font-weight: bold;
  312. margin-bottom: 10px;
  313. color: #333;
  314. }
  315. .emoji-config-textarea {
  316. width: 100%;
  317. flex: 1;
  318. min-height: 280px;
  319. padding: 10px;
  320. border: 1px solid #ccc;
  321. border-radius: 4px;
  322. font-family: "Consolas", "Monaco", "Courier New", monospace;
  323. font-size: 12px;
  324. resize: vertical;
  325. margin-bottom: 15px;
  326. box-sizing: border-box;
  327. line-height: 1.4;
  328. }
  329. .emoji-config-actions {
  330. display: flex;
  331. justify-content: space-between;
  332. align-items: center;
  333. padding-top: 15px;
  334. border-top: 1px solid #e2e2e2;
  335. margin-top: auto;
  336. }
  337. .emoji-config-btn {
  338. background: none;
  339. border: none;
  340. color: #778087;
  341. cursor: pointer;
  342. font-size: 12px;
  343. padding: 0;
  344. transition: color 0.2s ease;
  345. text-decoration: none;
  346. }
  347. .emoji-config-btn:hover {
  348. color: #4d5256;
  349. text-decoration: underline;
  350. }
  351. .imgur-upload-dropzone {
  352. border: 1px dashed #ccc;
  353. padding: 25px;
  354. text-align: center;
  355. margin-bottom: 15px;
  356. cursor: pointer;
  357. border-radius: 3px;
  358. transition: border-color 0.2s ease;
  359. font-size: 13px;
  360. color: #666;
  361. }
  362. .imgur-upload-dropzone:hover {
  363. border-color: #999;
  364. }
  365. .imgur-upload-dropzone.dragover {
  366. border-color: #778087;
  367. background-color: #f9f9f9;
  368. }
  369. .imgur-upload-preview {
  370. margin-top: 10px;
  371. max-width: 100%;
  372. max-height: 150px;
  373. border-radius: 2px;
  374. }
  375. .imgur-upload-actions {
  376. display: flex;
  377. justify-content: space-between;
  378. align-items: center;
  379. margin-top: 15px;
  380. padding-top: 10px;
  381. border-top: 1px solid #e2e2e2;
  382. }
  383. .imgur-upload-config-btn {
  384. background: none;
  385. border: none;
  386. color: #778087;
  387. cursor: pointer;
  388. font-size: 12px;
  389. padding: 0;
  390. }
  391. .imgur-upload-config-btn:hover {
  392. color: #4d5256;
  393. text-decoration: underline;
  394. }
  395. .imgur-upload-submit-btn {
  396. background-color: #f5f5f5;
  397. border: 1px solid #ccc;
  398. border-radius: 3px;
  399. color: #333;
  400. cursor: pointer;
  401. font-size: 12px;
  402. padding: 6px 12px;
  403. transition: all 0.2s ease;
  404. }
  405. .imgur-upload-submit-btn:hover {
  406. background-color: #e8e8e8;
  407. }
  408. .imgur-upload-submit-btn:disabled {
  409. background-color: #f9f9f9;
  410. color: #ccc;
  411. cursor: not-allowed;
  412. }
  413. .imgur-upload-config-panel {
  414. margin-top: 10px;
  415. padding: 10px;
  416. background-color: #f9f9f9;
  417. border-radius: 3px;
  418. border: 1px solid #e2e2e2;
  419. }
  420. .imgur-upload-config-row {
  421. display: flex;
  422. align-items: center;
  423. margin-bottom: 8px;
  424. }
  425. .imgur-upload-config-row:last-child {
  426. margin-bottom: 0;
  427. }
  428. .imgur-upload-config-label {
  429. font-size: 12px;
  430. color: #666;
  431. width: 70px;
  432. flex-shrink: 0;
  433. }
  434. .imgur-upload-config-input {
  435. flex: 1;
  436. padding: 3px 6px;
  437. border: 1px solid #ccc;
  438. border-radius: 2px;
  439. font-size: 12px;
  440. }
  441. .imgur-upload-config-save {
  442. background-color: #f5f5f5;
  443. border: 1px solid #ccc;
  444. border-radius: 2px;
  445. color: #333;
  446. cursor: pointer;
  447. font-size: 11px;
  448. margin-left: 6px;
  449. padding: 3px 8px;
  450. }
  451. .imgur-upload-config-save:hover {
  452. background-color: #e8e8e8;
  453. }
  454. .imgur-upload-modal-status {
  455. color: #666;
  456. font-size: 12px;
  457. text-align: center;
  458. }
  459. .imgur-upload-modal-status.success {
  460. color: #5cb85c;
  461. }
  462. .imgur-upload-modal-status.error {
  463. color: #d9534f;
  464. }
  465. .save-emoji-checkbox {
  466. display: flex;
  467. align-items: center;
  468. gap: 5px;
  469. font-size: 12px;
  470. color: #666;
  471. }
  472. .save-emoji-checkbox input[type="checkbox"] {
  473. margin: 0;
  474. }
  475. `;
  476.  
  477. // 获取表情配置
  478. function getEmojiConfig() {
  479. try {
  480. const savedConfig = GM_getValue(EMOJI_CONFIG_KEY, null);
  481. if (!savedConfig) {
  482. return DEFAULT_EMOJI_CONFIG;
  483. }
  484.  
  485. // 如果savedConfig已经是对象,直接返回
  486. if (typeof savedConfig === "object") {
  487. return savedConfig;
  488. }
  489.  
  490. // 如果是字符串,尝试解析
  491. if (typeof savedConfig === "string") {
  492. return JSON.parse(savedConfig);
  493. }
  494.  
  495. // 其他情况返回默认配置
  496. return DEFAULT_EMOJI_CONFIG;
  497. } catch (error) {
  498. console.error("解析表情配置失败,使用默认配置:", error);
  499. // 清除错误的配置并使用默认配置
  500. GM_setValue(EMOJI_CONFIG_KEY, JSON.stringify(DEFAULT_EMOJI_CONFIG));
  501. return DEFAULT_EMOJI_CONFIG;
  502. }
  503. }
  504.  
  505. // 保存表情配置
  506. function saveEmojiConfig(config) {
  507. try {
  508. // 确保保存的是JSON字符串
  509. const configString =
  510. typeof config === "string" ? config : JSON.stringify(config);
  511. GM_setValue(EMOJI_CONFIG_KEY, configString);
  512. } catch (error) {
  513. console.error("保存表情配置失败:", error);
  514. alert("保存配置失败,请检查配置格式");
  515. }
  516. }
  517.  
  518. // 添加样式到页面
  519. function addStyle() {
  520. const styleElement = document.createElement("style");
  521. styleElement.textContent = STYLE;
  522. document.head.appendChild(styleElement);
  523. }
  524.  
  525. // 创建上传弹窗
  526. function createUploadModal(textareaElement) {
  527. const modal = document.createElement("div");
  528. modal.className = "imgur-upload-modal";
  529.  
  530. const content = document.createElement("div");
  531. content.className = "imgur-upload-modal-content";
  532.  
  533. content.innerHTML = `
  534. <div class="imgur-upload-modal-header">
  535. <div class="imgur-upload-modal-title">上传图片</div>
  536. <div class="imgur-upload-modal-close">×</div>
  537. </div>
  538. <div class="imgur-upload-dropzone">
  539. <div>点击选择图片或拖拽图片到此处</div>
  540. <div style="font-size: 11px; color: #999; margin-top: 5px;">支持 JPG, PNG, GIF 格式</div>
  541. </div>
  542. <div class="imgur-upload-actions">
  543. <button class="imgur-upload-config-btn">⚙️ 配置</button>
  544. <button class="imgur-upload-submit-btn" disabled>确认上传</button>
  545. </div>
  546. <div class="imgur-upload-config-panel hidden">
  547. <div class="imgur-upload-config-row">
  548. <div class="imgur-upload-config-label">Imgur ID:</div>
  549. <input type="text" class="imgur-upload-config-input" placeholder="请输入 Imgur Client ID" value="${
  550. CLIENT_ID || ""
  551. }">
  552. <button class="imgur-upload-config-save">保存</button>
  553. </div>
  554. <div style="font-size: 11px; color: #666; margin-top: 8px;">
  555. <a href="https://api.imgur.com/oauth2/addclient" target="_blank">https://api.imgur.com/oauth2/addclient</a> 注册(不可用)获取(无回调)
  556. </div>
  557. </div>
  558. `;
  559.  
  560. modal.appendChild(content);
  561. document.body.appendChild(modal);
  562.  
  563. setupModalEvents(modal, textareaElement);
  564.  
  565. return modal;
  566. }
  567.  
  568. // 创建表情选择器弹窗
  569. function createEmojiModal(textareaElement) {
  570. const modal = document.createElement("div");
  571. modal.className = "emoji-modal";
  572.  
  573. const content = document.createElement("div");
  574. content.className = "emoji-modal-content";
  575.  
  576. const emojiConfig = getEmojiConfig();
  577. const categories = Object.keys(emojiConfig);
  578.  
  579. content.innerHTML = `
  580. <div class="emoji-modal-header">
  581. <div class="emoji-modal-title">选择表情</div>
  582. <div style="display: flex; align-items: center; gap: 15px;">
  583. <a class="emoji-config-btn" id="emoji-config-btn" href="javascript:void(0);">⚙️ 配置</a>
  584. <div class="emoji-modal-close">×</div>
  585. </div>
  586. </div>
  587. <div class="emoji-content">
  588. <div class="emoji-grid" id="emoji-grid"></div>
  589. </div>
  590. <div class="emoji-tabs" id="emoji-tabs"></div>
  591. `;
  592.  
  593. modal.appendChild(content);
  594. document.body.appendChild(modal);
  595.  
  596. // 预渲染所有分类内容
  597. preRenderCategories(emojiConfig);
  598.  
  599. // 创建分栏
  600. const tabsContainer = content.querySelector("#emoji-tabs");
  601. const fragment = document.createDocumentFragment();
  602. categories.forEach((category, index) => {
  603. const tab = document.createElement("button");
  604. tab.className = `emoji-tab ${index === 0 ? "active" : ""}`;
  605. tab.textContent = category;
  606. tab.dataset.category = category;
  607. fragment.appendChild(tab);
  608. });
  609. tabsContainer.appendChild(fragment);
  610.  
  611. // 使用事件委托处理分栏点击
  612. tabsContainer.addEventListener("click", (e) => {
  613. if (e.target.classList.contains("emoji-tab")) {
  614. const category = e.target.dataset.category;
  615. switchEmojiCategoryOptimized(category);
  616. }
  617. });
  618.  
  619. // 显示第一个分类的表情
  620. if (categories.length > 0) {
  621. switchEmojiCategoryOptimized(categories[0]);
  622. }
  623.  
  624. // 设置事件监听
  625. setupEmojiModalEvents(modal, textareaElement);
  626.  
  627. return modal;
  628. }
  629.  
  630. // 全局缓存已渲染的分类内容
  631. let renderedCategories = new Map();
  632. let currentActiveCategory = null;
  633.  
  634. // 预渲染所有分类内容
  635. function preRenderCategories(emojiConfig) {
  636. renderedCategories.clear();
  637.  
  638. Object.keys(emojiConfig).forEach((category) => {
  639. const categoryData = emojiConfig[category];
  640. if (categoryData && categoryData.container) {
  641. const container = document.createElement("div");
  642.  
  643. categoryData.container.forEach((item, index) => {
  644. const emojiEl = document.createElement("button");
  645. emojiEl.className = "emoji-item";
  646. emojiEl.dataset.emoji = item.icon;
  647. emojiEl.dataset.category = category;
  648. emojiEl.dataset.index = index;
  649.  
  650. if (
  651. categoryData.type === "image" ||
  652. categoryData.type === "sticker"
  653. ) {
  654. emojiEl.className += " image";
  655.  
  656. // 检查是否是imgur链接,如果是则使用大图标
  657. if (
  658. item.icon.includes("imgur.com") ||
  659. item.icon.includes("i.imgur.com")
  660. ) {
  661. emojiEl.className += " large";
  662. }
  663.  
  664. emojiEl.style.backgroundImage = `url(${item.icon})`;
  665. emojiEl.title = item.text || item.icon;
  666.  
  667. // 为自定义分组添加删除按钮
  668. if (categoryData.editable !== false) {
  669. const deleteBtn = document.createElement("button");
  670. deleteBtn.className = "delete-btn";
  671. deleteBtn.innerHTML = "×";
  672. deleteBtn.dataset.action = "delete";
  673. emojiEl.appendChild(deleteBtn);
  674. }
  675. } else {
  676. emojiEl.textContent = item.icon;
  677. emojiEl.title = item.text || item.icon;
  678.  
  679. // 为颜文字添加特殊样式
  680. if (
  681. categoryData.type === "text" ||
  682. categoryData.type === "emoticon"
  683. ) {
  684. emojiEl.className += " text";
  685. }
  686. }
  687.  
  688. container.appendChild(emojiEl);
  689. });
  690.  
  691. renderedCategories.set(category, container);
  692. }
  693. });
  694. }
  695.  
  696. // 优化的切换表情分类函数
  697. function switchEmojiCategoryOptimized(category) {
  698. if (currentActiveCategory === category) return;
  699.  
  700. const grid = document.getElementById("emoji-grid");
  701. const tabs = document.querySelectorAll(".emoji-tab");
  702.  
  703. if (!grid) {
  704. console.error("找不到表情网格元素");
  705. return;
  706. }
  707.  
  708. // 更新分栏状态
  709. tabs.forEach((tab) => {
  710. tab.classList.toggle("active", tab.dataset.category === category);
  711. });
  712.  
  713. // 清空网格并插入预渲染的内容
  714. grid.innerHTML = "";
  715. const categoryContainer = renderedCategories.get(category);
  716. if (categoryContainer) {
  717. // 克隆容器内容
  718. const clonedContainer = categoryContainer.cloneNode(true);
  719. // 将克隆的子元素添加到网格中
  720. while (clonedContainer.firstChild) {
  721. grid.appendChild(clonedContainer.firstChild);
  722. }
  723. }
  724.  
  725. currentActiveCategory = category;
  726. }
  727.  
  728. // 使用事件委托处理表情点击和删除
  729. function setupEmojiGridEvents() {
  730. const grid = document.getElementById("emoji-grid");
  731. if (!grid) return;
  732.  
  733. grid.addEventListener("click", (e) => {
  734. const target = e.target.closest(".emoji-item");
  735. if (!target) return;
  736.  
  737. const deleteBtn = e.target.closest(".delete-btn");
  738. if (deleteBtn) {
  739. e.stopPropagation();
  740. const category = target.dataset.category;
  741. const index = parseInt(target.dataset.index);
  742. deleteEmojiOptimized(category, index);
  743. } else {
  744. const emoji = target.dataset.emoji;
  745. insertEmoji(emoji);
  746. }
  747. });
  748. }
  749.  
  750. // 优化的删除表情函数
  751. function deleteEmojiOptimized(category, index) {
  752. if (confirm("确定要删除这个表情吗?")) {
  753. try {
  754. const config = getEmojiConfig();
  755. if (config[category] && config[category].container) {
  756. config[category].container.splice(index, 1);
  757. saveEmojiConfig(config);
  758.  
  759. // 重新预渲染并刷新当前分类
  760. preRenderCategories(config);
  761. switchEmojiCategoryOptimized(category);
  762. }
  763. } catch (error) {
  764. console.error("删除表情失败:", error);
  765. alert("删除表情失败: " + error.message);
  766. }
  767. }
  768. }
  769.  
  770. // 显示配置页面
  771. function showEmojiConfig() {
  772. const content = document.querySelector(".emoji-content");
  773. const tabs = document.querySelectorAll(".emoji-tab");
  774. const tabsContainer = document.querySelector("#emoji-tabs");
  775.  
  776. // 清除所有分栏的激活状态并隐藏分栏容器
  777. tabs.forEach((tab) => tab.classList.remove("active"));
  778. if (tabsContainer) {
  779. tabsContainer.style.display = "none";
  780. }
  781.  
  782. const currentConfig = getEmojiConfig();
  783.  
  784. content.innerHTML = `
  785. <div class="emoji-config-panel">
  786. <div class="emoji-config-title">表情配置</div>
  787. <textarea class="emoji-config-textarea" id="emoji-config-textarea"
  788. placeholder="请输入 JSON 格式的表情配置...">${JSON.stringify(
  789. currentConfig,
  790. null,
  791. 2
  792. )}</textarea>
  793. <div class="emoji-config-actions">
  794. <a href="https://owo.dogxi.me/" target="_blank" style="background-color: #f5f5f5; border: 1px solid #ccc; border-radius: 3px; color: #333; cursor: pointer; font-size: 12px; padding: 6px 12px; transition: all 0.2s ease; white-space: nowrap; text-decoration: none;">更多配置</a>
  795. <div>
  796. <button style="background-color: #f5f5f5; border: 1px solid #ccc; border-radius: 3px; color: #333; cursor: pointer; font-size: 12px; padding: 6px 12px; transition: all 0.2s ease; white-space: nowrap;" id="cancel-config-btn">取消</button>
  797. <button style="background-color: #778087; color: white; border: 1px solid #778087; border-radius: 3px; cursor: pointer; font-size: 12px; padding: 6px 12px; transition: all 0.2s ease; white-space: nowrap; margin-left: 10px;" id="save-config-btn">保存配置</button>
  798. </div>
  799. </div>
  800. </div>
  801. `;
  802.  
  803. // 添加事件监听
  804. setupEmojiConfigEvents();
  805. }
  806.  
  807. // 设置表情配置页面事件
  808. function setupEmojiConfigEvents() {
  809. const cancelBtn = document.getElementById("cancel-config-btn");
  810. const saveBtn = document.getElementById("save-config-btn");
  811.  
  812. if (cancelBtn) {
  813. cancelBtn.addEventListener("click", function () {
  814. const modal = document.querySelector(".emoji-modal");
  815. if (modal) {
  816. modal.remove();
  817. }
  818. });
  819. }
  820.  
  821. if (saveBtn) {
  822. saveBtn.addEventListener("click", function () {
  823. const textarea = document.getElementById("emoji-config-textarea");
  824. try {
  825. const config = JSON.parse(textarea.value);
  826. saveEmojiConfig(config);
  827. alert("配置保存成功!");
  828.  
  829. // 重新创建表情弹窗以应用新配置
  830. const modal = document.querySelector(".emoji-modal");
  831. if (modal) {
  832. const textareaElement = document.getElementById("reply_content");
  833. modal.remove();
  834. if (textareaElement) {
  835. createEmojiModal(textareaElement);
  836. }
  837. }
  838. } catch (e) {
  839. console.error("配置保存失败:", e);
  840. alert("JSON 格式错误,请检查配置:\n" + e.message);
  841. }
  842. });
  843. }
  844. }
  845.  
  846. // 设置表情弹窗事件监听
  847. function setupEmojiModalEvents(modal, textareaElement) {
  848. const closeBtn = modal.querySelector(".emoji-modal-close");
  849. const configBtn = modal.querySelector("#emoji-config-btn");
  850.  
  851. function closeModal() {
  852. if (document.body.contains(modal)) {
  853. // 清理缓存
  854. renderedCategories.clear();
  855. currentActiveCategory = null;
  856. document.body.removeChild(modal);
  857. }
  858. }
  859.  
  860. if (closeBtn) {
  861. closeBtn.addEventListener("click", closeModal);
  862. }
  863.  
  864. if (configBtn) {
  865. configBtn.addEventListener("click", showEmojiConfig);
  866. }
  867.  
  868. // 设置表情网格事件委托
  869. setupEmojiGridEvents();
  870.  
  871. modal.addEventListener("click", function (e) {
  872. if (e.target === modal) closeModal();
  873. });
  874.  
  875. // ESC键关闭
  876. const escHandler = function (e) {
  877. if (e.key === "Escape") {
  878. closeModal();
  879. document.removeEventListener("keydown", escHandler);
  880. }
  881. };
  882. document.addEventListener("keydown", escHandler);
  883. }
  884.  
  885. // 设置弹窗事件监听
  886. function setupModalEvents(modal, textareaElement) {
  887. const closeBtn = modal.querySelector(".imgur-upload-modal-close");
  888. const dropzone = modal.querySelector(".imgur-upload-dropzone");
  889. const configBtn = modal.querySelector(".imgur-upload-config-btn");
  890. const configPanel = modal.querySelector(".imgur-upload-config-panel");
  891. const configInput = modal.querySelector(".imgur-upload-config-input");
  892. const configSave = modal.querySelector(".imgur-upload-config-save");
  893. const submitBtn = modal.querySelector(".imgur-upload-submit-btn");
  894.  
  895. let selectedFile = null;
  896.  
  897. function closeModal() {
  898. if (document.body.contains(modal)) {
  899. document.body.removeChild(modal);
  900. }
  901. }
  902.  
  903. closeBtn.addEventListener("click", closeModal);
  904. modal.addEventListener("click", function (e) {
  905. if (e.target === modal) closeModal();
  906. });
  907.  
  908. configBtn.addEventListener("click", function () {
  909. configPanel.classList.toggle("hidden");
  910. });
  911.  
  912. configSave.addEventListener("click", function () {
  913. const newClientId = configInput.value.trim();
  914. if (newClientId) {
  915. GM_setValue(IMGUR_CLIENT_ID_KEY, newClientId);
  916. CLIENT_ID = newClientId;
  917. configPanel.classList.add("hidden");
  918. showStatusInModal(modal, "配置已保存", "success");
  919. }
  920. });
  921.  
  922. const fileInput = document.createElement("input");
  923. fileInput.type = "file";
  924. fileInput.accept = "image/*";
  925. fileInput.style.display = "none";
  926. modal.appendChild(fileInput);
  927.  
  928. dropzone.addEventListener("click", () => fileInput.click());
  929.  
  930. fileInput.addEventListener("change", function (e) {
  931. handleFileSelect(e.target.files[0]);
  932. });
  933.  
  934. dropzone.addEventListener("dragover", function (e) {
  935. e.preventDefault();
  936. dropzone.classList.add("dragover");
  937. });
  938.  
  939. dropzone.addEventListener("dragleave", function (e) {
  940. e.preventDefault();
  941. dropzone.classList.remove("dragover");
  942. });
  943.  
  944. dropzone.addEventListener("drop", function (e) {
  945. e.preventDefault();
  946. dropzone.classList.remove("dragover");
  947. const files = e.dataTransfer.files;
  948. if (files.length > 0) {
  949. handleFileSelect(files[0]);
  950. }
  951. });
  952.  
  953. // 处理文件选择
  954. function handleFileSelect(file) {
  955. if (!file || !file.type.match(/image\/.*/)) {
  956. showStatusInModal(modal, "请选择图片文件", "error");
  957. return;
  958. }
  959.  
  960. selectedFile = file;
  961.  
  962. const reader = new FileReader();
  963. reader.onload = function (e) {
  964. const preview = modal.querySelector(".imgur-upload-preview");
  965. if (preview) preview.remove();
  966.  
  967. const img = document.createElement("img");
  968. img.src = e.target.result;
  969. img.className = "imgur-upload-preview";
  970. dropzone.appendChild(img);
  971.  
  972. submitBtn.disabled = false;
  973. dropzone.querySelector("div").textContent = "已选择: " + file.name;
  974. };
  975. reader.readAsDataURL(file);
  976. }
  977.  
  978. submitBtn.addEventListener("click", function () {
  979. if (!selectedFile) return;
  980.  
  981. if (!CLIENT_ID) {
  982. showStatusInModal(modal, "请先配置 Imgur Client ID", "error");
  983. configPanel.classList.remove("hidden");
  984. return;
  985. }
  986.  
  987. submitBtn.disabled = true;
  988. submitBtn.textContent = "上传中...";
  989.  
  990. uploadToImgur(selectedFile, textareaElement, modal);
  991. });
  992. }
  993.  
  994. // 在弹窗中显示状态信息
  995. function showStatusInModal(modal, message, type) {
  996. let statusEl = modal.querySelector(".imgur-upload-modal-status");
  997. if (!statusEl) {
  998. statusEl = document.createElement("div");
  999. statusEl.className = "imgur-upload-modal-status";
  1000. statusEl.style.cssText =
  1001. "margin-top: 10px; font-size: 12px; text-align: center;";
  1002. modal.querySelector(".imgur-upload-modal-content").appendChild(statusEl);
  1003. }
  1004.  
  1005. statusEl.textContent = message;
  1006. statusEl.className = "imgur-upload-modal-status " + (type || "");
  1007.  
  1008. if (type === "success") {
  1009. setTimeout(() => (statusEl.textContent = ""), 3000);
  1010. }
  1011. }
  1012.  
  1013. // 修改上传成功处理逻辑
  1014. function handleUploadSuccess(imageUrl, fileName, modal) {
  1015. const textarea = document.getElementById("reply_content");
  1016.  
  1017. // 插入链接到文本框
  1018. insertLinkIntoTextarea(textarea, imageUrl, fileName);
  1019. showStatusInModal(modal, "上传成功!", "success");
  1020.  
  1021. setTimeout(() => {
  1022. if (document.body.contains(modal)) {
  1023. document.body.removeChild(modal);
  1024. }
  1025. }, 1500);
  1026. }
  1027.  
  1028. // 修改uploadToImgur函数中的成功处理部分
  1029. function uploadToImgur(file, textareaElement, modal) {
  1030. if (!file.type.match(/image\/.*/)) {
  1031. showStatusInModal(modal, "请选择图片文件", "error");
  1032. const submitBtn = modal.querySelector(".imgur-upload-submit-btn");
  1033. submitBtn.disabled = false;
  1034. submitBtn.textContent = "确认上传";
  1035. return;
  1036. }
  1037.  
  1038. const formData = new FormData();
  1039. formData.append("image", file);
  1040.  
  1041. GM_xmlhttpRequest({
  1042. method: "POST",
  1043. url: "https://api.imgur.com/3/image",
  1044. headers: {
  1045. Authorization: "Client-ID " + CLIENT_ID,
  1046. },
  1047. data: formData,
  1048. responseType: "json",
  1049. onload: function (response) {
  1050. const submitBtn = modal.querySelector(".imgur-upload-submit-btn");
  1051.  
  1052. try {
  1053. let responseData;
  1054. if (typeof response.response === "string") {
  1055. responseData = JSON.parse(response.response);
  1056. } else {
  1057. responseData = response.response;
  1058. }
  1059.  
  1060. if (response.status === 200 && responseData && responseData.success) {
  1061. const imageUrl = responseData.data.link;
  1062. handleUploadSuccess(imageUrl, file.name, modal);
  1063. } else {
  1064. let errorMessage = "";
  1065.  
  1066. if (response.status === 400) {
  1067. if (
  1068. responseData &&
  1069. responseData.data &&
  1070. responseData.data.error
  1071. ) {
  1072. if (
  1073. responseData.data.error === "These actions are forbidden."
  1074. ) {
  1075. errorMessage = "Client ID 无效或已被禁用,请检查配置";
  1076. } else {
  1077. errorMessage = responseData.data.error;
  1078. }
  1079. } else {
  1080. errorMessage = "Client ID 配置错误";
  1081. }
  1082. } else if (response.status === 403) {
  1083. errorMessage = "访问被拒绝,请检查 Client ID 权限";
  1084. } else if (response.status === 429) {
  1085. errorMessage = "请求过于频繁,请稍后再试";
  1086. } else {
  1087. errorMessage = `上传失败 (${response.status})`;
  1088. }
  1089.  
  1090. console.error("Imgur 上传错误:", response);
  1091. showStatusInModal(modal, errorMessage, "error");
  1092.  
  1093. if (response.status === 400 || response.status === 403) {
  1094. const configPanel = modal.querySelector(
  1095. ".imgur-upload-config-panel"
  1096. );
  1097. configPanel.classList.remove("hidden");
  1098. }
  1099.  
  1100. submitBtn.disabled = false;
  1101. submitBtn.textContent = "确认上传";
  1102. }
  1103. } catch (e) {
  1104. console.error("解析响应失败:", e, response);
  1105. showStatusInModal(modal, "响应解析失败,请重试", "error");
  1106.  
  1107. submitBtn.disabled = false;
  1108. submitBtn.textContent = "确认上传";
  1109. }
  1110. },
  1111. onerror: function (error) {
  1112. console.error("GM_xmlhttpRequest 错误:", error);
  1113. showStatusInModal(modal, "网络请求失败,请检查连接", "error");
  1114.  
  1115. const submitBtn = modal.querySelector(".imgur-upload-submit-btn");
  1116. submitBtn.disabled = false;
  1117. submitBtn.textContent = "确认上传";
  1118. },
  1119. ontimeout: function () {
  1120. console.error("Imgur 上传超时");
  1121. showStatusInModal(modal, "上传超时,请重试", "error");
  1122.  
  1123. const submitBtn = modal.querySelector(".imgur-upload-submit-btn");
  1124. submitBtn.disabled = false;
  1125. submitBtn.textContent = "确认上传";
  1126. },
  1127. });
  1128. }
  1129.  
  1130. // 将图片链接插入到文本框
  1131. function insertLinkIntoTextarea(textareaElement, imageUrl, fileName) {
  1132. const altText = fileName ? fileName.split(".")[0] : "image";
  1133. const textToInsert = imageUrl;
  1134.  
  1135. const currentValue = textareaElement.value;
  1136. const selectionStart = textareaElement.selectionStart;
  1137. const selectionEnd = textareaElement.selectionEnd;
  1138.  
  1139. const newText =
  1140. currentValue.substring(0, selectionStart) +
  1141. textToInsert +
  1142. currentValue.substring(selectionEnd);
  1143. textareaElement.value = newText;
  1144.  
  1145. const newCursorPosition = selectionStart + textToInsert.length;
  1146. textareaElement.selectionStart = newCursorPosition;
  1147. textareaElement.selectionEnd = newCursorPosition;
  1148.  
  1149. textareaElement.focus();
  1150. textareaElement.dispatchEvent(
  1151. new Event("input", { bubbles: true, cancelable: true })
  1152. );
  1153. }
  1154.  
  1155. // 在页面头部添加表情和上传按钮
  1156. function addUploadButtonToHeader() {
  1157. const replyBox = document.getElementById("reply-box");
  1158. if (!replyBox) return;
  1159.  
  1160. const headerCell = replyBox.querySelector(".cell.flex-one-row");
  1161. if (!headerCell) return;
  1162.  
  1163. if (headerCell.querySelector(".imgur-upload-btn")) return;
  1164.  
  1165. const leftDiv = headerCell.querySelector("div:first-child");
  1166. if (leftDiv) {
  1167. // 添加表情按钮
  1168. const emojiBtn = document.createElement("a");
  1169. emojiBtn.className = "emoji-btn";
  1170. emojiBtn.textContent = "表情";
  1171. emojiBtn.href = "javascript:void(0);";
  1172. emojiBtn.title = "选择表情";
  1173. emojiBtn.style.marginLeft = "10px";
  1174.  
  1175. leftDiv.appendChild(emojiBtn);
  1176.  
  1177. emojiBtn.addEventListener("click", function (e) {
  1178. e.preventDefault();
  1179. // console.log("表情按钮被点击"); // 调试日志
  1180. const textarea = document.getElementById("reply_content");
  1181. if (textarea) {
  1182. // console.log("创建表情弹窗"); // 调试日志
  1183. createEmojiModal(textarea);
  1184. } else {
  1185. console.error("找不到回复文本框");
  1186. }
  1187. });
  1188.  
  1189. // 添加上传按钮
  1190. const uploadBtn = document.createElement("a");
  1191. uploadBtn.className = "imgur-upload-btn";
  1192. uploadBtn.textContent = "上传";
  1193. uploadBtn.href = "javascript:void(0);";
  1194. uploadBtn.title = "上传图片";
  1195. uploadBtn.style.marginLeft = "10px";
  1196.  
  1197. leftDiv.appendChild(uploadBtn);
  1198.  
  1199. uploadBtn.addEventListener("click", function (e) {
  1200. e.preventDefault();
  1201. // console.log("上传按钮被点击"); // 调试日志
  1202. const textarea = document.getElementById("reply_content");
  1203. if (textarea) {
  1204. // console.log("创建上传弹窗"); // 调试日志
  1205. createUploadModal(textarea);
  1206. }
  1207. });
  1208. }
  1209. }
  1210.  
  1211. // 查找并添加上传按钮
  1212. function findTextareasAndAddButtons() {
  1213. addUploadButtonToHeader();
  1214. setupMutationObserver();
  1215. }
  1216.  
  1217. // 监听DOM变化
  1218. function setupMutationObserver() {
  1219. const observer = new MutationObserver(function (mutations) {
  1220. let shouldCheck = false;
  1221. mutations.forEach(function (mutation) {
  1222. mutation.addedNodes.forEach(function (node) {
  1223. if (
  1224. node.nodeType === Node.ELEMENT_NODE &&
  1225. (node.id === "reply-box" || node.querySelector("#reply-box"))
  1226. ) {
  1227. shouldCheck = true;
  1228. }
  1229. });
  1230. });
  1231.  
  1232. if (shouldCheck) {
  1233. setTimeout(addUploadButtonToHeader, 100);
  1234. }
  1235. });
  1236.  
  1237. observer.observe(document.body, {
  1238. childList: true,
  1239. subtree: true,
  1240. });
  1241. }
  1242.  
  1243. // 初始化脚本
  1244. function init() {
  1245. addStyle();
  1246. setTimeout(findTextareasAndAddButtons, 100);
  1247. }
  1248.  
  1249. if (document.readyState === "loading") {
  1250. document.addEventListener("DOMContentLoaded", init);
  1251. } else {
  1252. init();
  1253. }
  1254.  
  1255. // 插入表情到文本框
  1256. function insertEmoji(emoji) {
  1257. const textarea = document.getElementById("reply_content");
  1258. if (!textarea) {
  1259. console.error("找不到回复文本框");
  1260. return;
  1261. }
  1262.  
  1263. const textToInsert = emoji;
  1264. const currentValue = textarea.value;
  1265. const selectionStart = textarea.selectionStart;
  1266. const selectionEnd = textarea.selectionEnd;
  1267.  
  1268. const newText =
  1269. currentValue.substring(0, selectionStart) +
  1270. textToInsert +
  1271. currentValue.substring(selectionEnd);
  1272. textarea.value = newText;
  1273.  
  1274. const newCursorPosition = selectionStart + textToInsert.length;
  1275. textarea.selectionStart = newCursorPosition;
  1276. textarea.selectionEnd = newCursorPosition;
  1277.  
  1278. textarea.focus();
  1279. textarea.dispatchEvent(
  1280. new Event("input", { bubbles: true, cancelable: true })
  1281. );
  1282.  
  1283. // 关闭弹窗
  1284. const modal = document.querySelector(".emoji-modal");
  1285. if (modal) {
  1286. renderedCategories.clear();
  1287. currentActiveCategory = null;
  1288. modal.remove();
  1289. }
  1290. }
  1291. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址