Greasy Fork

Greasy Fork is available in English.

左键点击链接在新标签页打开 (白名单启用模式+UI)

默认禁用。仅在白名单站点强制链接在新标签页打开。可通过右下角UI面板管理白/黑名单。

当前为 2025-05-27 提交的版本,查看 最新版本

// ==UserScript==
// @name         左键点击链接在新标签页打开 (白名单启用模式+UI)
// @name:en      Open Links in New Tab on Whitelist (with UI Panel)
// @namespace    http://greasyfork.icu/users/your-username // 建议替换为你的唯一命名空间
// @version      1.5
// @description  默认禁用。仅在白名单站点强制链接在新标签页打开。可通过右下角UI面板管理白/黑名单。
// @description:en Disabled by default. Forces links in new tab ONLY on whitelisted sites. Manage rules via UI panel.
// @author       AI Assistant
// @match        *://*/*
// @grant        GM_openInTab
// @grant        window.open
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- 配置键名 ---
    const CONFIG_KEY_WHITELIST = 'openInNewTab_custom_whitelist_v1';
    const CONFIG_KEY_BLACKLIST = 'openInNewTab_custom_blacklist_v1';
    const CONFIG_KEY_PANEL_VISIBLE = 'openInNewTab_panelVisibleState_v1';

    // --- 默认配置 ---
    const defaultWhitelistPatterns = [
        // "https://example.com/always-open-new-tab/*" // 添加一些你希望默认启用的网站
    ];
    const defaultBlacklistPatterns = [
        // "https://example.com/never-open-new-tab/*"
    ];

    let userWhitelist = [];
    let userBlacklist = [];
    let panelVisible = GM_getValue(CONFIG_KEY_PANEL_VISIBLE, false);
    let uiPanel = null;

    // --- CSS 样式 ---
    GM_addStyle(`
        #openInNewTab-config-panel {
            position: fixed; right: 15px; bottom: 15px; width: 300px;
            max-height: calc(80vh - 30px); background-color: #f8f9fa;
            border: 1px solid #ced4da; border-radius: 8px; padding: 15px;
            z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            font-size: 14px; color: #212529; box-shadow: 0 5px 15px rgba(0,0,0,0.15);
            display: none; flex-direction: column;
        }
        #openInNewTab-config-panel.panel-visible { display: flex; }
        #openInNewTab-config-panel .panel-content-scrollable { overflow-y: auto; flex-grow: 1; margin-bottom: 10px; }
        #openInNewTab-config-panel h3 { margin-top: 0; margin-bottom: 12px; font-size: 18px; color: #343a40; border-bottom: 1px solid #dee2e6; padding-bottom: 8px; text-align: center; }
        #openInNewTab-config-panel h4 { margin-top: 10px; margin-bottom: 8px; font-size: 15px; color: #495057; }
        #openInNewTab-config-panel .add-form { display: flex; margin-bottom: 10px; }
        #openInNewTab-config-panel .add-form input[type="text"] { flex-grow: 1; margin-right: 8px; padding: 6px 8px; border: 1px solid #ced4da; border-radius: 4px; font-size: 13px; }
        #openInNewTab-config-panel .add-form button { padding: 6px 10px; font-size: 13px; color: white; background-color: #007bff; border: 1px solid #007bff; border-radius: 4px; cursor: pointer; white-space: nowrap; }
        #openInNewTab-config-panel .add-form button:hover { background-color: #0056b3; }
        #openInNewTab-config-panel ul { list-style-type: none; padding-left: 0; margin: 0 0 10px 0; border: 1px solid #e9ecef; border-radius: 4px; background-color: #fff; }
        #openInNewTab-config-panel li { padding: 7px 10px; border-bottom: 1px solid #f1f3f5; display: flex; align-items: center; justify-content: space-between; font-size: 13px; }
        #openInNewTab-config-panel li:last-child { border-bottom: none; }
        #openInNewTab-config-panel li .pattern-text { word-break: break-all; margin-right: 8px; flex-grow: 1; }
        #openInNewTab-config-panel .delete-btn { color: #dc3545; background-color: transparent; border: none; cursor: pointer; font-weight: bold; font-size: 18px; padding: 0 5px; line-height: 1; opacity: 0.6; }
        #openInNewTab-config-panel .delete-btn:hover { color: #c82333; opacity: 1; }
        #openInNewTab-config-panel .panel-empty-msg { font-size: 13px; color: #6c757d; padding: 10px; text-align: center; background-color: #fff; border-radius: 4px; }
        #openInNewTab-config-panel .close-panel-btn { display: block; width: 100%; padding: 8px 12px; font-size: 14px; color: #fff; background-color: #6c757d; border: none; border-radius: 4px; cursor: pointer; }
        #openInNewTab-config-panel .close-panel-btn:hover { background-color: #5a6268; }
    `);

    function createAddForm(listType, listArray, configKey, parentElement) {
        const form = document.createElement('div');
        form.className = 'add-form';
        const input = document.createElement('input');
        input.type = 'text';
        input.placeholder = `添加新${listType === 'white' ? '白' : '黑'}名单模式...`;
        input.addEventListener('keypress', function(e) { if (e.key === 'Enter') { addButton.click(); } });
        const addButton = document.createElement('button');
        addButton.textContent = '添加';
        addButton.onclick = () => {
            const pattern = input.value.trim();
            if (pattern) {
                if (listArray.includes(pattern)) { alert(`模式 "${pattern}" 已存在于${listType === 'white' ? '白' : '黑'}名单中!`); return; }
                listArray.push(pattern);
                GM_setValue(configKey, listArray.join(','));
                input.value = ''; renderListsUI();
            } else { alert('请输入有效的URL模式!'); }
        };
        form.appendChild(input); form.appendChild(addButton); parentElement.appendChild(form);
    }

    function createUIPanel() {
        if (!document.body) { setTimeout(createUIPanel, 100); return; }
        if (document.getElementById('openInNewTab-config-panel')) {
            uiPanel = document.getElementById('openInNewTab-config-panel');
            renderListsUI(); uiPanel.classList.toggle('panel-visible', panelVisible); return;
        }
        uiPanel = document.createElement('div'); uiPanel.id = 'openInNewTab-config-panel';
        const title = document.createElement('h3'); title.textContent = '链接新标签页打开 - 规则管理'; uiPanel.appendChild(title);
        const scrollableContent = document.createElement('div'); scrollableContent.className = 'panel-content-scrollable';
        ['whitelist', 'blacklist'].forEach(type => {
            const section = document.createElement('div'); section.id = `openInNewTab-${type}-section`;
            const sectionTitle = document.createElement('h4');
            sectionTitle.textContent = type === 'whitelist' ? '白名单 (强制启用):' : '黑名单 (明确禁用):';
            section.appendChild(sectionTitle);
            createAddForm(type, type === 'whitelist' ? userWhitelist : userBlacklist, type === 'whitelist' ? CONFIG_KEY_WHITELIST : CONFIG_KEY_BLACKLIST, section);
            const ul = document.createElement('ul'); ul.id = `openInNewTab-${type}-ul`; section.appendChild(ul);
            scrollableContent.appendChild(section);
        });
        uiPanel.appendChild(scrollableContent);
        const closeButton = document.createElement('button'); closeButton.textContent = '关闭面板';
        closeButton.className = 'close-panel-btn'; closeButton.onclick = togglePanelVisibility; uiPanel.appendChild(closeButton);
        document.body.appendChild(uiPanel); renderListsUI(); uiPanel.classList.toggle('panel-visible', panelVisible);
    }

    function renderListsUI() {
        if (!uiPanel || !document.body.contains(uiPanel)) return;
        const renderUlContent = (listArray, ulElementId, listType) => {
            const ul = uiPanel.querySelector('#' + ulElementId); if (!ul) return; ul.innerHTML = '';
            if (listArray.length === 0) {
                const emptyMsgLi = document.createElement('li'); emptyMsgLi.textContent = '(列表为空)';
                emptyMsgLi.className = 'panel-empty-msg'; emptyMsgLi.style.justifyContent = 'center'; emptyMsgLi.style.padding = '10px';
                ul.appendChild(emptyMsgLi); return;
            }
            listArray.forEach((pattern, index) => {
                const li = document.createElement('li'); const patternText = document.createElement('span');
                patternText.className = 'pattern-text'; patternText.textContent = pattern;
                const deleteBtn = document.createElement('button'); deleteBtn.innerHTML = '×';
                deleteBtn.className = 'delete-btn'; deleteBtn.title = '删除此条规则';
                deleteBtn.onclick = function() {
                    if (confirm(`确定要从${listType === 'white' ? '白' : '黑'}名单中删除 "${pattern}" 吗?`)) {
                        listArray.splice(index, 1);
                        GM_setValue(listType === 'white' ? CONFIG_KEY_WHITELIST : CONFIG_KEY_BLACKLIST, listArray.join(','));
                        renderListsUI();
                    }
                };
                li.appendChild(patternText); li.appendChild(deleteBtn); ul.appendChild(li);
            });
        };
        renderUlContent(userWhitelist, 'openInNewTab-whitelist-ul', 'white');
        renderUlContent(userBlacklist, 'openInNewTab-blacklist-ul', 'black');
    }

    function togglePanelVisibility() {
        panelVisible = !panelVisible; GM_setValue(CONFIG_KEY_PANEL_VISIBLE, panelVisible);
        if (panelVisible) {
            if (!uiPanel || !document.body.contains(uiPanel)) { createUIPanel(); }
            else { renderListsUI(); uiPanel.classList.add('panel-visible'); }
        } else { if (uiPanel) { uiPanel.classList.remove('panel-visible'); } }
    }

    function loadConfig() {
        const storedWhitelistStr = GM_getValue(CONFIG_KEY_WHITELIST);
        const storedBlacklistStr = GM_getValue(CONFIG_KEY_BLACKLIST);
        userWhitelist = (typeof storedWhitelistStr === 'string' && storedWhitelistStr.trim() !== '') ? storedWhitelistStr.split(',').map(s => s.trim()).filter(s => s) : (typeof storedWhitelistStr === 'undefined' ? (GM_setValue(CONFIG_KEY_WHITELIST, defaultWhitelistPatterns.join(',')), [...defaultWhitelistPatterns]) : []);
        userBlacklist = (typeof storedBlacklistStr === 'string' && storedBlacklistStr.trim() !== '') ? storedBlacklistStr.split(',').map(s => s.trim()).filter(s => s) : (typeof storedBlacklistStr === 'undefined' ? (GM_setValue(CONFIG_KEY_BLACKLIST, defaultBlacklistPatterns.join(',')), [...defaultBlacklistPatterns]) : []);
        if (uiPanel && panelVisible && document.body.contains(uiPanel)) { renderListsUI(); }
    }

    function urlMatchesPattern(url, pattern) {
        if (!pattern || !url) return false; let currentUrl = url; let p = pattern.trim();
        if (!p.includes("://")) { currentUrl = currentUrl.replace(/^https?:\/\//, ""); }
        if (!p.startsWith("www.") && !p.includes("://") && currentUrl.startsWith("www.")) { currentUrl = currentUrl.replace(/^www\./, "");}
        const escapedPattern = p.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
        const regexString = '^' + escapedPattern.replace(/\\\*/g, '.*') + '$';
        try { return new RegExp(regexString, 'i').test(currentUrl); } catch (e) { return false; }
    }

    if (typeof GM_registerMenuCommand === 'function' && typeof GM_getValue === 'function' && typeof GM_setValue === 'function') {
        GM_registerMenuCommand('配置 白名单 (Prompt)', function() {
            const current = GM_getValue(CONFIG_KEY_WHITELIST, userWhitelist.join(','));
            const newList = prompt('白名单 (强制启用),用逗号 "," 分隔:', current);
            if (newList !== null) { GM_setValue(CONFIG_KEY_WHITELIST, newList); loadConfig(); alert('白名单已更新!');}
        }, 'W');
        GM_registerMenuCommand('配置 黑名单 (Prompt)', function() {
            const current = GM_getValue(CONFIG_KEY_BLACKLIST, userBlacklist.join(','));
            const newList = prompt('黑名单 (明确禁用),用逗号 "," 分隔:', current);
            if (newList !== null) { GM_setValue(CONFIG_KEY_BLACKLIST, newList); loadConfig(); alert('黑名单已更新!');}
        }, 'B');
        GM_registerMenuCommand('显示/隐藏 规则管理面板', togglePanelVisibility, 'M'); // 'M' for Manage
    }

    loadConfig();

    const ensureInitialPanelState = () => {
        if (panelVisible) { // panelVisible 已从存储中加载
            if (!uiPanel || !document.body.contains(uiPanel)) { createUIPanel(); }
            else { uiPanel.classList.add('panel-visible'); renderListsUI(); }
        }
    };
    if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", ensureInitialPanelState); }
    else { ensureInitialPanelState(); }

    document.addEventListener('click', function(event) {
        const currentPageUrl = window.location.href;
        let scriptShouldRunOnThisPage = false; // 1. 默认禁用

        // 2. 检查白名单
        const isWhitelisted = userWhitelist.some(pattern => urlMatchesPattern(currentPageUrl, pattern));

        if (isWhitelisted) {
            scriptShouldRunOnThisPage = true; // 白名单匹配,则启用
        } else {
            // 3. 如果不在白名单中,检查黑名单
            const isBlacklisted = userBlacklist.some(pattern => urlMatchesPattern(currentPageUrl, pattern));
            if (isBlacklisted) {
                scriptShouldRunOnThisPage = false; // 在黑名单中(且不在白名单),则明确禁用
            } else {
                // 既不在白名单,也不在黑名单,保持默认禁用状态
                scriptShouldRunOnThisPage = false;
            }
        }

        if (!scriptShouldRunOnThisPage) {
            // console.log('左键新标签页脚本:当前页面未启用此功能。');
            return;
        }
        // console.log('左键新标签页脚本:当前页面已启用此功能。');

        if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
        let targetElement = event.target; let anchorElement = null;
        for (let i = 0; i < 5 && targetElement && targetElement !== document.body; i++) { if (targetElement.tagName === 'A') { anchorElement = targetElement; break; } targetElement = targetElement.parentElement; }
        if (anchorElement) {
            const href = anchorElement.href; const rawHref = anchorElement.getAttribute('href');
            if (!href || (rawHref && rawHref.startsWith('#')) || href.startsWith('javascript:')) return;
            if (anchorElement.hasAttribute('download')) return;
            if (event.altKey && event.shiftKey) { return; } // Alt+Shift+Click 绕过脚本

            event.preventDefault(); event.stopPropagation();
            if (typeof GM_openInTab === 'function') { GM_openInTab(href, { active: true, insert: true }); }
            else if (typeof window.open === 'function') { window.open(href, '_blank'); }
            else { console.warn('无法在新标签页中打开链接:GM_openInTab 和 window.open 均不可用。'); }
        }
    }, true);
})();