Clip-to-Gist Quote Script (ES5, Lemur Compatible)

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

  1. // ==UserScript==
  2. // @name Clip-to-Gist Quote Script (ES5, Lemur Compatible)
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.3.1
  5. // @description One-click clipboard quotes → GitHub Gist, with keyword highlighting, versioning & Lemur Browser compatibility
  6. // @author Your Name
  7. // @include *://*/*
  8. // @run-at document-end
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @grant GM_xmlhttpRequest
  12. // @connect api.github.com
  13. // ==/UserScript==
  14.  
  15. (function(){
  16. 'use strict';
  17.  
  18. // --- Fallback wrappers ---
  19. var setValue = (typeof GM_setValue === 'function') ?
  20. GM_setValue :
  21. function(k, v){ localStorage.setItem(k, v); };
  22.  
  23. var getValue = (typeof GM_getValue === 'function') ?
  24. function(k, def){ var v = GM_getValue(k); return (v===undefined||v===null)?def:v; } :
  25. function(k, def){ var v = localStorage.getItem(k); return (v===undefined||v===null)?def:v; };
  26.  
  27. var httpRequest = (typeof GM_xmlhttpRequest === 'function') ?
  28. GM_xmlhttpRequest :
  29. function(opts){
  30. var headers = opts.headers || {};
  31. if(opts.method === 'GET'){
  32. fetch(opts.url, { headers: headers })
  33. .then(function(res){ return res.text().then(function(txt){
  34. opts.onload({ status: res.status, responseText: txt });
  35. }); });
  36. } else {
  37. fetch(opts.url, { method: opts.method, headers: headers, body: opts.data })
  38. .then(function(res){ return res.text().then(function(txt){
  39. opts.onload({ status: res.status, responseText: txt });
  40. }); });
  41. }
  42. };
  43.  
  44. // --- Version key init ---
  45. var VERSION_KEY = 'clip2gistVersion';
  46. if(getValue(VERSION_KEY, null) === null){
  47. setValue(VERSION_KEY, 1);
  48. }
  49.  
  50. // --- Inject global CSS via <style> ---
  51. function addGlobalStyle(css){
  52. var head = document.getElementsByTagName('head')[0];
  53. if(!head){ return; }
  54. var style = document.createElement('style');
  55. style.type = 'text/css';
  56. style.textContent = css;
  57. head.appendChild(style);
  58. }
  59. addGlobalStyle(
  60. '#clip2gist-trigger{' +
  61. 'position:fixed!important;bottom:20px!important;right:20px!important;' +
  62. 'width:40px;height:40px;line-height:40px;text-align:center;' +
  63. 'background:#4CAF50;color:#fff;border-radius:50%;cursor:pointer;' +
  64. 'z-index:2147483647!important;font-size:24px;box-shadow:0 2px 6px rgba(0,0,0,0.3);' +
  65. '}' +
  66. '.clip2gist-mask{' +
  67. 'position:fixed;top:0;left:0;right:0;bottom:0;' +
  68. 'background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;' +
  69. 'z-index:2147483646;' +
  70. '}' +
  71. '.clip2gist-dialog{' +
  72. 'background:#fff;padding:20px;border-radius:8px;' +
  73. 'max-width:90%;max-height:90%;overflow:auto;' +
  74. 'box-shadow:0 2px 10px rgba(0,0,0,0.3);' +
  75. '}' +
  76. '.clip2gist-dialog input{' +
  77. 'width:100%;padding:6px;margin:4px 0 12px;box-sizing:border-box;font-size:14px;' +
  78. '}' +
  79. '.clip2gist-dialog button{' +
  80. 'margin-left:8px;padding:6px 12px;font-size:14px;cursor:pointer;' +
  81. '}' +
  82. '.clip2gist-word{' +
  83. 'display:inline-block;margin:2px;padding:4px 6px;border:1px solid #ccc;' +
  84. 'border-radius:4px;cursor:pointer;user-select:none;' +
  85. '}' +
  86. '.clip2gist-word.selected{' +
  87. 'background:#ffeb3b;border-color:#f1c40f;' +
  88. '}' +
  89. '#clip2gist-preview{' +
  90. 'margin-top:12px;padding:8px;border:1px solid #ddd;' +
  91. 'min-height:40px;font-family:monospace;' +
  92. '}'
  93. );
  94.  
  95. // --- Insert floating trigger button ---
  96. function insertTrigger(){
  97. if(!document.body){
  98. return setTimeout(insertTrigger, 100);
  99. }
  100. var btn = document.createElement('div');
  101. btn.id = 'clip2gist-trigger';
  102. btn.textContent = '📝';
  103. btn.addEventListener('click', mainFlow, false);
  104. btn.addEventListener('dblclick', openConfigDialog, false);
  105. document.body.appendChild(btn);
  106. }
  107. insertTrigger();
  108.  
  109. // --- Main: read from clipboard and pop editor ---
  110. function mainFlow(){
  111. navigator.clipboard && navigator.clipboard.readText
  112. ? navigator.clipboard.readText().then(function(txt){
  113. if(!txt.trim()){
  114. alert('Clipboard is empty');
  115. } else {
  116. showEditor(txt.trim());
  117. }
  118. }, function(){
  119. alert('Please use HTTPS and allow clipboard access');
  120. })
  121. : alert('Clipboard API not supported');
  122. }
  123.  
  124. // --- Show editor dialog ---
  125. function showEditor(rawText){
  126. var mask = document.createElement('div');
  127. mask.className = 'clip2gist-mask';
  128. var dlg = document.createElement('div');
  129. dlg.className = 'clip2gist-dialog';
  130.  
  131. // word spans
  132. var wrap = document.createElement('div');
  133. var words = rawText.split(/\s+/);
  134. for(var i=0;i<words.length;i++){
  135. var sp = document.createElement('span');
  136. sp.className = 'clip2gist-word';
  137. sp.textContent = words[i];
  138. sp.onclick = (function(el){
  139. return function(){
  140. el.classList.toggle('selected');
  141. updatePreview();
  142. };
  143. })(sp);
  144. wrap.appendChild(sp);
  145. }
  146. dlg.appendChild(wrap);
  147.  
  148. // preview
  149. var prev = document.createElement('div');
  150. prev.id = 'clip2gist-preview';
  151. dlg.appendChild(prev);
  152.  
  153. // buttons
  154. var row = document.createElement('div');
  155. ['Cancel','Configure','Confirm'].forEach(function(label){
  156. var b = document.createElement('button');
  157. b.textContent = label;
  158. if(label==='Cancel'){
  159. b.onclick = function(){ document.body.removeChild(mask); };
  160. } else if(label==='Configure'){
  161. b.onclick = openConfigDialog;
  162. } else {
  163. b.onclick = confirmUpload;
  164. }
  165. row.appendChild(b);
  166. });
  167. dlg.appendChild(row);
  168.  
  169. mask.appendChild(dlg);
  170. document.body.appendChild(mask);
  171. updatePreview();
  172.  
  173. // update preview text
  174. function updatePreview(){
  175. var spans = wrap.children;
  176. var parts = [];
  177. for(var j=0;j<spans.length;){
  178. if(spans[j].classList.contains('selected')){
  179. var grp = [spans[j].textContent], k = j+1;
  180. while(k<spans.length && spans[k].classList.contains('selected')){
  181. grp.push(spans[k].textContent);
  182. k++;
  183. }
  184. parts.push('{' + grp.join(' ') + '}');
  185. j = k;
  186. } else {
  187. parts.push(spans[j].textContent);
  188. j++;
  189. }
  190. }
  191. prev.textContent = parts.join(' ');
  192. }
  193.  
  194. // upload to Gist
  195. function confirmUpload(){
  196. var gistId = getValue('gistId','');
  197. var token = getValue('githubToken','');
  198. if(!gistId || !token){
  199. alert('Please configure Gist ID and GitHub Token first');
  200. return;
  201. }
  202. var ver = getValue(VERSION_KEY,1);
  203. var header = 'Version ' + ver;
  204. var content = prev.textContent;
  205.  
  206. // GET existing
  207. httpRequest({
  208. method: 'GET',
  209. url: 'https://api.github.com/gists/' + gistId,
  210. headers: { 'Authorization': 'token ' + token },
  211. onload: function(res1){
  212. if(res1.status !== 200){
  213. alert('Failed to fetch Gist: ' + res1.status);
  214. return;
  215. }
  216. var data = JSON.parse(res1.responseText);
  217. var file = Object.keys(data.files)[0];
  218. var oldc = data.files[file].content;
  219. var upd = '\n\n----\n' + header + '\n' + content + oldc;
  220.  
  221. // PATCH update
  222. httpRequest({
  223. method: 'PATCH',
  224. url: 'https://api.github.com/gists/' + gistId,
  225. headers: {
  226. 'Authorization': 'token ' + token,
  227. 'Content-Type': 'application/json'
  228. },
  229. data: JSON.stringify({ files: (function(){ var o={}; o[file]={content:upd}; return o; })() }),
  230. onload: function(res2){
  231. if(res2.status === 200){
  232. alert('Upload successful! Version ' + ver);
  233. setValue(VERSION_KEY, ver+1);
  234. document.body.removeChild(mask);
  235. } else {
  236. alert('Failed to update Gist: ' + res2.status);
  237. }
  238. }
  239. });
  240. }
  241. });
  242. }
  243. }
  244.  
  245. // --- Configuration dialog ---
  246. function openConfigDialog(){
  247. var mask = document.createElement('div');
  248. mask.className = 'clip2gist-mask';
  249. var dlg = document.createElement('div');
  250. dlg.className = 'clip2gist-dialog';
  251.  
  252. var label1 = document.createElement('label');
  253. label1.textContent = 'Gist ID:';
  254. var input1 = document.createElement('input');
  255. input1.value = getValue('gistId','');
  256.  
  257. var label2 = document.createElement('label');
  258. label2.textContent = 'GitHub Token:';
  259. var input2 = document.createElement('input');
  260. input2.value = getValue('githubToken','');
  261.  
  262. dlg.appendChild(label1);
  263. dlg.appendChild(input1);
  264. dlg.appendChild(label2);
  265. dlg.appendChild(input2);
  266.  
  267. var save = document.createElement('button');
  268. save.textContent = 'Save';
  269. save.onclick = function(){
  270. setValue('gistId', input1.value.trim());
  271. setValue('githubToken', input2.value.trim());
  272. alert('Configuration saved');
  273. document.body.removeChild(mask);
  274. };
  275. var cancel = document.createElement('button');
  276. cancel.textContent = 'Cancel';
  277. cancel.onclick = function(){
  278. document.body.removeChild(mask);
  279. };
  280.  
  281. dlg.appendChild(save);
  282. dlg.appendChild(cancel);
  283. mask.appendChild(dlg);
  284. document.body.appendChild(mask);
  285. }
  286.  
  287. })();