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
// @author       Your Name
// @include      *
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @run-at       document-end
// @license      MIT
// ==/UserScript==

;(function() {
  'use strict';

  // —— API 退回实现 —— 

  // 存储
  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(resp =>
            resp.text().then(text => opts.onload({ status: resp.status, responseText: text }))
          );
        } else {
          fetch(opts.url, {
            method: opts.method,
            headers: h,
            body: opts.data
          }).then(resp =>
            resp.text().then(text => opts.onload({ status: resp.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);
    }
  }

  // 菜单命令(Lemur 不支持时不用)
  if (typeof GM_registerMenuCommand === 'function') {
    GM_registerMenuCommand('配置 Gist 参数', openConfigModal);
  }

  // 版本号存储键
  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;
    }
  `);

  // 主流程
  async function mainFlow() {
    let text = '';
    try {
      text = await navigator.clipboard.readText();
    } catch (e) {
      return alert('请在 HTTPS 环境并授权剪贴板访问');
    }
    if (!text.trim()) {
      return alert('剪贴板内容为空');
    }
    showEditDialog(text.trim());
  }

  // 在移动端/桌面延迟插入浮动按钮
  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();

  // 编辑对话框
  function showEditDialog(rawText) {
    const mask = document.createElement('div');
    mask.className = 'clip2gist-mask';
    const dlg = document.createElement('div');
    dlg.className = 'clip2gist-dialog';

    // 词块化
    const container = document.createElement('div');
    rawText.split(/\s+/).forEach(w => {
      const sp = document.createElement('span');
      sp.className = 'clip2gist-word';
      sp.textContent = w;
      sp.addEventListener('click', () => {
        sp.classList.toggle('selected'); updatePreview();
      });
      container.appendChild(sp);
    });
    dlg.appendChild(container);

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

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

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

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

    async function onConfirm() {
      const gistId = getValue('gistId','');
      const token  = getValue('githubToken','');
      if (!gistId || !token) {
        return alert('请先配置 Gist ID 与 GitHub Token');
      }
      const ver     = getValue(VERSION_KEY,1);
      const header  = `版本 ${ver}`;
      const content = preview.textContent;

      // 拉取
      httpRequest({
        method: 'GET',
        url: `https://api.github.com/gists/${gistId}`,
        headers: { Authorization: `token ${token}` },
        onload(resp1) {
          if (resp1.status !== 200) {
            return alert('拉取 Gist 失败:'+resp1.status);
          }
          const data  = JSON.parse(resp1.responseText);
          const fname = Object.keys(data.files)[0];
          const old   = data.files[fname].content;
          const updated = `\n\n----\n${header}\n${content}` + old;
          // 更新
          httpRequest({
            method: 'PATCH',
            url: `https://api.github.com/gists/${gistId}`,
            headers: {
              Authorization: `token ${token}`,
              'Content-Type': 'application/json'
            },
            data: JSON.stringify({ files:{[fname]:{content:updated}} }),
            onload(resp2) {
              if (resp2.status === 200) {
                alert(`上传成功 🎉 已发布版本 ${ver}`);
                setValue(VERSION_KEY, ver+1);
                document.body.removeChild(mask);
              } else {
                alert('上传失败:'+resp2.status);
              }
            }
          });
        }
      });
    }
  }

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

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

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

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

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

    dlg.append(save,cancel);
    mask.appendChild(dlg);
    document.body.appendChild(mask);
  }

})();