Floating Link Menu

Customizable link menu.

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

您需要先安裝使用者腳本管理器擴展,如 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.2 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';

    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;
    let clickCount = 0;
    let clickTimer = 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 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 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');
        
        // Triple-click functionality
        const showBubbleButton = document.createElement('div');
        showBubbleButton.id = 'showBubbleButton';
        
        const style = document.createElement('style');
        style.innerHTML = `
            #customFloatingBubble {
                position: fixed;
                bottom: 30px;
                right: 30px;
                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;
            }
            #customFloatingBubble:hover {
                transform: scale(1.15);
                box-shadow: 0 0 20px 5px #0ff, 0 0 40px 15px #0ff;
            }
            #floatingMenu {
                position: fixed;
                bottom: 100px;
                right: 30px;
                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;
            }
            #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 {
                display: flex;
                flex-direction: column;
                gap: 5px;
                padding-top: 10px;
                border-top: 1px solid #444;
            }
            #linkForm h3, #excludeSection h3, #backupSection 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;
            }
            #showBubbleButton {
                position: fixed;
                bottom: 30px;
                right: 30px;
                width: 60px;
                height: 60px;
                cursor: pointer;
                z-index: 9999997;
            }
        `;

        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(controls);

        populateLinkList(linkList);
        populateExcludeList(excludeListElement);
        
        const isHidden = getBubbleHiddenState();
        bubble.style.display = isHidden ? 'none' : 'flex';
        
        if (isHidden) {
            document.body.appendChild(showBubbleButton);
        }

        // --- Event Listeners for the Triple-Click functionality ---
        function handleBubbleClick(e) {
            clickCount++;
            if (clickCount === 1) {
                clickTimer = setTimeout(() => {
                    if (clickCount === 1) {
                        const isMenuVisible = menu.style.display === 'flex';
                        menu.style.display = isMenuVisible ? 'none' : 'flex';
                    }
                    clickCount = 0;
                }, 300); // 300ms window for triple-click
            } else if (clickCount === 3) {
                clearTimeout(clickTimer);
                bubble.style.display = 'none';
                menu.style.display = 'none';
                saveBubbleHiddenState(true);
                document.body.appendChild(showBubbleButton);
                clickCount = 0;
            }
        }
        
        function handleShowBubbleClick(e) {
            clickCount++;
            if (clickCount === 3) {
                bubble.style.display = 'flex';
                saveBubbleHiddenState(false);
                menu.style.display = 'flex';
                showBubbleButton.remove();
                clickCount = 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);
        });
        
        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);
        });
        
        // --- BACKUP & RESTORE EVENT LISTENERS ---
        exportButton.addEventListener('click', () => {
            isExportMode = !isExportMode;
            if (isExportMode) {
                exportWrapper.innerHTML = `
                    <textarea readonly>${JSON.stringify(userLinks)}</textarea>
                    <button id="exportButton">Close</button>
                `;
                exportWrapper.querySelector('#exportButton').addEventListener('click', () => {
                    isExportMode = false;
                    exportWrapper.innerHTML = `<button id="exportButton">Export</button>`;
                    exportWrapper.querySelector('#exportButton').addEventListener('click', exportButton.click);
                });
            } else {
                exportWrapper.innerHTML = `<button id="exportButton">Export</button>`;
            }
        });

        importButton.addEventListener('click', () => {
            isImportMode = !isImportMode;
            if (isImportMode) {
                importWrapper.innerHTML = `
                    <textarea placeholder="Paste your link data here..."></textarea>
                    <button id="loadButton">Load</button>
                    <button id="importButton">Close</button>
                `;
                importWrapper.querySelector('#loadButton').addEventListener('click', () => {
                    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!');
                            isImportMode = false;
                            importWrapper.innerHTML = `<button id="importButton">Import</button>`;
                            importWrapper.querySelector('#importButton').addEventListener('click', importButton.click);
                        } else {
                            alert('Invalid data format. Please paste the correct JSON data.');
                        }
                    } catch (e) {
                        alert('Invalid data format. Please paste the correct JSON data.');
                    }
                });
                importWrapper.querySelector('#importButton').addEventListener('click', () => {
                    isImportMode = false;
                    importWrapper.innerHTML = `<button id="importButton">Import</button>`;
                    importWrapper.querySelector('#importButton').addEventListener('click', importButton.click);
                });
            } else {
                importWrapper.innerHTML = `<button id="importButton">Import</button>`;
            }
        });
    }

    function waitForBody() {
        if (document.body) {
            initializeScript();
        } else {
            setTimeout(waitForBody, 100);
        }
    }

    waitForBody();
})();