Clip-to-Gist

One-click clipboard quote → GitHub Gist, with keyword highlighting

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

  1. // ==UserScript==
  2. // @name Clip-to-Gist
  3. // @name:zh-CN Clip-to-Gist 金句剪贴脚本(v2)
  4. // @namespace https://github.com/yourusername
  5. // @version 2.0
  6. // @description One-click clipboard quote → GitHub Gist, with keyword highlighting
  7. // @description:zh-CN 一键剪贴板金句并上传至 GitHub Gist,支持关键词标注和高亮
  8. // @author Your Name
  9. // @match *://*/*
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // @grant GM_xmlhttpRequest
  13. // @grant GM_addStyle
  14. // @grant GM_registerMenuCommand
  15. // @license MIT
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. 'use strict';
  20.  
  21. // 注册(不可用)菜单:配置 Gist 参数
  22. GM_registerMenuCommand('配置 Gist 参数', openConfigModal);
  23.  
  24. // 注入右下角触发按钮
  25. const trigger = document.createElement('div');
  26. trigger.id = 'clip2gist-trigger';
  27. trigger.textContent = '📝';
  28. document.body.appendChild(trigger);
  29.  
  30. // 样式
  31. GM_addStyle(`
  32. #clip2gist-trigger {
  33. position: fixed; bottom: 20px; right: 20px;
  34. width: 40px; height: 40px; line-height: 40px;
  35. background: #4CAF50; color: #fff; text-align: center;
  36. border-radius: 50%; cursor: pointer; z-index: 9999;
  37. font-size: 24px;
  38. }
  39. .clip2gist-mask {
  40. position: fixed; inset: 0; background: rgba(0,0,0,0.5);
  41. display: flex; align-items: center; justify-content: center;
  42. z-index: 10000;
  43. }
  44. .clip2gist-dialog {
  45. background: #fff; padding: 20px; border-radius: 8px;
  46. max-width: 90%; max-height: 90%; overflow: auto;
  47. box-shadow: 0 2px 10px rgba(0,0,0,0.3);
  48. }
  49. .clip2gist-dialog input {
  50. width: 100%; padding: 6px; margin-top: 4px; margin-bottom: 12px;
  51. box-sizing: border-box; font-size: 14px;
  52. }
  53. .clip2gist-dialog button {
  54. margin-left: 8px; padding: 6px 12px; font-size: 14px;
  55. }
  56. .clip2gist-word {
  57. display: inline-block; margin: 2px; padding: 4px 6px;
  58. border: 1px solid #ccc; border-radius: 4px; cursor: pointer;
  59. user-select: none;
  60. }
  61. .clip2gist-word.selected {
  62. background: #ffeb3b; border-color: #f1c40f;
  63. }
  64. #clip2gist-preview {
  65. margin-top: 12px; padding: 8px; border: 1px solid #ddd;
  66. min-height: 40px; font-family: monospace;
  67. }
  68. `);
  69.  
  70. trigger.addEventListener('click', mainFlow);
  71.  
  72. async function mainFlow() {
  73. let text = '';
  74. try {
  75. text = await navigator.clipboard.readText();
  76. } catch (e) {
  77. return alert('请在 HTTPS 环境并授权剪贴板访问');
  78. }
  79. if (!text.trim()) return alert('剪贴板内容为空');
  80. showEditDialog(text.trim());
  81. }
  82.  
  83. function showEditDialog(rawText) {
  84. const mask = document.createElement('div');
  85. mask.className = 'clip2gist-mask';
  86. const dlg = document.createElement('div');
  87. dlg.className = 'clip2gist-dialog';
  88.  
  89. // 词块化
  90. const wordContainer = document.createElement('div');
  91. rawText.split(/\s+/).forEach(w => {
  92. const sp = document.createElement('span');
  93. sp.className = 'clip2gist-word';
  94. sp.textContent = w;
  95. sp.addEventListener('click', () => {
  96. sp.classList.toggle('selected');
  97. updatePreview();
  98. });
  99. wordContainer.appendChild(sp);
  100. });
  101. dlg.appendChild(wordContainer);
  102.  
  103. // 预览区
  104. const preview = document.createElement('div');
  105. preview.id = 'clip2gist-preview';
  106. dlg.appendChild(preview);
  107.  
  108. // 按钮行
  109. const btnRow = document.createElement('div');
  110. const cancelBtn = document.createElement('button');
  111. cancelBtn.textContent = '取消';
  112. cancelBtn.addEventListener('click', () => document.body.removeChild(mask));
  113. const configBtn = document.createElement('button');
  114. configBtn.textContent = '配置';
  115. configBtn.addEventListener('click', openConfigModal);
  116. const confirmBtn = document.createElement('button');
  117. confirmBtn.textContent = '确认';
  118. confirmBtn.addEventListener('click', onConfirm);
  119. btnRow.append(cancelBtn, configBtn, confirmBtn);
  120. dlg.appendChild(btnRow);
  121.  
  122. mask.appendChild(dlg);
  123. document.body.appendChild(mask);
  124.  
  125. updatePreview();
  126.  
  127. function updatePreview() {
  128. const spans = Array.from(wordContainer.children);
  129. const final = [];
  130. for (let i = 0; i < spans.length;) {
  131. if (spans[i].classList.contains('selected')) {
  132. const group = [spans[i].textContent];
  133. let j = i + 1;
  134. while (j < spans.length && spans[j].classList.contains('selected')) {
  135. group.push(spans[j].textContent);
  136. j++;
  137. }
  138. final.push(`{${group.join(' ')}}`);
  139. i = j;
  140. } else {
  141. final.push(spans[i].textContent);
  142. i++;
  143. }
  144. }
  145. preview.textContent = final.join(' ');
  146. }
  147.  
  148. async function onConfirm() {
  149. const gistId = await GM_getValue('gistId');
  150. const token = await GM_getValue('githubToken');
  151. if (!gistId || !token) {
  152. return alert('请先通过“配置 Gist 参数”填写 Gist ID 与 Token');
  153. }
  154. const content = preview.textContent;
  155. GM_xmlhttpRequest({
  156. method: 'GET',
  157. url: `https://api.github.com/gists/${gistId}`,
  158. headers: { Authorization: `token ${token}` },
  159. onload(resp1) {
  160. if (resp1.status !== 200) return alert('拉取 Gist 失败');
  161. const data = JSON.parse(resp1.responseText);
  162. const fname = Object.keys(data.files)[0];
  163. const old = data.files[fname].content;
  164. const updated = `\n\n----\n${content}` + old;
  165. GM_xmlhttpRequest({
  166. method: 'PATCH',
  167. url: `https://api.github.com/gists/${gistId}`,
  168. headers: {
  169. Authorization: `token ${token}`,
  170. 'Content-Type': 'application/json'
  171. },
  172. data: JSON.stringify({ files: { [fname]: { content: updated } } }),
  173. onload(resp2) {
  174. if (resp2.status === 200) {
  175. alert('上传成功 🎉');
  176. document.body.removeChild(mask);
  177. } else {
  178. alert('上传失败:' + resp2.status);
  179. }
  180. }
  181. });
  182. }
  183. });
  184. }
  185. }
  186.  
  187. function openConfigModal() {
  188. const mask = document.createElement('div');
  189. mask.className = 'clip2gist-mask';
  190. const dlg = document.createElement('div');
  191. dlg.className = 'clip2gist-dialog';
  192.  
  193. const idLabel = document.createElement('label');
  194. idLabel.textContent = 'Gist ID:';
  195. const idInput = document.createElement('input');
  196. idInput.value = GM_getValue('gistId', '');
  197.  
  198. const tkLabel = document.createElement('label');
  199. tkLabel.textContent = 'GitHub Token:';
  200. const tkInput = document.createElement('input');
  201. tkInput.value = GM_getValue('githubToken', '');
  202.  
  203. dlg.append(idLabel, idInput, tkLabel, tkInput);
  204.  
  205. const saveBtn = document.createElement('button');
  206. saveBtn.textContent = '保存';
  207. saveBtn.addEventListener('click', () => {
  208. GM_setValue('gistId', idInput.value.trim());
  209. GM_setValue('githubToken', tkInput.value.trim());
  210. alert('配置已保存');
  211. document.body.removeChild(mask);
  212. });
  213.  
  214. const cancelBtn2 = document.createElement('button');
  215. cancelBtn2.textContent = '取消';
  216. cancelBtn2.addEventListener('click', () => document.body.removeChild(mask));
  217.  
  218. dlg.append(saveBtn, cancelBtn2);
  219. mask.appendChild(dlg);
  220. document.body.appendChild(mask);
  221. }
  222.  
  223. })();

QingJ © 2025

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