Greasy Fork is available in English.
按Alt键复制鼠标下链接,按B键复制B站视频,悬浮面板可移动/折叠/缩放,支持勾选链接,一键导出TXT、批量打开、导入书签
// ==UserScript==
// @name Alt0501增强版链接收集器 · 可多选/批量打开/导入书签/可调整大小
// @version 3.0
// @description 按Alt键复制鼠标下链接,按B键复制B站视频,悬浮面板可移动/折叠/缩放,支持勾选链接,一键导出TXT、批量打开、导入书签
// @author You
// @match *://*/*
// @grant GM_setClipboard
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant window.open
// @run-at document-end
// @namespace http://greasyfork.icu/users/702964
// ==/UserScript==
(function() {
'use strict';
// ---------- 存储结构 ----------
let linksData = []; // 每个元素 { url, selected }
let mouseX = 0, mouseY = 0;
let isProcessing = false;
// 加载已保存的链接
const saved = GM_getValue('links_data', '[]');
try {
linksData = JSON.parse(saved);
if (!Array.isArray(linksData)) linksData = [];
// 确保每个对象有 selected 字段
linksData = linksData.map(item => ({ url: item.url, selected: item.selected === true }));
} catch(e) { linksData = []; }
// ---------- 辅助函数 ----------
function saveLinks() {
GM_setValue('links_data', JSON.stringify(linksData));
}
function renderList() {
// 重新渲染带复选框的列表
listWrap.innerHTML = '';
linksData.forEach((item, idx) => {
const row = document.createElement('div');
row.style.cssText = 'display: flex; align-items: center; padding: 6px 0; border-bottom: 1px solid #eee; gap: 6px;';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.checked = item.selected;
cb.style.margin = '0';
cb.onchange = (e) => {
item.selected = e.target.checked;
saveLinks();
updateSelectedCount();
};
const linkSpan = document.createElement('span');
linkSpan.style.cssText = 'flex:1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer;';
linkSpan.title = item.url;
linkSpan.textContent = item.url;
linkSpan.onclick = () => {
// 单击链接可将它复制到剪贴板
GM_setClipboard(item.url);
showToast('📋 已复制: ' + item.url);
};
const delBtn = document.createElement('span');
delBtn.textContent = '🗑️';
delBtn.style.cursor = 'pointer';
delBtn.style.fontSize = '14px';
delBtn.onclick = (e) => {
e.stopPropagation();
linksData.splice(idx, 1);
saveLinks();
renderList();
updateSelectedCount();
showToast('已删除');
};
row.appendChild(cb);
row.appendChild(linkSpan);
row.appendChild(delBtn);
listWrap.appendChild(row);
});
updateSelectedCount();
// 如果没有链接,显示提示
if (linksData.length === 0) {
const emptyDiv = document.createElement('div');
emptyDiv.textContent = '暂无链接,按 Alt 键或 B 键收集';
emptyDiv.style.padding = '20px';
emptyDiv.style.textAlign = 'center';
emptyDiv.style.color = '#999';
listWrap.appendChild(emptyDiv);
}
}
function updateSelectedCount() {
const count = linksData.filter(item => item.selected).length;
if (count > 0) {
dragBarTitle.innerHTML = `快捷链接收集 (${count}/${linksData.length})`;
} else {
dragBarTitle.innerHTML = `快捷链接收集 (${linksData.length})`;
}
}
// 添加新链接
function addLink(url) {
if (!url || !url.startsWith('http')) return false;
// 去重(基于 url)
const exists = linksData.some(item => item.url === url);
if (!exists) {
linksData.unshift({ url: url, selected: false });
saveLinks();
renderList();
showToast(`➕ 已添加: ${url}`);
return true;
} else {
showToast(`⚠️ 链接已存在: ${url}`);
return false;
}
}
// 获取鼠标下方的链接
function getLinkUnderCursor() {
const elements = document.elementsFromPoint(mouseX, mouseY);
for (const el of elements) {
const a = el.closest('a[href]');
if (a && a.href) {
const href = a.href.trim();
if (href.startsWith('http')) return href;
}
}
return null;
}
// Alt 键复制鼠标下链接
function altCopyLink() {
if (isProcessing) return;
isProcessing = true;
let link = getLinkUnderCursor();
if (!link) {
// 如果没有链接,则复制当前页面地址
link = window.location.href;
}
if (link) {
GM_setClipboard(link);
addLink(link);
showToast(`📋 已复制并添加: ${link}`);
} else {
showToast('❌ 未找到链接', true);
}
setTimeout(() => { isProcessing = false; }, 300);
}
// B 键复制 B站视频链接
const BV_REG = /(?:video\/|bvid=)(BV[a-zA-Z0-9]+)/i;
async function biliCopy() {
if (isProcessing) return;
isProcessing = true;
let bv = null;
const url = location.href;
let m = url.match(BV_REG);
if (m) bv = m[1];
if (!bv) {
const elements = document.elementsFromPoint(mouseX, mouseY);
for (const el of elements) {
const a = el.closest('a[href*="video"]');
if (a && a.href) {
m = a.href.match(BV_REG);
if (m) { bv = m[1]; break; }
}
}
}
if (bv) {
const link = `https://www.bilibili.com/video/${bv}`;
GM_setClipboard(link);
addLink(link);
showToast('🎬 B站链接已复制并添加');
} else {
showToast('❌ 未找到B站视频', true);
}
setTimeout(() => { isProcessing = false; }, 300);
}
// ---------- 批量操作 ----------
function getSelectedUrls() {
return linksData.filter(item => item.selected).map(item => item.url);
}
function batchOpen() {
const urls = getSelectedUrls();
if (urls.length === 0) {
showToast('请至少勾选一个链接', true);
return;
}
for (let url of urls) {
window.open(url, '_blank');
}
showToast(`🚀 已打开 ${urls.length} 个链接(注意浏览器可能拦截弹窗,请允许弹出窗口)`);
}
function batchExportTxt() {
const urls = getSelectedUrls();
if (urls.length === 0) {
showToast('请至少勾选一个链接', true);
return;
}
const txt = urls.join('\n');
const blob = new Blob([txt], { type: 'text/plain' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `links_${Date.now()}.txt`;
a.click();
URL.revokeObjectURL(a.href);
showToast(`📄 已导出 ${urls.length} 条链接`);
}
// 导入书签(需要书签 API 权限,普通网页无法直接添加书签,只能通过 bookmark API,但油猴无法直接调用浏览器书签 API)
// 替代方案:生成一个 HTML 书签文件供用户导入,或者引导用户手动添加。
// 更优雅:生成一个 data: 文本,提示用户保存为 .html 然后导入浏览器书签管理器。
function batchImportBookmarks() {
const urls = getSelectedUrls();
if (urls.length === 0) {
showToast('请至少勾选一个链接', true);
return;
}
// 生成 Netscape 书签格式
let html = '<!DOCTYPE NETSCAPE-Bookmark-file-1>\n';
html += '<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">\n';
html += '<Title>Bookmarks</Title>\n<h1>Bookmarks</h1>\n<dl><p>\n';
for (let url of urls) {
let title = url.replace(/^https?:\/\//, '').substring(0, 50);
html += `<dt><a href="${url}" add_date="${Math.floor(Date.now()/1000)}">${title}</a></dt>\n`;
}
html += '</dl><p>';
const blob = new Blob([html], { type: 'text/html' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `bookmarks_${Date.now()}.html`;
a.click();
URL.revokeObjectURL(a.href);
showToast(`📑 已生成书签文件,请手动导入浏览器书签管理器(通常导入HTML)`);
}
// ---------- 创建可调整大小的悬浮面板 ----------
const container = document.createElement('div');
container.style.cssText = `
position: fixed;
z-index: 999999;
width: 320px;
min-width: 200px;
background: #fff;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.25);
font-size: 12px;
font-family: system-ui, sans-serif;
overflow: hidden;
`;
const savedWidth = GM_getValue('panel_width', 320);
const savedHeight = GM_getValue('panel_height', null);
container.style.width = savedWidth + 'px';
if (savedHeight) container.style.height = savedHeight + 'px';
const dragBar = document.createElement('div');
dragBar.style.cssText = `
background: #fb7299;
color: white;
padding: 8px 12px;
font-weight: bold;
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
`;
const dragBarTitle = document.createElement('span');
dragBarTitle.textContent = '快捷链接收集';
const foldBtn = document.createElement('span');
foldBtn.textContent = '−';
foldBtn.style.cursor = 'pointer';
foldBtn.style.fontSize = '18px';
foldBtn.style.marginLeft = '8px';
dragBar.appendChild(dragBarTitle);
dragBar.appendChild(foldBtn);
const content = document.createElement('div');
content.style.padding = '10px';
content.style.overflow = 'auto';
// 如果保存了高度,内容区域高度需要减去标题栏
if (savedHeight) {
content.style.maxHeight = (savedHeight - 40) + 'px';
} else {
content.style.maxHeight = '300px';
}
const listWrap = document.createElement('div');
listWrap.style.maxHeight = '200px';
listWrap.style.overflowY = 'auto';
listWrap.style.marginBottom = '8px';
const btnGroup = document.createElement('div');
btnGroup.style.display = 'flex';
btnGroup.style.flexWrap = 'wrap';
btnGroup.style.gap = '6px';
btnGroup.style.marginTop = '8px';
const openBtn = document.createElement('button');
openBtn.textContent = '后台打开';
openBtn.style.cssText = 'flex:1; padding:6px; background:#ff9800; color:#fff; border:none; border-radius:4px; cursor:pointer;';
const exportBtn = document.createElement('button');
exportBtn.textContent = '导出TXT';
exportBtn.style.cssText = 'flex:1; padding:6px; background:#2196f3; color:#fff; border:none; border-radius:4px; cursor:pointer;';
const bookmarkBtn = document.createElement('button');
bookmarkBtn.textContent = '导入书签';
bookmarkBtn.style.cssText = 'flex:1; padding:6px; background:#4caf50; color:#fff; border:none; border-radius:4px; cursor:pointer;';
const clearBtn = document.createElement('button');
clearBtn.textContent = '清空';
clearBtn.style.cssText = 'flex:1; padding:6px; background:#f44336; color:#fff; border:none; border-radius:4px; cursor:pointer;';
const copyAllBtn = document.createElement('button');
copyAllBtn.textContent = '复制全部';
copyAllBtn.style.cssText = 'flex:1; padding:6px; background:#9c27b0; color:#fff; border:none; border-radius:4px; cursor:pointer;';
btnGroup.append(openBtn, exportBtn, bookmarkBtn, clearBtn, copyAllBtn);
content.append(listWrap, btnGroup);
container.append(dragBar, content);
document.body.appendChild(container);
// 添加调整大小手柄(右下角)
const resizeHandle = document.createElement('div');
resizeHandle.style.cssText = `
position: absolute;
bottom: 0;
right: 0;
width: 15px;
height: 15px;
cursor: nw-resize;
background: linear-gradient(135deg, transparent 50%, #aaa 50%);
border-bottom-right-radius: 10px;
z-index: 1000000;
`;
container.appendChild(resizeHandle);
// 恢复位置
const savedTop = GM_getValue('panel_top', '100px');
const savedLeft = GM_getValue('panel_left', '20px');
container.style.top = savedTop;
container.style.left = savedLeft;
// 折叠状态
const isFolded = GM_getValue('panel_folded', false);
if (isFolded) {
content.style.display = 'none';
foldBtn.textContent = '+';
}
// 折叠事件
foldBtn.onclick = () => {
if (content.style.display === 'none') {
content.style.display = 'block';
foldBtn.textContent = '−';
GM_setValue('panel_folded', false);
} else {
content.style.display = 'none';
foldBtn.textContent = '+';
GM_setValue('panel_folded', true);
}
};
// 拖拽移动(复用原逻辑)
function makeDraggable(el, handle, onMove) {
let pos = { x: 0, y: 0, mx: 0, my: 0 };
handle.onmousedown = e => {
if (e.target === resizeHandle) return;
e.preventDefault();
pos.mx = e.clientX;
pos.my = e.clientY;
document.onmousemove = move;
document.onmouseup = up;
};
function move(e) {
pos.x = pos.mx - e.clientX;
pos.y = pos.my - e.clientY;
pos.mx = e.clientX;
pos.my = e.clientY;
let top = el.offsetTop - pos.y;
let left = el.offsetLeft - pos.x;
el.style.top = top + 'px';
el.style.left = left + 'px';
if (onMove) onMove(top, left);
}
function up() {
document.onmousemove = null;
document.onmouseup = null;
}
}
makeDraggable(container, dragBar, (top, left) => {
GM_setValue('panel_top', top + 'px');
GM_setValue('panel_left', left + 'px');
});
// 调整大小功能
let resizeStart = false, startX, startY, startW, startH;
resizeHandle.onmousedown = (e) => {
e.preventDefault();
e.stopPropagation();
resizeStart = true;
startX = e.clientX;
startY = e.clientY;
startW = container.offsetWidth;
startH = container.offsetHeight;
document.onmousemove = doResize;
document.onmouseup = stopResize;
};
function doResize(e) {
if (!resizeStart) return;
let newW = startW + (e.clientX - startX);
let newH = startH + (e.clientY - startY);
if (newW < 200) newW = 200;
if (newH < 150) newH = 150;
container.style.width = newW + 'px';
container.style.height = newH + 'px';
content.style.maxHeight = (newH - 40) + 'px';
GM_setValue('panel_width', newW);
GM_setValue('panel_height', newH);
}
function stopResize() {
resizeStart = false;
document.onmousemove = null;
document.onmouseup = null;
}
// 按钮事件绑定
openBtn.onclick = batchOpen;
exportBtn.onclick = batchExportTxt;
bookmarkBtn.onclick = batchImportBookmarks;
clearBtn.onclick = () => {
if (confirm('确定清空所有链接吗?')) {
linksData = [];
saveLinks();
renderList();
showToast('已清空');
}
};
copyAllBtn.onclick = () => {
const allUrls = linksData.map(item => item.url).join('\n');
GM_setClipboard(allUrls);
showToast(`📋 已复制全部 ${linksData.length} 条链接`);
};
// 鼠标移动追踪
document.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
});
// 键盘监听:Alt 键(左Alt或右Alt)和 B 键
document.addEventListener('keydown', (e) => {
const tag = e.target.tagName.toLowerCase();
if (tag === 'input' || tag === 'textarea' || e.target.isContentEditable) return;
if (e.key === 'Alt' || e.code === 'AltLeft' || e.code === 'AltRight') {
e.preventDefault();
altCopyLink();
}
else if (e.key === 'b' || e.key === 'B') {
e.preventDefault();
biliCopy();
}
});
// 初始渲染
renderList();
// 简单的toast提示
function showToast(msg, isErr = false) {
let toast = document.getElementById('customToast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'customToast';
toast.style.cssText = `
position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%);
background: #333; color: #fff; padding: 8px 16px; border-radius: 6px;
z-index: 10000000; font-size: 13px; opacity: 0; transition: 0.2s;
pointer-events: none;
`;
document.body.appendChild(toast);
}
toast.textContent = msg;
toast.style.background = isErr ? '#e74c3c' : '#2c3e50';
toast.style.opacity = '1';
setTimeout(() => { toast.style.opacity = '0'; }, 2000);
}
// 加上一些防冲突的样式
GM_addStyle(`
#customToast { font-family: system-ui, sans-serif; }
`);
})();