Greasy Fork

Clip-to-Gist Quote Script (Lemur Compatible)

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

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

// ==UserScript==
// @name         Clip-to-Gist Quote Script (Lemur Compatible)
// @namespace    http://tampermonkey.net/
// @version      2.3
// @description  One-click clipboard quotes → GitHub Gist, with keyword highlighting, versioning & Lemur Browser compatibility
// @author       Your Name
// @match        *://*/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @connect      api.github.com
// @run-at       document-end
// ==/UserScript==

(function(){
  'use strict';

  // Fallback implementations
  const setValue = typeof GM_setValue === 'function'
    ? GM_setValue
    : (key, val) => localStorage.setItem(key, val);
  const getValue = typeof GM_getValue === 'function'
    ? (key, def) => { const v = GM_getValue(key); return v == null ? def : v; }
    : (key, def) => { const v = localStorage.getItem(key); return v == null ? def : v; };
  const httpRequest = typeof GM_xmlhttpRequest === 'function'
    ? GM_xmlhttpRequest
    : opts => {
        const headers = opts.headers || {};
        if (opts.method === 'GET') {
          fetch(opts.url, { headers })
            .then(res => res.text().then(text => opts.onload({ status: res.status, responseText: text })));
        } else {
          fetch(opts.url, { method: opts.method, headers, body: opts.data })
            .then(res => res.text().then(text => opts.onload({ status: res.status, responseText: text })));
        }
      };
  function addStyle(css) {
    if (typeof GM_addStyle === 'function') {
      GM_addStyle(css);
    } else {
      const s = document.createElement('style');
      s.textContent = css;
      document.head.appendChild(s);
    }
  }

  // Version management
  const VERSION_KEY = 'clip2gistVersion';
  if (getValue(VERSION_KEY, null) == null) {
    setValue(VERSION_KEY, 1);
  }

  // Global CSS
  addStyle(`
    #clip2gist-trigger { position:fixed!important;bottom:20px!important;right:20px!important;
      width:40px;height:40px;line-height:40px;text-align:center;
      background:#4CAF50;color:#fff;border-radius:50%;cursor:pointer;
      z-index:2147483647!important;font-size:24px;box-shadow:0 2px 6px rgba(0,0,0,0.3);
    }
    .clip2gist-mask { position:fixed;inset:0;background:rgba(0,0,0,0.5);
      display:flex;align-items:center;justify-content:center;z-index:2147483646;
    }
    .clip2gist-dialog { background:#fff;padding:20px;border-radius:8px;
      max-width:90%;max-height:90%;overflow:auto;box-shadow:0 2px 10px rgba(0,0,0,0.3);
    }
    .clip2gist-dialog input { width:100%;padding:6px;margin:4px 0 12px;box-sizing:border-box;font-size:14px; }
    .clip2gist-dialog button { margin-left:8px;padding:6px 12px;font-size:14px;cursor:pointer; }
    .clip2gist-word { display:inline-block;margin:2px;padding:4px 6px;
      border:1px solid #ccc;border-radius:4px;cursor:pointer;user-select:none;
    }
    .clip2gist-word.selected { background:#ffeb3b;border-color:#f1c40f; }
    #clip2gist-preview { margin-top:12px;padding:8px;border:1px solid #ddd;
      min-height:40px;font-family:monospace;
    }
  `);

  // If supported, register menu command
  if (typeof GM_registerMenuCommand === 'function') {
    GM_registerMenuCommand('Configure Gist', openConfigDialog);
  }

  // Insert floating trigger button
  function insertTrigger() {
    if (!document.body) {
      return setTimeout(insertTrigger, 100);
    }
    const btn = document.createElement('div');
    btn.id = 'clip2gist-trigger';
    btn.textContent = '📝';
    btn.addEventListener('click', mainFlow, false);
    btn.addEventListener('dblclick', openConfigDialog, false);
    document.body.appendChild(btn);
  }
  insertTrigger();

  // Main flow: read clipboard, show editor
  async function mainFlow() {
    let text = '';
    try {
      text = await navigator.clipboard.readText();
    } catch (e) {
      return alert('Please use HTTPS and allow clipboard access');
    }
    if (!text.trim()) {
      return alert('Clipboard is empty');
    }
    showEditor(text.trim());
  }

  // Editor dialog
  function showEditor(rawText) {
    const mask = document.createElement('div'); mask.className = 'clip2gist-mask';
    const dlg  = document.createElement('div'); dlg.className = 'clip2gist-dialog';

    // Word-by-word spans
    const container = document.createElement('div');
    rawText.split(/\s+/).forEach(word => {
      const span = document.createElement('span');
      span.className = 'clip2gist-word';
      span.textContent = word;
      span.onclick = () => { span.classList.toggle('selected'); updatePreview(); };
      container.appendChild(span);
    });
    dlg.appendChild(container);

    // Preview area
    const preview = document.createElement('div');
    preview.id = 'clip2gist-preview';
    dlg.appendChild(preview);

    // Buttons row
    const row = document.createElement('div');
    ['Cancel', 'Configure', 'Confirm'].forEach(label => {
      const btn = document.createElement('button');
      btn.textContent = label;
      if (label === 'Cancel')     btn.onclick = () => document.body.removeChild(mask);
      if (label === 'Configure')  btn.onclick = openConfigDialog;
      if (label === 'Confirm')    btn.onclick = confirmUpload;
      row.appendChild(btn);
    });
    dlg.appendChild(row);

    mask.appendChild(dlg);
    document.body.appendChild(mask);
    updatePreview();

    // Build preview string
    function updatePreview() {
      const spans = Array.from(container.children);
      const parts = [];
      for (let i = 0; i < spans.length;) {
        if (spans[i].classList.contains('selected')) {
          const group = [spans[i].textContent];
          let j = i + 1;
          while (j < spans.length && spans[j].classList.contains('selected')) {
            group.push(spans[j].textContent);
            j++;
          }
          parts.push(`{${group.join(' ')}}`);
          i = j;
        } else {
          parts.push(spans[i].textContent);
          i++;
        }
      }
      preview.textContent = parts.join(' ');
    }

    // Confirm upload
    function confirmUpload() {
      const gistId = getValue('gistId', '');
      const token  = getValue('githubToken', '');
      if (!gistId || !token) {
        return alert('Please configure Gist ID and GitHub Token first');
      }
      const ver      = getValue(VERSION_KEY, 1);
      const header   = `Version ${ver}`;
      const content  = preview.textContent;

      // GET existing gist
      httpRequest({
        method: 'GET',
        url: `https://api.github.com/gists/${gistId}`,
        headers: { Authorization: `token ${token}` },
        onload(res1) {
          if (res1.status !== 200) {
            return alert('Failed to fetch Gist: ' + res1.status);
          }
          const data = JSON.parse(res1.responseText);
          const file = Object.keys(data.files)[0];
          const oldContent = data.files[file].content;
          const updated    = `\n\n----\n${header}\n${content}` + oldContent;

          // PATCH update
          httpRequest({
            method: 'PATCH',
            url: `https://api.github.com/gists/${gistId}`,
            headers: {
              Authorization: `token ${token}`,
              'Content-Type': 'application/json'
            },
            data: JSON.stringify({ files: { [file]: { content: updated } } }),
            onload(res2) {
              if (res2.status === 200) {
                alert(`Upload successful! Version ${ver}`);
                setValue(VERSION_KEY, ver + 1);
                document.body.removeChild(mask);
              } else {
                alert('Failed to update Gist: ' + res2.status);
              }
            }
          });
        }
      });
    }
  }

  // Configuration dialog
  function openConfigDialog() {
    const mask = document.createElement('div'); mask.className = 'clip2gist-mask';
    const dlg  = document.createElement('div'); dlg.className = 'clip2gist-dialog';

    const label1 = document.createElement('label');
    label1.textContent = 'Gist ID:';
    const input1 = document.createElement('input');
    input1.value = getValue('gistId', '');

    const label2 = document.createElement('label');
    label2.textContent = 'GitHub Token:';
    const input2 = document.createElement('input');
    input2.value = getValue('githubToken', '');

    dlg.append(label1, input1, label2, input2);

    const saveBtn = document.createElement('button');
    saveBtn.textContent = 'Save';
    saveBtn.onclick = () => {
      setValue('gistId', input1.value.trim());
      setValue('githubToken', input2.value.trim());
      alert('Configuration saved');
      document.body.removeChild(mask);
    };

    const cancelBtn = document.createElement('button');
    cancelBtn.textContent = 'Cancel';
    cancelBtn.onclick = () => document.body.removeChild(mask);

    dlg.append(saveBtn, cancelBtn);
    mask.appendChild(dlg);
    document.body.appendChild(mask);
  }
})();