// ==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);
}
})();