Clip to Gist

一键剪贴板金句并上传至 GitHub Gist,支持关键词标注和高亮

目前為 2025-05-03 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Clip to Gist 
// @namespace    https://github.com/yourusername   // ← 换成你的脚本主页或用户名
// @version      2.0
// @description  一键剪贴板金句并上传至 GitHub Gist,支持关键词标注和高亮
// @author       Your Name                        // ← 填写你的名字或昵称
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @license      MIT
// ==/UserScript==

(function() {
  'use strict';

  // 注册菜单:配置 Gist 参数
  GM_registerMenuCommand('配置 Gist 参数', openConfigModal);

  // 注入右下角触发按钮
  const trigger = document.createElement('div');
  trigger.id = 'clip2gist-trigger';
  trigger.textContent = '📝';
  document.body.appendChild(trigger);

  // 样式
  GM_addStyle(`
    #clip2gist-trigger {
      position: fixed; bottom: 20px; right: 20px;
      width: 40px; height: 40px; line-height: 40px;
      background: #4CAF50; color: #fff; text-align:center;
      border-radius: 50%; cursor: pointer; z-index: 9999;
      font-size: 24px;
    }
    .clip2gist-mask {
      position: fixed; inset: 0; background: rgba(0,0,0,0.5);
      display:flex; align-items:center; justify-content:center;
      z-index: 10000;
    }
    .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-top:4px; margin-bottom:12px;
      box-sizing:border-box; font-size:14px;
    }
    .clip2gist-dialog button {
      margin-left:8px; padding:6px 12px; font-size:14px;
    }
    .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;
    }
  `);

  trigger.addEventListener('click', mainFlow);

  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 showEditDialog(rawText) {
    const mask = document.createElement('div');
    mask.className = 'clip2gist-mask';
    const dlg = document.createElement('div');
    dlg.className = 'clip2gist-dialog';

    // 词块化
    const wordContainer = 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();
      });
      wordContainer.appendChild(sp);
    });
    dlg.appendChild(wordContainer);

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

    // 按钮行
    const btnRow = document.createElement('div');
    const cancelBtn = document.createElement('button');
    cancelBtn.textContent = '取消';
    cancelBtn.addEventListener('click', () => document.body.removeChild(mask));
    const configBtn = document.createElement('button');
    configBtn.textContent = '配置';
    configBtn.addEventListener('click', openConfigModal);
    const confirmBtn = document.createElement('button');
    confirmBtn.textContent = '确认';
    confirmBtn.addEventListener('click', onConfirm);
    btnRow.append(cancelBtn, configBtn, confirmBtn);
    dlg.appendChild(btnRow);

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

    updatePreview();

    function updatePreview() {
      const spans = Array.from(wordContainer.children);
      const final = [];
      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++;
          }
          final.push(`{${group.join(' ')}}`);
          i = j;
        } else {
          final.push(spans[i].textContent);
          i++;
        }
      }
      preview.textContent = final.join(' ');
    }

    async function onConfirm() {
      const gistId = await GM_getValue('gistId');
      const token  = await GM_getValue('githubToken');
      if (!gistId || !token) {
        return alert('请先通过“配置 Gist 参数”填写 Gist ID 与 Token');
      }
      const content = preview.textContent;
      // 拉取并更新 Gist
      GM_xmlhttpRequest({
        method: 'GET',
        url: `https://api.github.com/gists/${gistId}`,
        headers: { Authorization: `token ${token}` },
        onload(resp1) {
          if (resp1.status !== 200) return alert('拉取 Gist 失败');
          const data = JSON.parse(resp1.responseText);
          const fname = Object.keys(data.files)[0];
          const old   = data.files[fname].content;
          const updated = `\n\n----\n${content}` + old;
          GM_xmlhttpRequest({
            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('上传成功 🎉');
                document.body.removeChild(mask);
              } else {
                alert('上传失败:' + resp2.status);
              }
            }
          });
        }
      });
    }
  }

  // 一次性配置 Gist ID & Token
  function openConfigModal() {
    const mask = document.createElement('div');
    mask.className = 'clip2gist-mask';
    const dlg  = document.createElement('div');
    dlg.className = 'clip2gist-dialog';

    const idLabel = document.createElement('label');
    idLabel.textContent = 'Gist ID:';
    const idInput = document.createElement('input');
    idInput.value = GM_getValue('gistId', '');

    const tkLabel = document.createElement('label');
    tkLabel.textContent = 'GitHub Token:';
    const tkInput = document.createElement('input');
    tkInput.value = GM_getValue('githubToken', '');

    dlg.append(idLabel, idInput, tkLabel, tkInput);

    const save = document.createElement('button');
    save.textContent = '保存';
    save.addEventListener('click', () => {
      GM_setValue('gistId', idInput.value.trim());
      GM_setValue('githubToken', tkInput.value.trim());
      alert('配置已保存');
      document.body.removeChild(mask);
    });
    const cancel = document.createElement('button');
    cancel.textContent = '取消';
    cancel.addEventListener('click', () => document.body.removeChild(mask));
    dlg.append(save, cancel);

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

})();