Discourse Base64 Helper

Base64编解码工具 for Discourse论坛

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

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

QingJ © 2025

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