Clip-to-Gist

One-click clipboard quote → GitHub Gist, with keyword highlighting, versioning & Lemur compatibility

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

  1. // ==UserScript==
  2. // @name Clip-to-Gist
  3. // @name:zh-CN Clip-to-Gist 金句剪贴脚本(v2.3)
  4. // @namespace https://github.com/yourusername
  5. // @version 2.3
  6. // @description One-click clipboard quote → GitHub Gist, with keyword highlighting, versioning & Lemur compatibility
  7. // @description:zh-CN 一键剪贴板金句并上传至 GitHub Gist,支持关键词标注、高亮、版本号,并兼容 Lemur Browser
  8. // @author Your Name
  9. // @include *
  10. // @match *://*/*
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // @grant GM_xmlhttpRequest
  14. // @grant GM_addStyle
  15. // @grant GM_registerMenuCommand
  16. // @run-at document-end
  17. // @license MIT
  18. // ==/UserScript==
  19.  
  20. ;(function() {
  21. 'use strict';
  22.  
  23. // —— API 退回实现 ——
  24.  
  25. // 存储
  26. const setValue = typeof GM_setValue === 'function'
  27. ? GM_setValue
  28. : (k, v) => localStorage.setItem(k, v);
  29.  
  30. const getValue = typeof GM_getValue === 'function'
  31. ? (k, def) => {
  32. const v = GM_getValue(k);
  33. return v == null ? def : v;
  34. }
  35. : (k, def) => {
  36. const v = localStorage.getItem(k);
  37. return v == null ? def : v;
  38. };
  39.  
  40. // HTTP 请求
  41. const httpRequest = typeof GM_xmlhttpRequest === 'function'
  42. ? GM_xmlhttpRequest
  43. : opts => {
  44. const h = opts.headers || {};
  45. if (opts.method === 'GET') {
  46. fetch(opts.url, { headers: h }).then(resp =>
  47. resp.text().then(text => opts.onload({ status: resp.status, responseText: text }))
  48. );
  49. } else {
  50. fetch(opts.url, {
  51. method: opts.method,
  52. headers: h,
  53. body: opts.data
  54. }).then(resp =>
  55. resp.text().then(text => opts.onload({ status: resp.status, responseText: text }))
  56. );
  57. }
  58. };
  59.  
  60. // 样式注入
  61. function addStyle(css) {
  62. if (typeof GM_addStyle === 'function') {
  63. GM_addStyle(css);
  64. } else {
  65. const s = document.createElement('style');
  66. s.textContent = css;
  67. document.head.appendChild(s);
  68. }
  69. }
  70.  
  71. // 菜单命令(Lemur 不支持时不用)
  72. if (typeof GM_registerMenuCommand === 'function') {
  73. GM_registerMenuCommand('配置 Gist 参数', openConfigModal);
  74. }
  75.  
  76. // 版本号存储键
  77. const VERSION_KEY = 'clip2gistVersion';
  78. if (getValue(VERSION_KEY, null) == null) {
  79. setValue(VERSION_KEY, 1);
  80. }
  81.  
  82. // 全局样式
  83. addStyle(`
  84. #clip2gist-trigger {
  85. position: fixed !important;
  86. bottom: 20px !important;
  87. right: 20px !important;
  88. width: 40px; height: 40px;
  89. line-height: 40px; text-align: center;
  90. background: #4CAF50; color: #fff;
  91. border-radius: 50%; cursor: pointer;
  92. z-index: 2147483647 !important;
  93. font-size: 24px;
  94. box-shadow: 0 2px 6px rgba(0,0,0,0.3);
  95. }
  96. .clip2gist-mask {
  97. position: fixed; inset: 0; background: rgba(0,0,0,0.5);
  98. display: flex; align-items: center; justify-content: center;
  99. z-index: 2147483646;
  100. }
  101. .clip2gist-dialog {
  102. background: #fff; padding: 20px; border-radius: 8px;
  103. max-width: 90%; max-height: 90%; overflow: auto;
  104. box-shadow: 0 2px 10px rgba(0,0,0,0.3);
  105. }
  106. .clip2gist-dialog input {
  107. width: 100%; padding: 6px; margin: 4px 0 12px;
  108. box-sizing: border-box; font-size: 14px;
  109. }
  110. .clip2gist-dialog button {
  111. margin-left: 8px; padding: 6px 12px; font-size: 14px;
  112. cursor: pointer;
  113. }
  114. .clip2gist-word {
  115. display: inline-block; margin: 2px; padding: 4px 6px;
  116. border: 1px solid #ccc; border-radius: 4px; cursor: pointer;
  117. user-select: none;
  118. }
  119. .clip2gist-word.selected {
  120. background: #ffeb3b; border-color: #f1c40f;
  121. }
  122. #clip2gist-preview {
  123. margin-top: 12px; padding: 8px; border: 1px solid #ddd;
  124. min-height: 40px; font-family: monospace;
  125. }
  126. `);
  127.  
  128. // 主流程
  129. async function mainFlow() {
  130. let text = '';
  131. try {
  132. text = await navigator.clipboard.readText();
  133. } catch (e) {
  134. return alert('请在 HTTPS 环境并授权剪贴板访问');
  135. }
  136. if (!text.trim()) {
  137. return alert('剪贴板内容为空');
  138. }
  139. showEditDialog(text.trim());
  140. }
  141.  
  142. // 在移动端/桌面延迟插入浮动按钮
  143. function insertTrigger() {
  144. if (!document.body) {
  145. return setTimeout(insertTrigger, 100);
  146. }
  147. const btn = document.createElement('div');
  148. btn.id = 'clip2gist-trigger';
  149. btn.textContent = '📝';
  150. // 单击→主流程,双击→配置
  151. btn.addEventListener('click', mainFlow, false);
  152. btn.addEventListener('dblclick', openConfigModal, false);
  153. document.body.appendChild(btn);
  154. }
  155. insertTrigger();
  156.  
  157. // 编辑对话框
  158. function showEditDialog(rawText) {
  159. const mask = document.createElement('div');
  160. mask.className = 'clip2gist-mask';
  161. const dlg = document.createElement('div');
  162. dlg.className = 'clip2gist-dialog';
  163.  
  164. // 词块化
  165. const container = document.createElement('div');
  166. rawText.split(/\s+/).forEach(w => {
  167. const sp = document.createElement('span');
  168. sp.className = 'clip2gist-word';
  169. sp.textContent = w;
  170. sp.addEventListener('click', () => {
  171. sp.classList.toggle('selected'); updatePreview();
  172. });
  173. container.appendChild(sp);
  174. });
  175. dlg.appendChild(container);
  176.  
  177. // 预览区
  178. const preview = document.createElement('div');
  179. preview.id = 'clip2gist-preview';
  180. dlg.appendChild(preview);
  181.  
  182. // 按钮
  183. const row = document.createElement('div');
  184. ['取消','配置','确认'].forEach(label => {
  185. const b = document.createElement('button');
  186. b.textContent = label;
  187. if (label==='取消') b.onclick = () => document.body.removeChild(mask);
  188. else if (label==='配置') b.onclick = openConfigModal;
  189. else b.onclick = onConfirm;
  190. row.appendChild(b);
  191. });
  192. dlg.appendChild(row);
  193.  
  194. mask.appendChild(dlg);
  195. document.body.appendChild(mask);
  196. updatePreview();
  197.  
  198. function updatePreview() {
  199. const spans = Array.from(container.children);
  200. const segs = [];
  201. for (let i=0; i<spans.length;) {
  202. if (spans[i].classList.contains('selected')) {
  203. const group=[spans[i].textContent], j=i+1;
  204. while (j<spans.length && spans[j].classList.contains('selected')) {
  205. group.push(spans[j].textContent); j++;
  206. }
  207. segs.push(`{${group.join(' ')}}`);
  208. i=j;
  209. } else {
  210. segs.push(spans[i].textContent); i++;
  211. }
  212. }
  213. preview.textContent = segs.join(' ');
  214. }
  215.  
  216. async function onConfirm() {
  217. const gistId = getValue('gistId','');
  218. const token = getValue('githubToken','');
  219. if (!gistId || !token) {
  220. return alert('请先配置 Gist ID 与 GitHub Token');
  221. }
  222. const ver = getValue(VERSION_KEY,1);
  223. const header = `版本 ${ver}`;
  224. const content = preview.textContent;
  225.  
  226. // 拉取
  227. httpRequest({
  228. method: 'GET',
  229. url: `https://api.github.com/gists/${gistId}`,
  230. headers: { Authorization: `token ${token}` },
  231. onload(resp1) {
  232. if (resp1.status !== 200) {
  233. return alert('拉取 Gist 失败:'+resp1.status);
  234. }
  235. const data = JSON.parse(resp1.responseText);
  236. const fname = Object.keys(data.files)[0];
  237. const old = data.files[fname].content;
  238. const updated = `\n\n----\n${header}\n${content}` + old;
  239. // 更新
  240. httpRequest({
  241. method: 'PATCH',
  242. url: `https://api.github.com/gists/${gistId}`,
  243. headers: {
  244. Authorization: `token ${token}`,
  245. 'Content-Type': 'application/json'
  246. },
  247. data: JSON.stringify({ files:{[fname]:{content:updated}} }),
  248. onload(resp2) {
  249. if (resp2.status === 200) {
  250. alert(`上传成功 🎉 已发布版本 ${ver}`);
  251. setValue(VERSION_KEY, ver+1);
  252. document.body.removeChild(mask);
  253. } else {
  254. alert('上传失败:'+resp2.status);
  255. }
  256. }
  257. });
  258. }
  259. });
  260. }
  261. }
  262.  
  263. // 配置对话框
  264. function openConfigModal() {
  265. const mask = document.createElement('div');
  266. mask.className = 'clip2gist-mask';
  267. const dlg = document.createElement('div');
  268. dlg.className = 'clip2gist-dialog';
  269.  
  270. const l1 = document.createElement('label');
  271. l1.textContent = 'Gist ID:';
  272. const i1 = document.createElement('input');
  273. i1.value = getValue('gistId','');
  274.  
  275. const l2 = document.createElement('label');
  276. l2.textContent = 'GitHub Token:';
  277. const i2 = document.createElement('input');
  278. i2.value = getValue('githubToken','');
  279.  
  280. dlg.append(l1,i1,l2,i2);
  281.  
  282. const save = document.createElement('button');
  283. save.textContent = '保存';
  284. save.onclick = () => {
  285. setValue('gistId', i1.value.trim());
  286. setValue('githubToken', i2.value.trim());
  287. alert('配置已保存');
  288. document.body.removeChild(mask);
  289. };
  290. const cancel = document.createElement('button');
  291. cancel.textContent = '取消';
  292. cancel.onclick = () => document.body.removeChild(mask);
  293.  
  294. dlg.append(save,cancel);
  295. mask.appendChild(dlg);
  296. document.body.appendChild(mask);
  297. }
  298.  
  299. })();

QingJ © 2025

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