Gist 共享剪贴板

共享选定文本到Gist并粘贴到剪贴板

  1. // ==UserScript==
  2. // @name Gist Shared Clipboard
  3. // @name:ja Gist 共有クリップボード
  4. // @name:zh-CN Gist 共享剪贴板
  5. // @name:zh-TW Gist 共享剪貼簿
  6. // @license MIT
  7. // @namespace http://tampermonkey.net/
  8. // @version 2025.07.24
  9. // @description Share selected text to Gist and paste it to clipboard
  10. // @description:ja Gistに選択したテキストを共有し、クリップボードに貼り付ける
  11. // @description:zh-CN 共享选定文本到Gist并粘贴到剪贴板
  12. // @description:zh-TW 共享選定文本到Gist並粘貼到剪貼簿
  13. // @author Julia Lee
  14. // @match *://*/*
  15. // @noframes
  16. // @icon https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net
  17. // @grant GM_registerMenuCommand
  18. // @grant GM_setValue
  19. // @grant GM_getValue
  20. // @grant GM_deleteValue
  21. // @grant GM_setClipboard
  22. // @grant GM_notification
  23. // @grant GM_xmlhttpRequest
  24. // @grant GM.xmlHttpRequest
  25. // @connect github.com
  26. // @connect api.github.com
  27. // ==/UserScript==
  28.  
  29. (async function () {
  30. 'use strict';
  31.  
  32. const GITHUB_TOKEN = await GM.getValue('GITHUB_TOKEN', ''); // GitHubのPersonal Access Tokenを指定
  33. const GIST_ID = await GM.getValue('GIST_ID', ''); // GistのIDを指定
  34. const GIST_ENCRYPT_KEY = await GM.getValue('GIST_ENCRYPT_KEY', ''); // Gistの暗号化キーを指定
  35. const FILENAME = 'GM-Shared-Clipboard.txt'; // Gist内のファイル名
  36. const RIGHT_CLICK_MAX_AGE = 20 * 1000; // 右クリックしてからTargetの保持時間(ミリ秒)
  37.  
  38. await GM.deleteValue('GIST_DOWNLOADING');
  39. await GM.deleteValue('GIST_UPLOADING');
  40.  
  41. let crtRightTgtContent = null;
  42. let crtRightTgtUpdated = 0;
  43.  
  44. if (GITHUB_TOKEN && GIST_ID && GIST_ENCRYPT_KEY) {
  45. const menu1 = GM_registerMenuCommand("Gist Share Clipboard", gistUploadClipboard, {
  46. accessKey: 'b',
  47. autoClose: true,
  48. title: 'Share Clipboard text to Gist',
  49. });
  50.  
  51. const menu2 = GM_registerMenuCommand("Gist Share Selected", gistUploadSelected, {
  52. accessKey: 'c',
  53. autoClose: true,
  54. title: 'Share selected text to Gist',
  55. });
  56.  
  57. const menu3 = GM_registerMenuCommand("Gist Paste", gistDowload, {
  58. accessKey: 'v',
  59. autoClose: true,
  60. title: 'Paste Gist content to clipboard',
  61. });
  62.  
  63. if (location.href.includes(GIST_ID)) {
  64. const menu4 = GM_registerMenuCommand("Decrypt Selected Text", decryptSelectedText, {
  65. accessKey: 'd',
  66. autoClose: true,
  67. title: 'Decrypt selected text and update clipboard',
  68. });
  69. }
  70. }
  71.  
  72. const menu0 = GM_registerMenuCommand("Gist Setup", setup, {
  73. accessKey: 'x',
  74. autoClose: true,
  75. title: 'Setup Gist ID and Token',
  76. });
  77.  
  78. document.body.addEventListener("mousedown", event => {
  79. if (event.button == 0) { // left click for mouse
  80. // crtRightTgtContent = null;
  81. } else if (event.button == 1) { // wheel click for mouse
  82. // crtRightTgtContent = null;
  83. } else if (event.button == 2) { // right click for mouse
  84. const elm = event.target;
  85. const nodName = elm.nodeName.toLowerCase();
  86.  
  87. switch (nodName) {
  88. case 'img':
  89. crtRightTgtContent = elm.src;
  90. break;
  91. case 'a':
  92. crtRightTgtContent = elm.href;
  93. break;
  94. default:
  95. crtRightTgtContent = null;
  96. break;
  97. }
  98.  
  99. if (crtRightTgtContent) {
  100. crtRightTgtUpdated = new Date();
  101. }
  102. }
  103. });
  104.  
  105. const gistUrl = `https://api.github.com/gists/${GIST_ID}`;
  106. const headers = {
  107. 'Authorization': `Bearer ${GITHUB_TOKEN}`,
  108. 'Content-Type': 'application/json',
  109. };
  110.  
  111. async function gistUploadClipboard(_event) {
  112. let clipboardText;
  113. try {
  114. clipboardText = await navigator.clipboard.readText();
  115. console.log("Clipboard Text: ", clipboardText);
  116. } catch (e) {
  117. const errorMsg = 'Please execute "Share Clipboard" by right-clicking.';
  118. await showMessage(`❌ ${errorMsg}`, 'NG', 3500);
  119. }
  120.  
  121. if (!clipboardText) { return }
  122. await gistUploadContents(clipboardText || crtRightTgtContent);
  123. }
  124.  
  125. async function gistUploadSelected(_event) {
  126. // If the target is too old, reset it
  127. if (crtRightTgtContent && (new Date()) - crtRightTgtUpdated > RIGHT_CLICK_MAX_AGE) {
  128. crtRightTgtContent = null;
  129. // crtRightTgtUpdated = 0;
  130. }
  131.  
  132. const selectedText = document.getSelection().toString();
  133. if (!crtRightTgtContent && !selectedText) { return }
  134.  
  135. await gistUploadContents(selectedText || crtRightTgtContent);
  136. }
  137.  
  138. const ENCRYPT_KEY = await GM.getValue('GIST_ENCRYPT_KEY', '');
  139.  
  140. async function gistUploadContents(contents) {
  141. if (!contents || contents.length === 0) {
  142. await showMessage('❌ No content to upload!', 'NG', 2500);
  143. return;
  144. }
  145.  
  146. if (!ENCRYPT_KEY) {
  147. await showMessage('❌ No encryption key set!', 'NG', 2500);
  148. return;
  149. }
  150.  
  151. const locked = await GM.getValue('GIST_UPLOADING');
  152. if (locked) {
  153. console.log("Gist is already uploading.");
  154. return;
  155. }
  156.  
  157. // --- ここで暗号化 ---
  158. const encrypted = await encryptContentAES(contents, ENCRYPT_KEY);
  159. const data = {
  160. files: {
  161. [FILENAME]: { content: JSON.stringify(encrypted) }
  162. }
  163. };
  164.  
  165. try {
  166. await GM.setValue('GIST_UPLOADING', true);
  167. const res = await GM.xmlHttpRequest({
  168. method: 'POST',
  169. url: gistUrl,
  170. headers,
  171. data: JSON.stringify(data),
  172. responseType: 'json'
  173. }).catch(e => { throw e; });
  174.  
  175. if (!res || res.status < 200 || res.status >= 300) {
  176. const error = (res && res.response) ? res.response : { message: res && res.statusText ? res.statusText : 'Unknown error' };
  177. throw new Error(`Failed to update Gist: ${error.message}`);
  178. }
  179.  
  180. const result = res.response;
  181. console.log("Gist URL: ", result.html_url);
  182. await showMessage('✅ Target Shared!', 'OK', 2500);
  183. } catch (error) {
  184. console.error("Error: ", error);
  185. await showMessage(`❌ ${error.message}`, 'NG', 2500);
  186. } finally {
  187. await GM.deleteValue('GIST_UPLOADING');
  188. }
  189.  
  190. }
  191.  
  192. async function gistDowload(_event) {
  193. if (inIframe()) {
  194. console.log("Gist Paste is not available in iframe.");
  195. return;
  196. }
  197.  
  198. const locked = await GM.getValue('GIST_DOWNLOADING');
  199. if (locked) {
  200. console.log("Gist is already Downloading.");
  201. return;
  202. }
  203.  
  204. try {
  205. await GM.setValue('GIST_DOWNLOADING', true);
  206. const res = await GM.xmlHttpRequest({
  207. method: 'GET',
  208. url: gistUrl,
  209. headers,
  210. responseType: 'json'
  211. }).catch(e => { throw e; });
  212.  
  213. if (!res || res.status < 200 || res.status >= 300) {
  214. const error = (res && res.response) ? res.response : { message: res && res.statusText ? res.statusText : 'Unknown error' };
  215. throw new Error(`Failed to fetch Gist: ${error.message}`);
  216. }
  217.  
  218. const result = res.response;
  219. const encryptedContent = result.files[FILENAME].content;
  220.  
  221. if (!encryptedContent) {
  222. throw new Error('No content found in the Gist.');
  223. }
  224.  
  225. if (!ENCRYPT_KEY) {
  226. throw new Error('No encryption key set!');
  227. }
  228.  
  229. // --- 復号 ---
  230. let decrypted;
  231. try {
  232. decrypted = await decryptContentAES(JSON.parse(encryptedContent), ENCRYPT_KEY);
  233. } catch (e) {
  234. throw new Error('Failed to decrypt content!');
  235. }
  236.  
  237. await GM.setClipboard(decrypted, "text");
  238. console.log("Gist Content: ", decrypted);
  239. await showMessage('✅ Clipboard Updated!', 'OK', 2500);
  240.  
  241. } catch (error) {
  242. console.error("Error: ", error);
  243. await showMessage(`❌ ${error.message}`, 'NG', 2500);
  244. } finally {
  245. await GM.deleteValue('GIST_DOWNLOADING');
  246. }
  247. }
  248.  
  249. async function setup() {
  250. if (inIframe()) {
  251. console.log("Gist Setup is not available in iframe.");
  252. return;
  253. }
  254.  
  255. const registerDialog = await createRegisterDialog();
  256. const gistIdInput = document.getElementById('gist-id-input');
  257. const gistTokenInput = document.getElementById('gist-token-input');
  258. const gistEncryptKeyInput = document.getElementById('gist-encrypt-key-input');
  259. const gistIdHelpLabel = document.querySelector('#gist-id-help span');
  260. const gistIdHelpLink = document.querySelector('#gist-id-help a');
  261. if (GIST_ID) {
  262. const gistUrl = `https://gist.github.com/${GIST_ID}/revisions`;
  263. gistIdHelpLabel.textContent = 'Gist Revisions URL: ';
  264. gistIdHelpLink.textContent = gistUrl.substring(0, 32) + '...';
  265. gistIdHelpLink.href = gistUrl;
  266. }
  267.  
  268. const saveButton = document.getElementById('save-button');
  269. saveButton.addEventListener('click', async () => {
  270. const gistId = gistIdInput.value;
  271. const token = gistTokenInput.value;
  272. const encryptKey = gistEncryptKeyInput.value;
  273.  
  274. if (!gistId || !token || !encryptKey) {
  275. await showMessage('❌ Gist ID, Token, and Encryption Key are required!', 'NG', 2500);
  276. return;
  277. }
  278.  
  279. await GM.setValue('GIST_ID', gistId);
  280. await GM.setValue('GITHUB_TOKEN', token);
  281. await GM.setValue('GIST_ENCRYPT_KEY', encryptKey);
  282. registerDialog.close();
  283. registerDialog.remove();
  284.  
  285. setTimeout(() => { location.reload() }, 2500); // Restart Script
  286.  
  287. await showMessage('✅ Gist ID, Token, and Encryption Key saved!', 'OK', 2500);
  288.  
  289. });
  290.  
  291. const clearInfoButton = document.getElementById('clear-button');
  292. clearInfoButton.addEventListener('click', async () => {
  293. if (!confirm('Are you sure you want to clear Gist ID, Token, and Encryption Key?')) {
  294. return;
  295. }
  296. await GM.deleteValue('GITHUB_TOKEN');
  297. await GM.deleteValue('GIST_ID');
  298. await GM.deleteValue('GIST_ENCRYPT_KEY');
  299. registerDialog.close();
  300. registerDialog.remove();
  301.  
  302. setTimeout(() => { location.reload() }, 2500); // Restart Script
  303.  
  304. await showMessage('✅ Gist ID, Token, and Encryption Key cleared!', 'OK', 2500);
  305. });
  306.  
  307. const generateKeyButton = document.getElementById('generate-key-button');
  308. generateKeyButton.addEventListener('click', () => {
  309. if (gistEncryptKeyInput.value) {
  310. let confirmMessage = 'Are you sure you want to generate a new encryption key?\n'
  311. confirmMessage += 'This will overwrite the existing key.';
  312. if (!confirm(confirmMessage)) { return }
  313. }
  314. const keyBytes = crypto.getRandomValues(new Uint8Array(16)); // 128bit = 16byte
  315. const keyBase64 = btoa(String.fromCharCode(...keyBytes));
  316. gistEncryptKeyInput.value = keyBase64;
  317. });
  318.  
  319. registerDialog.showModal();
  320. }
  321.  
  322. async function decryptSelectedText() {
  323. const selectedText = document.getSelection().toString();
  324. if (!selectedText) {
  325. await showMessage('❌ No selected text to decrypt!', 'NG', 2500);
  326. return;
  327. }
  328.  
  329. if (!ENCRYPT_KEY) {
  330. await showMessage('❌ No encryption key set!', 'NG', 2500);
  331. return;
  332. }
  333.  
  334. try {
  335. const decrypted = await decryptContentAES(JSON.parse(selectedText), ENCRYPT_KEY);
  336. await GM.setClipboard(decrypted, "text");
  337. console.log("Decrypted Content: ", decrypted);
  338. await showMessage('✅ Clipboard Updated with Decrypted Text!', 'OK', 2500);
  339. } catch (error) {
  340. console.error("Error: ", error);
  341. await showMessage(`❌ ${error.message}`, 'NG', 2500);
  342. }
  343. }
  344.  
  345. })();
  346.  
  347. async function showMessage(text, type = 'OK', duration = 4000) {
  348. const htmlId = `GistShare_Message-${type}`;
  349. const existingMessage = document.getElementById(htmlId);
  350. if (existingMessage) { return; } // 既に表示されている場合は何もしない
  351.  
  352. if (duration < 1000) { duration = 1000; } // 最低1秒は表示する
  353.  
  354. return new Promise((resolve) => {
  355. const message = document.createElement('div');
  356. message.id = `GistShare_Message-${type}`;
  357. message.textContent = text;
  358.  
  359. // 共通スタイル
  360. Object.assign(message.style, {
  361. position: 'fixed',
  362. top: '20px',
  363. right: '20px',
  364. padding: '12px 18px',
  365. borderRadius: '10px',
  366. color: '#fff',
  367. fontSize: '14px',
  368. boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
  369. zIndex: 9999,
  370. transform: 'translateY(20px)',
  371. opacity: '0',
  372. transition: 'opacity 0.4s ease, transform 0.4s ease'
  373. });
  374.  
  375. // タイプ別デザイン
  376. if (type === 'OK') {
  377. message.style.backgroundColor = '#4caf50'; // 緑
  378. message.style.borderLeft = '6px solid #2e7d32';
  379. } else if (type === 'NG') {
  380. message.style.backgroundColor = '#f44336'; // 赤
  381. message.style.borderLeft = '6px solid #b71c1c';
  382. }
  383.  
  384. document.body.appendChild(message);
  385.  
  386. // フェードイン(下から)
  387. setTimeout(() => {
  388. message.style.opacity = '.95';
  389. message.style.transform = 'translateY(0)';
  390. }, 10);
  391. // requestAnimationFrame(() => {
  392. // message.style.opacity = '1';
  393. // message.style.transform = 'translateY(0)';
  394. // });
  395.  
  396. // 指定時間後にフェードアウト
  397. setTimeout(() => {
  398. message.style.opacity = '0';
  399. message.style.transform = 'translateY(-20px)';
  400. setTimeout(() => {
  401. message.remove();
  402. resolve(); // メッセージが削除された後にresolveを呼び出す
  403. }, 400); // transition と一致
  404. }, duration - 400);
  405. });
  406. }
  407.  
  408. async function createRegisterDialog() {
  409. const existDialog = document.getElementById('tm-gist-dialog');
  410. if (existDialog) existDialog.remove();
  411.  
  412. const registerDialog = document.createElement('dialog');
  413. registerDialog.id = 'tm-gist-dialog';
  414. registerDialog.style.padding = '1em';
  415. registerDialog.style.zIndex = 9999;
  416. registerDialog.style.maxWidth = '400px';
  417. registerDialog.style.margin = 'auto';
  418. registerDialog.style.border = '2px solid #ccc';
  419. registerDialog.style.borderRadius = '8px';
  420. registerDialog.style.backgroundColor = '#fff';
  421. registerDialog.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.1)';
  422. registerDialog.style.position = 'fixed';
  423. registerDialog.style.padding = '1.5em';
  424. registerDialog.style.boxSizing = 'border-box';
  425.  
  426. // --- Gist ID ラベルと入力欄 ---
  427. const gistIdLabel = document.createElement('label');
  428. gistIdLabel.textContent = 'Gist ID:';
  429. gistIdLabel.style.display = 'block';
  430. gistIdLabel.style.marginBottom = '0.5em';
  431. gistIdLabel.for = 'gist-id-input';
  432. registerDialog.appendChild(gistIdLabel);
  433.  
  434. const gistIdInput = document.createElement('input');
  435. gistIdInput.id = 'gist-id-input';
  436. gistIdInput.type = 'text';
  437. gistIdInput.style.width = '100%';
  438. gistIdInput.style.boxSizing = 'border-box';
  439. gistIdInput.style.padding = '0.5em';
  440. gistIdInput.style.border = '1px solid #ccc';
  441. gistIdInput.style.borderRadius = '4px';
  442. gistIdInput.style.marginBottom = '1em';
  443. gistIdInput.value = await GM.getValue('GIST_ID', '');
  444. gistIdInput.placeholder = 'Your Gist ID';
  445. registerDialog.appendChild(gistIdInput);
  446.  
  447. const gistIdHelpText = document.createElement('small');
  448. gistIdHelpText.id = 'gist-id-help';
  449. gistIdHelpText.style.display = 'block';
  450. gistIdHelpText.style.marginBottom = '1.1em';
  451. gistIdHelpText.style.color = '#666';
  452. const gistIdHelpLabel = document.createElement('span');
  453. gistIdHelpLabel.textContent = 'Create or Select a Gist: ';
  454. const gistIdHelpLink = document.createElement('a');
  455. gistIdHelpLink.href = 'https://gist.github.com/mine';
  456. gistIdHelpLink.target = '_blank';
  457. gistIdHelpLink.textContent = 'https://gist.github.com/';
  458. gistIdHelpText.appendChild(gistIdHelpLabel);
  459. gistIdHelpText.appendChild(gistIdHelpLink);
  460. registerDialog.appendChild(gistIdHelpText);
  461.  
  462. // --- Gist Token ラベルと入力欄 ---
  463. const gistTokenLabel = document.createElement('label');
  464. gistTokenLabel.textContent = 'Gist Token:';
  465. gistTokenLabel.style.display = 'block';
  466. gistTokenLabel.style.marginBottom = '0.5em';
  467. gistTokenLabel.for = 'gist-token-input';
  468. registerDialog.appendChild(gistTokenLabel);
  469.  
  470. const gistTokenInput = document.createElement('input');
  471. gistTokenInput.id = 'gist-token-input';
  472. // gistTokenInput.type = 'password';
  473. gistTokenInput.style.width = '100%';
  474. gistTokenInput.style.boxSizing = 'border-box';
  475. gistTokenInput.style.padding = '0.5em';
  476. gistTokenInput.style.border = '1px solid #ccc';
  477. gistTokenInput.style.borderRadius = '4px';
  478. gistTokenInput.style.marginBottom = '1em';
  479. gistTokenInput.value = await GM.getValue('GITHUB_TOKEN', '');
  480. gistTokenInput.placeholder = 'ghp_XXXXXXXXXXXXXXXX';
  481. registerDialog.appendChild(gistTokenInput);
  482.  
  483. const gistTokenHelpText = document.createElement('small');
  484. gistTokenHelpText.style.display = 'block';
  485. gistTokenHelpText.style.marginBottom = '1em';
  486. gistTokenHelpText.style.color = '#666';
  487. const gistTokenHelpLabel = document.createElement('span');
  488. gistTokenHelpLabel.textContent = 'Create a Token: ';
  489. gistTokenHelpText.appendChild(gistTokenHelpLabel);
  490. const gistTokenHelpLink = document.createElement('a');
  491. gistTokenHelpLink.href = 'https://github.com/settings/tokens';
  492. gistTokenHelpLink.target = '_blank';
  493. gistTokenHelpLink.textContent = 'https://github.com/settings/tokens';
  494. gistTokenHelpText.appendChild(gistTokenHelpLink);
  495. registerDialog.appendChild(gistTokenHelpText);
  496.  
  497. // --- Encryption Key ラベルと入力欄 ---
  498. const gistEncryptKeyLabel = document.createElement('label');
  499. gistEncryptKeyLabel.textContent = 'Encryption Key:';
  500. gistEncryptKeyLabel.style.display = 'block';
  501. gistEncryptKeyLabel.style.marginBottom = '0.5em';
  502. gistEncryptKeyLabel.for = 'gist-encrypt-key-input';
  503. registerDialog.appendChild(gistEncryptKeyLabel);
  504.  
  505. const gistEncryptKeyInput = document.createElement('input');
  506. gistEncryptKeyInput.id = 'gist-encrypt-key-input';
  507. // gistEncryptKeyInput.type = 'password';
  508. gistEncryptKeyInput.style.width = 'calc(100% - 100px)';
  509. gistEncryptKeyInput.style.boxSizing = 'border-box';
  510. gistEncryptKeyInput.style.padding = '0.5em';
  511. gistEncryptKeyInput.style.border = '1px solid #ccc';
  512. gistEncryptKeyInput.style.borderRadius = '4px';
  513. gistEncryptKeyInput.style.marginBottom = '1em';
  514. gistEncryptKeyInput.style.display = 'inline-block';
  515. gistEncryptKeyInput.value = await GM.getValue('GIST_ENCRYPT_KEY', '');
  516. gistEncryptKeyInput.placeholder = 'Base64 128bit key';
  517. registerDialog.appendChild(gistEncryptKeyInput);
  518.  
  519. // 生成ボタン
  520. const generateKeyButton = document.createElement('button');
  521. generateKeyButton.id = 'generate-key-button';
  522. generateKeyButton.textContent = 'KeyGen';
  523. generateKeyButton.type = 'button';
  524. generateKeyButton.style.width = '90px';
  525. generateKeyButton.style.marginLeft = '10px';
  526. generateKeyButton.style.marginBottom = '1em';
  527. generateKeyButton.style.padding = '0.5em 1em';
  528. generateKeyButton.style.borderRadius = '4px';
  529. generateKeyButton.style.border = '1px solid #888';
  530. generateKeyButton.style.backgroundColor = '#eee';
  531. generateKeyButton.style.cursor = 'pointer';
  532. registerDialog.appendChild(generateKeyButton);
  533.  
  534. const saveButton = document.createElement('button');
  535. saveButton.textContent = 'Save Info';
  536. saveButton.style.backgroundColor = '#4caf50';
  537. saveButton.style.color = '#fff';
  538. saveButton.style.border = 'none';
  539. saveButton.style.padding = '0.5em 1em';
  540. saveButton.style.borderRadius = '4px';
  541. saveButton.style.cursor = 'pointer';
  542. saveButton.style.marginTop = '1em';
  543. saveButton.style.float = 'right';
  544. saveButton.id = 'save-button';
  545. registerDialog.appendChild(saveButton);
  546.  
  547. const clearInfoButton = document.createElement('button');
  548. clearInfoButton.textContent = 'Clear Info';
  549. clearInfoButton.style.backgroundColor = '#f44336';
  550. clearInfoButton.style.color = '#fff';
  551. clearInfoButton.style.border = 'none';
  552. clearInfoButton.style.padding = '0.5em 1em';
  553. clearInfoButton.style.borderRadius = '4px';
  554. clearInfoButton.style.cursor = 'pointer';
  555. clearInfoButton.style.marginTop = '1em';
  556. clearInfoButton.style.marginRight = '0.5em';
  557. clearInfoButton.style.float = 'right';
  558. clearInfoButton.id = 'clear-button';
  559. registerDialog.appendChild(clearInfoButton);
  560.  
  561. const closeButton = document.createElement('button');
  562. closeButton.textContent = 'X';
  563. closeButton.style.position = 'absolute';
  564. closeButton.style.top = '7px';
  565. closeButton.style.right = '7px';
  566. closeButton.style.backgroundColor = '#ccc';
  567. closeButton.style.border = 'none';
  568. closeButton.style.borderRadius = '15%';
  569. closeButton.style.color = '#fff';
  570. closeButton.style.cursor = 'pointer';
  571. closeButton.style.padding = '0.2em 0.5em';
  572. closeButton.style.fontSize = '14px';
  573. closeButton.addEventListener('click', () => {
  574. registerDialog.close();
  575. registerDialog.remove();
  576. });
  577. registerDialog.appendChild(closeButton);
  578.  
  579. const css = document.createElement('style');
  580. css.id = 'tm-gist-css';
  581. css.textContent = `
  582. #tm-gist-dialog button:hover {
  583. opacity: 0.8;
  584. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  585. }`;
  586. registerDialog.appendChild(css);
  587.  
  588. document.body.appendChild(registerDialog);
  589.  
  590. return registerDialog;
  591. }
  592.  
  593. function inIframe() {
  594. try {
  595. return window.self !== window.top;
  596. } catch (e) {
  597. return true;
  598. }
  599. }
  600.  
  601. // --- 追加: 暗号化/復号関数 ---
  602. async function encryptContentAES(plainText, keyBase64) {
  603. // keyBase64: base64エンコードされた16byte(128bit)キー
  604. const enc = new TextEncoder();
  605. const keyBytes = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
  606. if (keyBytes.length !== 16) throw new Error('Encryption key must be 128bit (16 bytes, base64)');
  607. const iv = crypto.getRandomValues(new Uint8Array(16));
  608. const key = await crypto.subtle.importKey(
  609. 'raw', keyBytes, { name: 'AES-CBC' }, false, ['encrypt']
  610. );
  611. const encrypted = await crypto.subtle.encrypt(
  612. { name: 'AES-CBC', iv }, key, enc.encode(plainText)
  613. );
  614. return {
  615. iv: btoa(String.fromCharCode(...iv)),
  616. data: btoa(String.fromCharCode(...new Uint8Array(encrypted)))
  617. };
  618. }
  619.  
  620. async function decryptContentAES(encryptedObj, keyBase64) {
  621. const dec = new TextDecoder();
  622. const keyBytes = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
  623. if (keyBytes.length !== 16) throw new Error('Encryption key must be 128bit (16 bytes, base64)');
  624. const iv = Uint8Array.from(atob(encryptedObj.iv), c => c.charCodeAt(0));
  625. const data = Uint8Array.from(atob(encryptedObj.data), c => c.charCodeAt(0));
  626. const key = await crypto.subtle.importKey(
  627. 'raw', keyBytes, { name: 'AES-CBC' }, false, ['decrypt']
  628. );
  629. const decrypted = await crypto.subtle.decrypt(
  630. { name: 'AES-CBC', iv }, key, data
  631. );
  632. return dec.decode(decrypted);
  633. }

QingJ © 2025

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