Greasy Fork

Clip-to-Gist

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

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

// ==UserScript==
// @name         Clip-to-Gist
// @name:zh-CN   Clip-to-Gist 金句剪贴脚本(v2.3)
// @namespace    https://github.com/yourusername
// @version      2.3
// @description  One-click clipboard quote → GitHub Gist, with keyword highlighting, versioning & Lemur compatibility
// @description:zh-CN 一键剪贴板金句并上传至 GitHub Gist,支持关键词标注、高亮、版本号,并兼容 Lemur Browser
// @include      *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// ==/UserScript==

;(function() {
  'use strict';

  // Storage 回退
  const setValue = typeof GM_setValue === 'function'
    ? GM_setValue
    : (k, v) => localStorage.setItem(k, v);
  const getValue = typeof GM_getValue === 'function'
    ? (k, def) => { const v = GM_getValue(k); return v == null ? def : v; }
    : (k, def) => { const v = localStorage.getItem(k); return v == null ? def : v; };

  // HTTP 回退
  const httpRequest = typeof GM_xmlhttpRequest === 'function'
    ? GM_xmlhttpRequest
    : opts => {
        const h = opts.headers || {};
        if (opts.method === 'GET') {
          fetch(opts.url, { headers: h })
            .then(r => r.text().then(t => opts.onload({ status: r.status, responseText: t })));
        } else {
          fetch(opts.url, { method: opts.method, headers: h, body: opts.data })
            .then(r => r.text().then(t => opts.onload({ status: r.status, responseText: t })));
        }
      };

  // 样式注入回退
  function addStyle(css) {
    if (typeof GM_addStyle === 'function') GM_addStyle(css);
    else document.head.appendChild(Object.assign(document.createElement('style'), { textContent: css }));
  }

  // 版本号管理
  const VERSION_KEY = 'clip2gistVersion';
  if (getValue(VERSION_KEY, null) == null) setValue(VERSION_KEY, 1);

  // 全局样式
  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;
    }
  `);

  // 插入触发按钮(延迟直到 body 就绪)
  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', openConfigModal, false);
    document.body.appendChild(btn);
  }
  insertTrigger();

  // 主逻辑:读剪贴板 → 弹编辑框
  async function mainFlow() {
    let txt = '';
    try { txt = await navigator.clipboard.readText(); }
    catch (e) { return alert('请在 HTTPS 环境并授权剪贴板访问'); }
    if (!txt.trim()) return alert('剪贴板为空');
    showEditor(txt.trim());
  }

  // 编辑/标注框
  function showEditor(text) {
    const mask = document.createElement('div'); mask.className = 'clip2gist-mask';
    const dlg  = document.createElement('div'); dlg.className = 'clip2gist-dialog';

    // 词块化
    const wrap = document.createElement('div');
    text.split(/\s+/).forEach(w => {
      const s = document.createElement('span');
      s.className = 'clip2gist-word'; s.textContent = w;
      s.onclick = () => { s.classList.toggle('selected'); update(); };
      wrap.appendChild(s);
    });
    dlg.appendChild(wrap);

    // 预览区
    const prev = document.createElement('div'); prev.id = 'clip2gist-preview';
    dlg.appendChild(prev);

    // 按钮
    const row = document.createElement('div');
    ['取消','配置','确认'].forEach(l => {
      const b = document.createElement('button'); b.textContent = l;
      if (l==='取消') b.onclick = () => document.body.removeChild(mask);
      if (l==='配置') b.onclick = openConfigModal;
      if (l==='确认') b.onclick = confirmUpload;
      row.appendChild(b);
    });
    dlg.appendChild(row);

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

    // 更新预览
    function update() {
      const spans = Array.from(wrap.children), segs = [];
      for (let i=0; i<spans.length;) {
        if (spans[i].classList.contains('selected')) {
          const grp=[spans[i].textContent], j=i+1;
          while (j<spans.length && spans[j].classList.contains('selected')) {
            grp.push(spans[j].textContent); j++;
          }
          segs.push(`{${grp.join(' ')}}`); i=j;
        } else { segs.push(spans[i].textContent); i++; }
      }
      prev.textContent = segs.join(' ');
    }

    // 上传
    function confirmUpload() {
      const gistId = getValue('gistId','');
      const token  = getValue('githubToken','');
      if (!gistId||!token) return alert('请先配置 Gist ID 与 Token');
      const ver    = getValue(VERSION_KEY,1);
      const hdr    = `版本 ${ver}`;
      const cnt    = prev.textContent;

      // GET
      httpRequest({
        method:'GET', url:`https://api.github.com/gists/${gistId}`,
        headers:{ Authorization:`token ${token}` },
        onload(r1) {
          if (r1.status!==200) return alert('拉取失败:'+r1.status);
          const data = JSON.parse(r1.responseText);
          const fn   = Object.keys(data.files)[0];
          const oldc = data.files[fn].content;
          const upd  = `\n\n----\n${hdr}\n${cnt}`+oldc;

          // PATCH
          httpRequest({
            method:'PATCH', url:`https://api.github.com/gists/${gistId}`,
            headers:{
              Authorization:`token ${token}`,
              'Content-Type':'application/json'
            },
            data:JSON.stringify({ files:{ [fn]:{ content:upd } } }),
            onload(r2) {
              if (r2.status===200) {
                alert(`上传成功 🎉 版本 ${ver}`);
                setValue(VERSION_KEY, ver+1);
                document.body.removeChild(mask);
              } else alert('上传失败:'+r2.status);
            }
          });
        }
      });
    }
  }

  // 配置框
  function openConfigModal() {
    const mask = document.createElement('div'); mask.className='clip2gist-mask';
    const dlg  = document.createElement('div'); dlg.className='clip2gist-dialog';

    const l1 = Object.assign(document.createElement('label'),{ textContent:'Gist ID:' });
    const i1 = Object.assign(document.createElement('input'),{ value:getValue('gistId','') });
    const l2 = Object.assign(document.createElement('label'),{ textContent:'GitHub Token:' });
    const i2 = Object.assign(document.createElement('input'),{ value:getValue('githubToken','') });

    dlg.append(l1,i1,l2,i2);

    const s = document.createElement('button'); s.textContent='保存';
    s.onclick = () => {
      setValue('gistId', i1.value.trim());
      setValue('githubToken', i2.value.trim());
      alert('配置已保存');
      document.body.removeChild(mask);
    };
    const c = document.createElement('button'); c.textContent='取消';
    c.onclick = () => document.body.removeChild(mask);

    dlg.append(s,c);
    mask.appendChild(dlg);
    document.body.appendChild(mask);
  }

})();