Gist 共享剪贴板

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

当前为 2025-05-22 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Gist Shared Clipboard
  3. // @name:ja Gist 共有クリップボード
  4. // @name:zh-CN Gist 共享剪贴板
  5. // @name:zh-TW Gist 共享剪貼簿
  6. // @description Share selected text to Gist and paste it to clipboard
  7. // @description:ja Gistに選択したテキストを共有し、クリップボードに貼り付ける
  8. // @description:zh-CN 共享选定文本到Gist并粘贴到剪贴板
  9. // @description:zh-TW 共享選定文本到Gist並粘貼到剪貼簿
  10. // @license MIT
  11. // @namespace http://tampermonkey.net/
  12. // @version 2025-05-20
  13. // @description Share selected text to Gist and paste it to clipboard
  14. // @author Julia Lee
  15. // @match *://*/*
  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. // ==/UserScript==
  23.  
  24. (async function () {
  25. 'use strict';
  26.  
  27. const GITHUB_TOKEN = await GM.getValue('GITHUB_TOKEN', ''); // GitHubのPersonal Access Tokenを指定
  28. const GIST_ID = await GM.getValue('GIST_ID', ''); // GistのIDを指定
  29. const FILENAME = 'GM-Shared-Clipboard.txt'; // Gist内のファイル名
  30.  
  31. await GM.deleteValue('GIST_DOWNLOADING');
  32. await GM.deleteValue('GIST_UPLOADING');
  33.  
  34. let crtRightTgtContent = null;
  35. let crtRightTgtUpdated = 0;
  36.  
  37. if (GITHUB_TOKEN && GIST_ID) {
  38. const menu1 = GM_registerMenuCommand("Gist Share", gistUpload, {
  39. accessKey: 'c',
  40. autoClose: true,
  41. title: 'Share selected text to Gist',
  42. });
  43.  
  44. const menu2 = GM_registerMenuCommand("Gist Paste", gistDowload, {
  45. accessKey: 'v',
  46. autoClose: true,
  47. title: 'Paste Gist content to clipboard',
  48. });
  49. }
  50.  
  51. const menu3 = GM_registerMenuCommand("Gist Setup", setup, {
  52. accessKey: 's',
  53. autoClose: true,
  54. title: 'Setup Gist ID and Token',
  55. });
  56.  
  57. if (GITHUB_TOKEN && GIST_ID) {
  58. const menu4 = GM_registerMenuCommand("Gist Clear", async () =>{
  59. await GM.deleteValue('GITHUB_TOKEN');
  60. await GM.deleteValue('GIST_ID');
  61. setTimeout(() => { location.reload() }, 2500); // Restart Script
  62. showMessage('✅ Gist ID and Token cleared!', 'OK', 2500);
  63. }, {
  64. // accessKey: 'x',
  65. autoClose: true,
  66. title: 'Clear Gist ID and Token',
  67. });
  68. }
  69.  
  70. document.body.addEventListener("mousedown", event => {
  71. if (event.button == 0) { // left click for mouse
  72. // crtRightTgtContent = null;
  73. } else if (event.button == 1) { // wheel click for mouse
  74. // crtRightTgtContent = null;
  75. } else if (event.button == 2) { // right click for mouse
  76. const elm = event.target;
  77. const nodName = elm.nodeName.toLowerCase();
  78.  
  79. switch (nodName) {
  80. case 'img':
  81. crtRightTgtContent = elm.src;
  82. break;
  83. case 'a':
  84. crtRightTgtContent = elm.href;
  85. break;
  86. default:
  87. crtRightTgtContent = null;
  88. break;
  89. }
  90.  
  91. if (crtRightTgtContent) {
  92. crtRightTgtUpdated = new Date();
  93. }
  94. }
  95. });
  96.  
  97. const gistUrl = `https://api.github.com/gists/${GIST_ID}`;
  98. const headers = {
  99. 'Authorization': `Bearer ${GITHUB_TOKEN}`,
  100. 'Content-Type': 'application/json',
  101. };
  102.  
  103. async function gistUpload(_event) {
  104. // If the target is too old, reset it
  105. if (crtRightTgtContent && (new Date()) - crtRightTgtUpdated > 30*1000) {
  106. crtRightTgtContent = null;
  107. // crtRightTgtUpdated = 0;
  108. }
  109.  
  110. const selectedText = document.getSelection().toString();
  111. if (!crtRightTgtContent && !selectedText) { return }
  112.  
  113. const locked = await GM.getValue('GIST_UPLOADING');
  114. if (locked) {
  115. console.log("Gist is already uploading.");
  116. return;
  117. }
  118.  
  119. const data = {
  120. files: {
  121. [FILENAME]: { content: selectedText || crtRightTgtContent }
  122. }
  123. };
  124.  
  125. try {
  126. await GM.setValue('GIST_UPLOADING', true);
  127. const res = await fetch(gistUrl, {
  128. method: 'POST', headers,
  129. body: JSON.stringify(data)
  130. });
  131.  
  132. if (!res.ok) {
  133. const error = await res.json();
  134. throw new Error(`Failed to update Gist: ${error.message}`);
  135. }
  136.  
  137. const result = await res.json();
  138.  
  139. await GM.setClipboard(result.html_url, "text")
  140. await showMessage('✅ Target Shared!', 'OK', 2500);
  141. } catch (error) {
  142. console.error("Error: ", error);
  143. await showMessage(`❌ ${error.message}`, 'NG', 2500);
  144. }
  145. finally {
  146. await GM.deleteValue('GIST_UPLOADING');
  147. }
  148. }
  149.  
  150. async function gistDowload(_event) {
  151. if (inIframe()) {
  152. console.log("Gist Paste is not available in iframe.");
  153. return;
  154. }
  155.  
  156. const locked = await GM.getValue('GIST_DOWNLOADING');
  157. if (locked) {
  158. console.log("Gist is already Downloading.");
  159. return;
  160. }
  161.  
  162. try {
  163. await GM.setValue('GIST_DOWNLOADING', true);
  164. const res = await fetch(gistUrl, { headers });
  165. if (!res.ok) {
  166. const error = await res.json();
  167. throw new Error(`Failed to fetch Gist: ${error.message}`);
  168. }
  169.  
  170. const result = await res.json();
  171. const content = result.files[FILENAME].content;
  172.  
  173. if (!content) {
  174. throw new Error('No content found in the Gist.');
  175. }
  176.  
  177. await GM.setClipboard(content, "text");
  178. console.log("Gist Content: ", content);
  179. await showMessage('✅ Clipboard Updated!', 'OK', 2500);
  180.  
  181. } catch (error) {
  182. console.error("Error: ", error);
  183. await showMessage(`❌ ${error.message}`, 'NG', 2500);
  184. } finally {
  185. await GM.deleteValue('GIST_DOWNLOADING');
  186. }
  187. }
  188.  
  189. async function setup() {
  190. if (inIframe()) {
  191. console.log("Gist Setup is not available in iframe.");
  192. return;
  193. }
  194.  
  195. const dialog = await createRegisterDialog();
  196. dialog.showModal();
  197. const button = document.getElementById('save-button');
  198. const input = document.getElementById('gist-id-input');
  199. const input2 = document.getElementById('gist-token-input');
  200. button.addEventListener('click', async () => {
  201. const gistId = input.value;
  202. const token = input2.value;
  203.  
  204. if (!gistId || !token) {
  205. await showMessage('❌ Gist ID and Token are required!', 'NG', 2500);
  206. return;
  207. }
  208.  
  209. await GM.setValue('GIST_ID', gistId);
  210. await GM.setValue('GITHUB_TOKEN', token);
  211. dialog.close();
  212. dialog.remove();
  213.  
  214. setTimeout(() => { location.reload() }, 2500); // Restart Script
  215.  
  216. await showMessage('✅ Gist ID and Token saved!', 'OK', 2500);
  217.  
  218. });
  219. }
  220.  
  221. })();
  222.  
  223. async function showMessage(text, type = 'OK', duration = 4000) {
  224. const htmlId = `GistShare_Message-${type}`;
  225. const existingMessage = document.getElementById(htmlId);
  226. if (existingMessage) { return; } // 既に表示されている場合は何もしない
  227.  
  228. if (duration < 1000) { duration = 1000; } // 最低1秒は表示する
  229.  
  230. return new Promise((resolve) => {
  231. const message = document.createElement('div');
  232. message.id = `GistShare_Message-${type}`;
  233. message.textContent = text;
  234.  
  235. // 共通スタイル
  236. Object.assign(message.style, {
  237. position: 'fixed',
  238. top: '20px',
  239. right: '20px',
  240. padding: '12px 18px',
  241. borderRadius: '10px',
  242. color: '#fff',
  243. fontSize: '14px',
  244. boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
  245. zIndex: 9999,
  246. transform: 'translateY(20px)',
  247. opacity: '0',
  248. transition: 'opacity 0.4s ease, transform 0.4s ease'
  249. });
  250.  
  251. // タイプ別デザイン
  252. if (type === 'OK') {
  253. message.style.backgroundColor = '#4caf50'; // 緑
  254. message.style.borderLeft = '6px solid #2e7d32';
  255. } else if (type === 'NG') {
  256. message.style.backgroundColor = '#f44336'; // 赤
  257. message.style.borderLeft = '6px solid #b71c1c';
  258. }
  259.  
  260. document.body.appendChild(message);
  261.  
  262. // フェードイン(下から)
  263. setTimeout(() => {
  264. message.style.opacity = '.95';
  265. message.style.transform = 'translateY(0)';
  266. }, 10);
  267. // requestAnimationFrame(() => {
  268. // message.style.opacity = '1';
  269. // message.style.transform = 'translateY(0)';
  270. // });
  271.  
  272. // 指定時間後にフェードアウト
  273. setTimeout(() => {
  274. message.style.opacity = '0';
  275. message.style.transform = 'translateY(-20px)';
  276. setTimeout(() => {
  277. message.remove();
  278. resolve(); // メッセージが削除された後にresolveを呼び出す
  279. }, 400); // transition と一致
  280. }, duration - 400);
  281. });
  282. }
  283.  
  284. async function createRegisterDialog() {
  285. const existing = document.getElementById('tm-gist-dialog');
  286. if (existing) existing.remove();
  287.  
  288. const dialog = document.createElement('dialog');
  289. dialog.id = 'tm-gist-dialog';
  290. dialog.style.padding = '1em';
  291. dialog.style.zIndex = 9999;
  292.  
  293. const label = document.createElement('label');
  294. label.textContent = 'Gist ID:';
  295. label.style.display = 'block';
  296. label.style.marginBottom = '0.5em';
  297. label.for = 'gist-id-input';
  298. dialog.appendChild(label);
  299. const input = document.createElement('input');
  300. input.id = 'gist-id-input';
  301. input.type = 'text';
  302. input.style.width = '100%';
  303. input.style.boxSizing = 'border-box';
  304. input.style.padding = '0.5em';
  305. input.style.border = '1px solid #ccc';
  306. input.style.borderRadius = '4px';
  307. input.style.marginBottom = '1em';
  308. input.value = await GM.getValue('GIST_ID', '');
  309. input.placeholder = 'Your Gist ID';
  310. dialog.appendChild(input);
  311. const small = document.createElement('small');
  312. small.style.display = 'block';
  313. small.style.marginBottom = '1.1em';
  314. small.style.color = '#666';
  315. const span = document.createElement('span');
  316. span.textContent = 'Create or Select a Gist: ';
  317. small.appendChild(span);
  318. const a = document.createElement('a');
  319. a.href = 'https://gist.github.com/mine';
  320. a.target = '_blank';
  321. a.textContent = 'https://gist.github.com/';
  322. small.appendChild(a);
  323. dialog.appendChild(small);
  324.  
  325. const label2 = document.createElement('label');
  326. label2.textContent = 'Gist Token:';
  327. label2.style.display = 'block';
  328. label2.style.marginBottom = '0.5em';
  329. label2.for = 'gist-token-input';
  330. dialog.appendChild(label2);
  331. const input2 = document.createElement('input');
  332. input2.id = 'gist-token-input';
  333. input2.type = 'password';
  334. input2.style.width = '100%';
  335. input2.style.boxSizing = 'border-box';
  336. input2.style.padding = '0.5em';
  337. input2.style.border = '1px solid #ccc';
  338. input2.style.borderRadius = '4px';
  339. input2.style.marginBottom = '1em';
  340. input2.value = await GM.getValue('GITHUB_TOKEN', '');
  341. input2.placeholder = 'ghp_XXXXXXXXXXXXXXXX';
  342. dialog.appendChild(input2);
  343. const small2 = document.createElement('small');
  344. small2.style.display = 'block';
  345. small2.style.marginBottom = '1em';
  346. small2.style.color = '#666';
  347. const span2 = document.createElement('span');
  348. span2.textContent = 'Create a Token: ';
  349. small2.appendChild(span2);
  350. const a2 = document.createElement('a');
  351. a2.href = 'https://github.com/settings/tokens';
  352. a2.target = '_blank';
  353. a2.textContent = 'https://github.com/settings/tokens';
  354. small2.appendChild(a2);
  355. dialog.appendChild(small2);
  356.  
  357. const button = document.createElement('button');
  358. button.textContent = 'Save Info';
  359. button.style.backgroundColor = '#4caf50';
  360. button.style.color = '#fff';
  361. button.style.border = 'none';
  362. button.style.padding = '0.5em 1em';
  363. button.style.borderRadius = '4px';
  364. button.style.cursor = 'pointer';
  365. button.style.marginTop = '1em';
  366. button.style.float = 'right';
  367. button.id = 'save-button';
  368. dialog.appendChild(button);
  369.  
  370. document.body.appendChild(dialog);
  371.  
  372. return dialog;
  373. }
  374.  
  375. function inIframe() {
  376. try {
  377. return window.self !== window.top;
  378. } catch (e) {
  379. return true;
  380. }
  381. }

QingJ © 2025

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