Discourse Base64 Helper

Base64编解码工具 for Discourse论坛

当前为 2025-04-03 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Discourse Base64 Helper
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.3.3
  5. // @description Base64编解码工具 for Discourse论坛
  6. // @author Xavier
  7. // @match *://linux.do/*
  8. // @match *://clochat.com/*
  9. // @grant GM_notification
  10. // @grant GM_setClipboard
  11. // @grant GM_addStyle
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // @run-at document-idle
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19.  
  20. // 常量定义
  21. const Z_INDEX = 2147483647;
  22. const SELECTORS = {
  23. POST_CONTENT: '.cooked, .post-body',
  24. DECODED_TEXT: '.decoded-text'
  25. };
  26. const STORAGE_KEYS = {
  27. BUTTON_POSITION: 'btnPosition'
  28. };
  29. const BASE64_REGEX = /(?<!\w)([A-Za-z0-9+/]{6,}?={0,2})(?!\w)/g;
  30. // 样式常量
  31. const STYLES = {
  32. GLOBAL: `
  33. /* 基础内容样式 */
  34. .decoded-text {
  35. cursor: pointer;
  36. transition: all 0.2s;
  37. padding: 1px 3px;
  38. border-radius: 3px;
  39. background-color: #fff3cd !important;
  40. color: #664d03 !important;
  41. }
  42. .decoded-text:hover {
  43. background-color: #ffe69c !important;
  44. }
  45. /* 通知动画 */
  46. @keyframes slideIn {
  47. from {
  48. transform: translate(-50%, -20px);
  49. opacity: 0;
  50. }
  51. to {
  52. transform: translate(-50%, 0);
  53. opacity: 1;
  54. }
  55. }
  56. @keyframes fadeOut {
  57. from { opacity: 1; }
  58. to { opacity: 0; }
  59. }
  60. /* 暗色模式全局样式 */
  61. @media (prefers-color-scheme: dark) {
  62. .decoded-text {
  63. background-color: #332100 !important;
  64. color: #ffd54f !important;
  65. }
  66. .decoded-text:hover {
  67. background-color: #664d03 !important;
  68. }
  69. }
  70. `,
  71. NOTIFICATION: `
  72. .base64-notification {
  73. position: fixed;
  74. top: 20px;
  75. left: 50%;
  76. transform: translateX(-50%);
  77. padding: 12px 24px;
  78. border-radius: 8px;
  79. z-index: ${Z_INDEX};
  80. animation: slideIn 0.3s forwards, fadeOut 0.3s 2s forwards;
  81. font-family: system-ui, -apple-system, sans-serif;
  82. pointer-events: none;
  83. backdrop-filter: blur(4px);
  84. border: 1px solid rgba(255, 255, 255, 0.1);
  85. max-width: 80vw;
  86. text-align: center;
  87. line-height: 1.5;
  88. background: rgba(255, 255, 255, 0.95);
  89. color: #2d3748;
  90. box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
  91. }
  92. .base64-notification[data-type="success"] {
  93. background: rgba(72, 187, 120, 0.95) !important;
  94. color: #f7fafc !important;
  95. }
  96. .base64-notification[data-type="error"] {
  97. background: rgba(245, 101, 101, 0.95) !important;
  98. color: #f8fafc !important;
  99. }
  100. .base64-notification[data-type="info"] {
  101. background: rgba(66, 153, 225, 0.95) !important;
  102. color: #f7fafc !important;
  103. }
  104. @media (prefers-color-scheme: dark) {
  105. .base64-notification {
  106. background: rgba(26, 32, 44, 0.95) !important;
  107. color: #e2e8f0 !important;
  108. box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
  109. border-color: rgba(255, 255, 255, 0.05);
  110. }
  111. .base64-notification[data-type="success"] {
  112. background: rgba(22, 101, 52, 0.95) !important;
  113. }
  114. .base64-notification[data-type="error"] {
  115. background: rgba(155, 28, 28, 0.95) !important;
  116. }
  117. .base64-notification[data-type="info"] {
  118. background: rgba(29, 78, 216, 0.95) !important;
  119. }
  120. }
  121. `,
  122. SHADOW_DOM: `
  123. :host {
  124. all: initial !important;
  125. position: fixed !important;
  126. z-index: ${Z_INDEX} !important;
  127. pointer-events: none !important;
  128. }
  129. .base64-helper {
  130. position: fixed;
  131. z-index: ${Z_INDEX} !important;
  132. transform: translateZ(100px);
  133. cursor: move;
  134. font-family: system-ui, -apple-system, sans-serif;
  135. opacity: 0.5;
  136. transition: opacity 0.3s ease, transform 0.2s;
  137. pointer-events: auto !important;
  138. will-change: transform;
  139. }
  140. .base64-helper:hover {
  141. opacity: 1 !important;
  142. }
  143. .main-btn {
  144. background: #ffffff;
  145. color: #000000 !important;
  146. padding: 8px 16px;
  147. border-radius: 6px;
  148. box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
  149. font-weight: 500;
  150. user-select: none;
  151. transition: all 0.2s;
  152. font-size: 14px;
  153. cursor: pointer;
  154. border: none !important;
  155. }
  156. .menu {
  157. position: absolute;
  158. bottom: calc(100% + 5px);
  159. right: 0;
  160. background: #ffffff;
  161. border-radius: 6px;
  162. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  163. display: none;
  164. min-width: auto !important;
  165. width: max-content !important;
  166. overflow: hidden;
  167. }
  168. .menu-item {
  169. padding: 8px 12px !important;
  170. color: #333 !important;
  171. transition: all 0.2s;
  172. font-size: 13px;
  173. cursor: pointer;
  174. position: relative;
  175. border-radius: 0 !important;
  176. isolation: isolate;
  177. white-space: nowrap !important;
  178. }
  179. .menu-item:hover::before {
  180. content: '';
  181. position: absolute;
  182. top: 0;
  183. left: 0;
  184. right: 0;
  185. bottom: 0;
  186. background: currentColor;
  187. opacity: 0.1;
  188. z-index: -1;
  189. }
  190. @media (prefers-color-scheme: dark) {
  191. .main-btn {
  192. background: #2d2d2d;
  193. color: #fff !important;
  194. box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
  195. }
  196. .menu {
  197. background: #1a1a1a;
  198. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
  199. }
  200. .menu-item {
  201. color: #e0e0e0 !important;
  202. }
  203. .menu-item:hover::before {
  204. opacity: 0.08;
  205. }
  206. }
  207. `
  208. };
  209.  
  210. // 样式初始化
  211. const initStyles = () => {
  212. GM_addStyle(STYLES.GLOBAL + STYLES.NOTIFICATION);
  213. };
  214.  
  215.  
  216. class Base64Helper {
  217. constructor() {
  218. this.originalContents = new Map();
  219. this.isDragging = false;
  220. this.menuVisible = false;
  221. this.resizeTimer = null;
  222. this.initUI();
  223. this.initEventListeners();
  224. this.addRouteListeners();
  225. }
  226.  
  227. // UI 初始化
  228. initUI() {
  229. if (document.getElementById('base64-helper-root')) return;
  230.  
  231. this.container = document.createElement('div');
  232. this.container.id = 'base64-helper-root';
  233. document.body.append(this.container);
  234.  
  235. this.shadowRoot = this.container.attachShadow({ mode: 'open' });
  236. this.shadowRoot.appendChild(this.createShadowStyles());
  237. this.shadowRoot.appendChild(this.createMainUI());
  238.  
  239. this.initPosition();
  240. }
  241.  
  242. createShadowStyles() {
  243. const style = document.createElement('style');
  244. style.textContent = STYLES.SHADOW_DOM;
  245. return style;
  246. }
  247.  
  248. createMainUI() {
  249. const uiContainer = document.createElement('div');
  250. uiContainer.className = 'base64-helper';
  251.  
  252. this.mainBtn = this.createButton('Base64', 'main-btn');
  253. this.menu = this.createMenu();
  254.  
  255. uiContainer.append(this.mainBtn, this.menu);
  256. return uiContainer;
  257. }
  258.  
  259. createButton(text, className) {
  260. const btn = document.createElement('button');
  261. btn.className = className;
  262. btn.textContent = text;
  263. return btn;
  264. }
  265.  
  266. createMenu() {
  267. const menu = document.createElement('div');
  268. menu.className = 'menu';
  269.  
  270. this.decodeBtn = this.createMenuItem('解析本页Base64', 'decode');
  271. this.encodeBtn = this.createMenuItem('文本转Base64');
  272.  
  273. menu.append(this.decodeBtn, this.encodeBtn);
  274. return menu;
  275. }
  276.  
  277. createMenuItem(text, mode) {
  278. const item = document.createElement('div');
  279. item.className = 'menu-item';
  280. item.textContent = text;
  281. if (mode) item.dataset.mode = mode;
  282. return item;
  283. }
  284.  
  285. // 位置管理
  286. initPosition() {
  287. const pos = this.positionManager.get() || {
  288. x: window.innerWidth - 120,
  289. y: window.innerHeight - 80
  290. };
  291.  
  292. const ui = this.shadowRoot.querySelector('.base64-helper');
  293. ui.style.left = `${pos.x}px`;
  294. ui.style.top = `${pos.y}px`;
  295. }
  296.  
  297. get positionManager() {
  298. return {
  299. get: () => {
  300. const saved = GM_getValue(STORAGE_KEYS.BUTTON_POSITION);
  301. if (!saved) return null;
  302.  
  303. const ui = this.shadowRoot.querySelector('.base64-helper');
  304. const maxX = window.innerWidth - ui.offsetWidth - 20;
  305. const maxY = window.innerHeight - ui.offsetHeight - 20;
  306.  
  307. return {
  308. x: Math.min(Math.max(saved.x, 20), maxX),
  309. y: Math.min(Math.max(saved.y, 20), maxY)
  310. };
  311. },
  312. set: (x, y) => {
  313. const ui = this.shadowRoot.querySelector('.base64-helper');
  314. const pos = {
  315. x: Math.max(20, Math.min(x, window.innerWidth - ui.offsetWidth - 20)),
  316. y: Math.max(20, Math.min(y, window.innerHeight - ui.offsetHeight - 20))
  317. };
  318.  
  319. GM_setValue(STORAGE_KEYS.BUTTON_POSITION, pos);
  320. return pos;
  321. }
  322. };
  323. }
  324.  
  325. // 事件监听
  326. initEventListeners() {
  327. this.mainBtn.addEventListener('click', (e) => this.toggleMenu(e));
  328. document.addEventListener('click', (e) => this.handleDocumentClick(e));
  329.  
  330. // 拖拽事件
  331. this.mainBtn.addEventListener('mousedown', (e) => this.startDrag(e));
  332. document.addEventListener('mousemove', (e) => this.drag(e));
  333. document.addEventListener('mouseup', () => this.stopDrag());
  334.  
  335. // 功能按钮
  336. this.decodeBtn.addEventListener('click', () => this.handleDecode());
  337. this.encodeBtn.addEventListener('click', () => this.handleEncode());
  338.  
  339. // 窗口resize
  340. window.addEventListener('resize', () => this.handleResize());
  341. }
  342.  
  343. // 菜单切换
  344. toggleMenu(e) {
  345. e.stopPropagation();
  346. this.menuVisible = !this.menuVisible;
  347. this.menu.style.display = this.menuVisible ? 'block' : 'none';
  348. }
  349.  
  350. handleDocumentClick(e) {
  351. if (this.menuVisible && !this.shadowRoot.contains(e.target)) {
  352. this.menuVisible = false;
  353. this.menu.style.display = 'none';
  354. }
  355. }
  356.  
  357. // 拖拽功能
  358. startDrag(e) {
  359. this.isDragging = true;
  360. this.startX = e.clientX;
  361. this.startY = e.clientY;
  362. const rect = this.shadowRoot.querySelector('.base64-helper').getBoundingClientRect();
  363. this.initialX = rect.left;
  364. this.initialY = rect.top;
  365. this.shadowRoot.querySelector('.base64-helper').style.transition = 'none';
  366. }
  367.  
  368. drag(e) {
  369. if (!this.isDragging) return;
  370. const dx = e.clientX - this.startX;
  371. const dy = e.clientY - this.startY;
  372.  
  373. const newX = this.initialX + dx;
  374. const newY = this.initialY + dy;
  375.  
  376. const pos = this.positionManager.set(newX, newY);
  377. const ui = this.shadowRoot.querySelector('.base64-helper');
  378. ui.style.left = `${pos.x}px`;
  379. ui.style.top = `${pos.y}px`;
  380. }
  381.  
  382. stopDrag() {
  383. this.isDragging = false;
  384. this.shadowRoot.querySelector('.base64-helper').style.transition = 'opacity 0.3s ease';
  385. }
  386.  
  387. // 窗口resize处理
  388. handleResize() {
  389. clearTimeout(this.resizeTimer);
  390. this.resizeTimer = setTimeout(() => {
  391. const pos = this.positionManager.get();
  392. if (pos) {
  393. const ui = this.shadowRoot.querySelector('.base64-helper');
  394. ui.style.left = `${pos.x}px`;
  395. ui.style.top = `${pos.y}px`;
  396. }
  397. }, 100);
  398. }
  399.  
  400. // 路由监听
  401. addRouteListeners() {
  402. const handleRouteChange = () => {
  403. //GM_setValue(STORAGE_KEYS.BUTTON_POSITION, this.positionManager.get());
  404. this.resetState();
  405. };
  406.  
  407. // 重写history方法
  408. const originalPushState = history.pushState;
  409. const originalReplaceState = history.replaceState;
  410.  
  411. history.pushState = (...args) => {
  412. originalPushState.apply(history, args);
  413. handleRouteChange();
  414. };
  415.  
  416. history.replaceState = (...args) => {
  417. originalReplaceState.apply(history, args);
  418. handleRouteChange();
  419. };
  420.  
  421. // 事件监听
  422. [
  423. 'popstate',
  424. 'hashchange',
  425. 'turbo:render',
  426. 'discourse:before-auto-refresh',
  427. 'page:changed'
  428. ].forEach(event => {
  429. window.addEventListener(event, handleRouteChange);
  430. });
  431. }
  432.  
  433.  
  434. // 核心功能
  435. handleDecode() {
  436. if (this.decodeBtn.dataset.mode === 'restore') {
  437. this.restoreContent();
  438. return;
  439. }
  440.  
  441. this.originalContents.clear();
  442. let hasValidBase64 = false;
  443.  
  444. try {
  445. document.querySelectorAll(SELECTORS.POST_CONTENT).forEach(element => {
  446. let newHtml = element.innerHTML;
  447. let modified = false;
  448.  
  449. Array.from(newHtml.matchAll(BASE64_REGEX)).reverse().forEach(match => {
  450. const original = match[0];
  451. if (!this.validateBase64(original)) return;
  452.  
  453. try {
  454. const decoded = this.decodeBase64(original);
  455. this.originalContents.set(element, element.innerHTML);
  456.  
  457. newHtml = newHtml.substring(0, match.index) +
  458. `<span class="decoded-text">${decoded}</span>` +
  459. newHtml.substring(match.index + original.length);
  460.  
  461. hasValidBase64 = modified = true;
  462. } catch(e) {}
  463. });
  464.  
  465. if (modified) element.innerHTML = newHtml;
  466. });
  467.  
  468. if (!hasValidBase64) {
  469. this.showNotification('本页未发现有效Base64内容', 'info');
  470. this.originalContents.clear();
  471. return;
  472. }
  473.  
  474. document.querySelectorAll(SELECTORS.DECODED_TEXT).forEach(el => {
  475. el.addEventListener('click', (e) => this.copyToClipboard(e));
  476. });
  477.  
  478. this.decodeBtn.textContent = '恢复本页Base64';
  479. this.decodeBtn.dataset.mode = 'restore';
  480. this.showNotification('解析完成', 'success');
  481. } catch (e) {
  482. this.showNotification('解析失败: ' + e.message, 'error');
  483. this.originalContents.clear();
  484. }
  485.  
  486. this.menuVisible = false;
  487. this.menu.style.display = 'none';
  488. }
  489.  
  490. handleEncode() {
  491. const text = prompt('请输入要编码的文本:');
  492. if (text === null) return;
  493.  
  494. try {
  495. const encoded = this.encodeBase64(text);
  496. GM_setClipboard(encoded);
  497. this.showNotification('Base64已复制', 'success');
  498. } catch (e) {
  499. this.showNotification('编码失败: ' + e.message, 'error');
  500. }
  501. this.menu.style.display = 'none';
  502. }
  503.  
  504. // 工具方法
  505. validateBase64(str) {
  506. return typeof str === 'string' &&
  507. str.length >= 6 &&
  508. str.length % 4 === 0 &&
  509. /^[A-Za-z0-9+/]+={0,2}$/.test(str) &&
  510. str.replace(/=+$/, '').length >= 6;
  511. }
  512.  
  513.  
  514. decodeBase64(str) {
  515. return decodeURIComponent(escape(atob(str)));
  516. }
  517.  
  518. encodeBase64(str) {
  519. return btoa(unescape(encodeURIComponent(str)));
  520. }
  521.  
  522. restoreContent() {
  523. this.originalContents.forEach((html, element) => {
  524. element.innerHTML = html;
  525. });
  526. this.originalContents.clear();
  527. this.decodeBtn.textContent = '解析本页Base64';
  528. this.decodeBtn.dataset.mode = 'decode';
  529. this.showNotification('已恢复原始内容', 'success');
  530. this.menu.style.display = 'none';
  531. }
  532.  
  533. copyToClipboard(e) {
  534. GM_setClipboard(e.target.innerText);
  535. this.showNotification('内容已复制', 'success');
  536. e.stopPropagation();
  537. }
  538.  
  539. resetState() {
  540. if (this.decodeBtn.dataset.mode === 'restore') {
  541. this.restoreContent();
  542. }
  543. }
  544.  
  545. showNotification(text, type) {
  546. const notification = document.createElement('div');
  547. notification.className = 'base64-notification';
  548. notification.setAttribute('data-type', type);
  549. notification.textContent = text;
  550. document.body.appendChild(notification);
  551. setTimeout(() => notification.remove(), 2300);
  552. }
  553.  
  554.  
  555. }
  556.  
  557. // 初始化
  558. initStyles();
  559. const instance = new Base64Helper();
  560.  
  561. // 防冲突处理和清理
  562. if (window.__base64HelperInstance) {
  563. window.__base64HelperInstance.destroy();
  564. }
  565. window.__base64HelperInstance = instance;
  566.  
  567. // 页面卸载时清理
  568. window.addEventListener('unload', () => {
  569. instance.destroy();
  570. delete window.__base64HelperInstance;
  571. });
  572. })();

QingJ © 2025

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