Greasy Fork

来自缓存

Greasy Fork is available in English.

Alt0501增强版链接收集器 · 可多选/批量打开/导入书签/可调整大小

按Alt键复制鼠标下链接,按B键复制B站视频,悬浮面板可移动/折叠/缩放,支持勾选链接,一键导出TXT、批量打开、导入书签

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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