Floating Link Menu

Customizable link menu.

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