Floating Link Menu

Customizable link menu.

当前为 2025-08-24 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Floating Link Menu
// @namespace    http://tampermonkey.net/
// @version      2.3 universal
// @description  Customizable link menu.
// @author       echoZ
// @license      MIT
// @match        *://*/*
// @exclude      *://*routerlogin.net/*
// @exclude      *://*192.168.1.1/*
// @exclude      *://*192.168.0.1/*
// @exclude      *://*my.bankofamerica.com/*
// @exclude      *://*wellsfargo.com/*
// @exclude      *://*chase.com/*
// @exclude      *://*citibank.com/*
// @exclude      *://*online.citi.com/*
// @exclude      *://*capitalone.com/*
// @exclude      *://*usbank.com/*
// @exclude      *://*paypal.com/*
// @grant        none
// @run-at       document-start
// ==/UserScript==
(function() {
    'use strict';

    // --- SCRIPT EXCLUSION LOGIC ---
    const excludedDomainsStorageKey = 'excludedUniversalDomains';
    const isBubbleHiddenStorageKey = 'isBubbleHidden';
    const buttonPositionStorageKey = 'bubblePosition';

    function getExcludedDomains() {
        const storedDomains = localStorage.getItem(excludedDomainsStorageKey);
        return storedDomains ? JSON.parse(storedDomains) : [];
    }

    const excludedDomains = getExcludedDomains();
    const currentUrl = window.location.href;
    const isExcluded = excludedDomains.some(domain => currentUrl.includes(domain));
    if (isExcluded) {
        return;
    }
    // --- END EXCLUSION LOGIC ---

    // --- UNIFIED LINKS & STATE ---
    const storageKey = 'universalLinkManagerLinks';
    let isDeleteMode = false;
    let isExcludeDeleteMode = false;
    let isExportMode = false;
    let isImportMode = false;

    // --- FIXED: SEPARATE CLICK COUNTERS TO PREVENT CONFLICTS ---
    let bubbleClickCount = 0;
    let bubbleClickTimer = null;
    let restoreClickCount = 0;
    let restoreClickTimer = null;


    function getBubbleHiddenState() {
        return localStorage.getItem(isBubbleHiddenStorageKey) === 'true';
    }

    function saveBubbleHiddenState(isHidden) {
        localStorage.setItem(isBubbleHiddenStorageKey, isHidden);
    }

    function saveExcludedDomains() {
        localStorage.setItem(excludedDomainsStorageKey, JSON.stringify(excludedDomains));
    }

    function getLinks() {
        const storedLinks = localStorage.getItem(storageKey);
        if (storedLinks) {
            return JSON.parse(storedLinks);
        }
        return [
            { label: 'Google', url: 'https://www.google.com/' },
            { label: 'Gemini AI', url: 'https://gemini.google.com/' },
            { label: 'OpenAI', url: 'https://www.openai.com/' }
        ];
    }

    let userLinks = getLinks();

    function saveLinks() {
        localStorage.setItem(storageKey, JSON.stringify(userLinks));
    }

    function getButtonPosition() {
        const storedPosition = localStorage.getItem(buttonPositionStorageKey);
        return storedPosition ? JSON.parse(storedPosition) : { vertical: 'bottom', horizontal: 'right' };
    }

    function saveButtonPosition(position) {
        localStorage.setItem(buttonPositionStorageKey, JSON.stringify(position));
    }

    let buttonPosition = getButtonPosition();

    function populateLinkList(linkListElement) {
        linkListElement.innerHTML = '';
        userLinks.forEach((linkData, index) => {
            const linkWrapper = document.createElement('div');
            linkWrapper.className = 'link-wrapper';

            const link = document.createElement('a');
            link.href = linkData.url;
            link.textContent = linkData.label;
            link.target = '_blank';

            linkWrapper.appendChild(link);

            if (isDeleteMode) {
                const deleteButton = document.createElement('button');
                deleteButton.className = 'delete-link-button';
                deleteButton.textContent = 'x';
                deleteButton.addEventListener('click', (event) => {
                    event.preventDefault();
                    userLinks.splice(index, 1);
                    saveLinks();
                    populateLinkList(linkListElement);
                });
                linkWrapper.appendChild(deleteButton);
            }
            linkListElement.appendChild(linkWrapper);
        });
    }

    function populateExcludeList(excludeListElement) {
        excludeListElement.innerHTML = '';
        excludedDomains.forEach((domain, index) => {
            const domainWrapper = document.createElement('div');
            domainWrapper.className = 'exclude-wrapper';

            const domainLabel = document.createElement('span');
            domainLabel.textContent = domain;
            domainWrapper.appendChild(domainLabel);

            if (isExcludeDeleteMode) {
                const deleteButton = document.createElement('button');
                deleteButton.className = 'delete-exclude-button';
                deleteButton.textContent = 'x';
                deleteButton.addEventListener('click', (event) => {
                    event.preventDefault();
                    excludedDomains.splice(index, 1);
                    saveExcludedDomains();
                    populateExcludeList(excludeListElement);
                });
                domainWrapper.appendChild(deleteButton);
            }
            excludeListElement.appendChild(domainWrapper);
        });
    }

    function initializeScript() {
        if (document.getElementById('customFloatingBubble')) {
            return;
        }

        const bubble = document.createElement('div');
        bubble.id = 'customFloatingBubble';
        bubble.textContent = 'λ';

        const menu = document.createElement('div');
        menu.id = 'floatingMenu';

        const linkList = document.createElement('div');
        linkList.id = 'linkList';

        const linkForm = document.createElement('div');
        linkForm.id = 'linkForm';
        linkForm.innerHTML = `
            <h3>Add New Link</h3>
            <input type="text" id="linkLabel" placeholder="Label (e.g. My Site)">
            <input type="text" id="linkUrl" placeholder="URL (e.g. https://example.com)">
            <button id="saveLinkButton">Save</button>
        `;
        const saveLinkButton = linkForm.querySelector('#saveLinkButton');
        const linkLabelInput = linkForm.querySelector('#linkLabel');
        const linkUrlInput = linkForm.querySelector('#linkUrl');

        const excludeSection = document.createElement('div');
        excludeSection.id = 'excludeSection';
        excludeSection.innerHTML = `
            <h3>Excluded Websites</h3>
            <div id="excludeList"></div>
            <input type="text" id="excludeUrl" placeholder="Domain (e.g. example.com)">
            <button id="saveExcludeButton">Add Exclude</button>
            <button id="deleteExcludeButton">Delete Excludes</button>
        `;
        const excludeListElement = excludeSection.querySelector('#excludeList');
        const deleteExcludeButton = excludeSection.querySelector('#deleteExcludeButton');
        const saveExcludeButton = excludeSection.querySelector('#saveExcludeButton');
        const excludeUrlInput = excludeSection.querySelector('#excludeUrl');

        const backupSection = document.createElement('div');
        backupSection.id = 'backupSection';
        backupSection.innerHTML = `
            <h3>Backup & Restore</h3>
            <div id="exportWrapper">
                <button id="exportButton">Export</button>
            </div>
            <div id="importWrapper">
                <button id="importButton">Import</button>
            </div>
        `;
        const exportWrapper = backupSection.querySelector('#exportWrapper');
        const importWrapper = backupSection.querySelector('#importWrapper');

        const positionControls = document.createElement('div');
        positionControls.id = 'positionControls';
        positionControls.innerHTML = `
            <h3>Button Position</h3>
            <div class="position-buttons">
                <button id="position-top-left">Top-Left</button>
                <button id="position-top-right">Top-Right</button>
                <button id="position-bottom-left">Bottom-Left</button>
                <button id="position-bottom-right">Bottom-Right</button>
            </div>
        `;

        const controls = document.createElement('div');
        controls.id = 'menuControls';
        controls.innerHTML = `
            <button id="deleteLinksButton">Delete Links</button>
            <button id="hideButton">Hide Button</button>
            <button id="closeMenuButton">Close Menu</button>
        `;
        const deleteLinksButton = controls.querySelector('#deleteLinksButton');
        const hideButton = controls.querySelector('#hideButton');
        const closeMenuButton = controls.querySelector('#closeMenuButton');

        // This button is now an invisible clickable area
        const showBubbleButton = document.createElement('div');
        showBubbleButton.id = 'showBubbleButton';

        const style = document.createElement('style');
        style.innerHTML = `
            #customFloatingBubble {
                position: fixed;
                width: 60px;
                height: 60px;
                background-color: #0ff;
                border-radius: 50%;
                box-shadow: 0 0 15px 3px #0ff, 0 0 30px 10px #0ff;
                cursor: pointer;
                z-index: 9999999;
                display: flex;
                justify-content: center;
                align-items: center;
                font-size: 36px;
                font-weight: 900;
                color: #001f3f;
                user-select: none;
                font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
                transition: transform 0.2s ease, box-shadow 0.2s ease, top 0.2s ease, bottom 0.2s ease, left 0.2s ease, right 0.2s ease;
            }
            #customFloatingBubble:hover {
                transform: scale(1.15);
                box-shadow: 0 0 20px 5px #0ff, 0 0 40px 15px #0ff;
            }
            #floatingMenu {
                position: fixed;
                width: 300px;
                background-color: #222;
                border: 2px solid #0ff;
                border-radius: 8px;
                box-shadow: 0 0 10px rgba(0,255,255,0.7);
                padding: 10px;
                z-index: 9999998;
                display: none;
                flex-direction: column;
                gap: 10px;
                max-height: 80vh;
                overflow-y: auto;
                transition: top 0.2s ease, bottom 0.2s ease, left 0.2s ease, right 0.2s ease;
            }
            #linkList, #excludeList {
                display: flex;
                flex-direction: column;
                gap: 5px;
            }
            .link-wrapper, .exclude-wrapper {
                display: flex;
                align-items: center;
                gap: 5px;
            }
            #linkList a, .exclude-wrapper span {
                flex-grow: 1;
                padding: 8px;
                color: #fff;
                background-color: #333;
                border: 1px solid #0ff;
                text-align: center;
                text-decoration: none;
                border-radius: 5px;
                transition: background-color 0.2s ease, color 0.2s ease;
            }
            #linkList a:hover {
                background-color: #0ff;
                color: #000;
            }
            .delete-link-button, .delete-exclude-button {
                width: 30px;
                height: 30px;
                background-color: #a00;
                color: #fff;
                border: 1px solid #f00;
                border-radius: 50%;
                cursor: pointer;
                font-weight: bold;
                transition: background-color 0.2s ease;
                display: flex;
                justify-content: center;
                align-items: center;
                padding: 0;
            }
            .delete-link-button:hover, .delete-exclude-button:hover {
                background-color: #f00;
            }
            #menuControls {
                display: flex;
                flex-wrap: wrap;
                justify-content: space-between;
                gap: 5px;
            }
            #menuControls button {
                padding: 8px 12px;
                background-color: #444;
                color: #0ff;
                border: 1px solid #0ff;
                border-radius: 5px;
                cursor: pointer;
                flex: 1 1 45%;
                font-size: 12px;
                text-align: center;
            }
            #menuControls button:hover {
                background-color: #0ff;
                color: #000;
            }
            #linkForm, #excludeSection, #backupSection, #positionControls {
                display: flex;
                flex-direction: column;
                gap: 5px;
                padding-top: 10px;
                border-top: 1px solid #444;
            }
            #linkForm h3, #excludeSection h3, #backupSection h3, #positionControls h3 {
                color: #fff;
                margin: 0;
                text-align: center;
            }
            #linkForm input, #excludeSection input, #backupSection textarea {
                padding: 8px;
                border: 1px solid #0ff;
                background-color: #333;
                color: #fff;
                border-radius: 5px;
            }
            #backupSection button {
                padding: 8px 12px;
                background-color: #444;
                color: #0ff;
                border: 1px solid #0ff;
                border-radius: 5px;
                cursor: pointer;
                flex: 1;
                font-size: 14px;
            }
            #backupSection button:hover {
                background-color: #0ff;
                color: #000;
            }
            /* --- FIXED: Made the show button an invisible overlay --- */
            #showBubbleButton {
                position: fixed;
                width: 60px;
                height: 60px;
                cursor: pointer;
                z-index: 9999997;
                background-color: transparent;
                border: none;
                user-select: none;
            }
            #positionControls .position-buttons {
                display: grid;
                grid-template-columns: 1fr 1fr;
                gap: 5px;
            }
            #positionControls button {
                padding: 8px;
                background-color: #444;
                color: #0ff;
                border: 1px solid #0ff;
                border-radius: 5px;
                cursor: pointer;
                font-size: 12px;
            }
            #positionControls button.active, #positionControls button:hover {
                background-color: #0ff;
                color: #000;
            }
            .import-buttons {
                display: flex;
                gap: 5px;
            }
            .import-buttons button {
                flex: 1;
            }
        `;

        document.head.appendChild(style);
        document.body.appendChild(bubble);
        document.body.appendChild(menu);
        menu.appendChild(linkList);
        menu.appendChild(linkForm);
        menu.appendChild(excludeSection);
        menu.appendChild(backupSection);
        menu.appendChild(positionControls);
        menu.appendChild(controls);

        populateLinkList(linkList);
        populateExcludeList(excludeListElement);

        function applyButtonPosition() {
            bubble.style.top = '';
            bubble.style.left = '';
            bubble.style.bottom = '';
            bubble.style.right = '';
            menu.style.top = '';
            menu.style.left = '';
            menu.style.bottom = '';
            menu.style.right = '';
            showBubbleButton.style.top = '';
            showBubbleButton.style.left = '';
            showBubbleButton.style.bottom = '';
            showBubbleButton.style.right = '';

            bubble.style[buttonPosition.vertical] = '30px';
            bubble.style[buttonPosition.horizontal] = '30px';
            menu.style[buttonPosition.vertical] = '100px';
            menu.style[buttonPosition.horizontal] = '30px';
            showBubbleButton.style[buttonPosition.vertical] = '30px';
            showBubbleButton.style[buttonPosition.horizontal] = '30px';

            const positionButtons = positionControls.querySelectorAll('button');
            positionButtons.forEach(btn => {
                btn.classList.remove('active');
            });
            const activeBtnId = `position-${buttonPosition.vertical}-${buttonPosition.horizontal}`;
            const activeBtn = document.getElementById(activeBtnId);
            if (activeBtn) {
                activeBtn.classList.add('active');
            }
        }

        applyButtonPosition();

        const isHidden = getBubbleHiddenState();
        bubble.style.display = isHidden ? 'none' : 'flex';

        if (isHidden) {
            document.body.appendChild(showBubbleButton);
        }

        // --- FIXED: Event Listeners with separate counters ---
        function handleBubbleClick(e) {
            bubbleClickCount++;
            if (bubbleClickCount === 1) {
                bubbleClickTimer = setTimeout(() => {
                    if (bubbleClickCount === 1) {
                        const isMenuVisible = menu.style.display === 'flex';
                        menu.style.display = isMenuVisible ? 'none' : 'flex';
                    }
                    bubbleClickCount = 0;
                }, 300);
            } else if (bubbleClickCount === 3) {
                clearTimeout(bubbleClickTimer);
                bubble.style.display = 'none';
                menu.style.display = 'none';
                saveBubbleHiddenState(true);
                document.body.appendChild(showBubbleButton);
                applyButtonPosition();
                bubbleClickCount = 0;
            }
        }

        function handleShowBubbleClick(e) {
            restoreClickCount++;
            if (restoreClickTimer) clearTimeout(restoreClickTimer);
            restoreClickTimer = setTimeout(() => {
                restoreClickCount = 0;
            }, 400); // 400ms window for triple click

            if (restoreClickCount === 3) {
                clearTimeout(restoreClickTimer);
                bubble.style.display = 'flex';
                saveBubbleHiddenState(false);
                if (showBubbleButton.parentNode) {
                    showBubbleButton.remove();
                }
                restoreClickCount = 0;
            }
        }

        bubble.addEventListener('click', handleBubbleClick);
        showBubbleButton.addEventListener('click', handleShowBubbleClick);

        // --- UI BUTTON LISTENERS ---
        const exportButton = backupSection.querySelector('#exportButton');
        const importButton = backupSection.querySelector('#importWrapper #importButton');

        hideButton.addEventListener('click', () => {
            bubble.style.display = 'none';
            menu.style.display = 'none';
            saveBubbleHiddenState(true);
            document.body.appendChild(showBubbleButton);
            applyButtonPosition();
        });

        closeMenuButton.addEventListener('click', () => {
            menu.style.display = 'none';
        });

        saveLinkButton.addEventListener('click', () => {
            const label = linkLabelInput.value;
            const url = linkUrlInput.value;
            if (label && url) {
                userLinks.push({ label, url });
                saveLinks();
                populateLinkList(linkList);
                linkLabelInput.value = '';
                linkUrlInput.value = '';
            } else {
                alert('Please enter both a label and a URL.');
            }
        });

        deleteLinksButton.addEventListener('click', () => {
            isDeleteMode = !isDeleteMode;
            deleteLinksButton.textContent = isDeleteMode ? 'Exit Delete' : 'Delete Links';
            populateLinkList(linkList);
        });

        saveExcludeButton.addEventListener('click', () => {
            const domain = excludeUrlInput.value;
            if (domain && !excludedDomains.includes(domain)) {
                excludedDomains.push(domain);
                saveExcludedDomains();
                populateExcludeList(excludeListElement);
                excludeUrlInput.value = '';
            } else if (excludedDomains.includes(domain)) {
                alert('This domain is already on the exclusion list.');
            } else {
                alert('Please enter a domain to exclude.');
            }
        });

        deleteExcludeButton.addEventListener('click', () => {
            isExcludeDeleteMode = !isExcludeDeleteMode;
            deleteExcludeButton.textContent = isExcludeDeleteMode ? 'Exit Exclude Delete' : 'Delete Excludes';
            populateExcludeList(excludeListElement);
        });

        positionControls.querySelectorAll('button').forEach(button => {
            button.addEventListener('click', (event) => {
                const id = event.target.id;
                const parts = id.split('-');
                const newVertical = parts[1];
                const newHorizontal = parts[2];
                buttonPosition.vertical = newVertical;
                buttonPosition.horizontal = newHorizontal;
                saveButtonPosition(buttonPosition);
                applyButtonPosition();
            });
        });

        // --- BACKUP & RESTORE EVENT LISTENERS ---
        exportWrapper.addEventListener('click', (event) => {
            const button = event.target;
            if (button.id === 'exportButton') {
                if (button.textContent === 'Export') {
                    exportWrapper.innerHTML = `
                        <textarea readonly>${JSON.stringify(userLinks)}</textarea>
                        <button id="exportButton">Close</button>
                    `;
                } else {
                    exportWrapper.innerHTML = `<button id="exportButton">Export</button>`;
                }
            }
        });

        importWrapper.addEventListener('click', (event) => {
            const button = event.target;
            if (button.id === 'importButton') {
                if (button.textContent === 'Import') {
                    importWrapper.innerHTML = `
                        <textarea placeholder="Paste your link data here..."></textarea>
                        <div class="import-buttons">
                            <button id="loadButton">Load</button>
                            <button id="importButton">Cancel</button>
                        </div>
                    `;
                } else {
                    importWrapper.innerHTML = `<button id="importButton">Import</button>`;
                }
            } else if (button.id === 'loadButton') {
                const data = importWrapper.querySelector('textarea').value;
                try {
                    const importedLinks = JSON.parse(data);
                    if (Array.isArray(importedLinks)) {
                        userLinks = importedLinks;
                        saveLinks();
                        populateLinkList(linkList);
                        alert('Links imported successfully!');
                        importWrapper.innerHTML = `<button id="importButton">Import</button>`;
                    } else {
                        alert('Invalid data format. Please paste the correct JSON data.');
                    }
                } catch (e) {
                    alert('Invalid data format. Please paste the correct JSON data.');
                }
            }
        });
    }

    if (document.body) {
        initializeScript();
    } else {
        document.addEventListener('DOMContentLoaded', initializeScript);
    }
})();