您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Customizable link menu.
当前为
// ==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); } })();