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