Floating Link Menu

Customizable link menu.

目前為 2025-08-24 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);
    }
})();