// ==UserScript==
// @name Floating Link Menu
// @namespace http://tampermonkey.net/
// @version 2.7 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 GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @run-at document-start
// ==/UserScript==
(async function() {
'use strict';
// --- SCRIPT EXCLUSION LOGIC ---
const excludedDomainsStorageKey = 'excludedUniversalDomains';
const currentUrl = window.location.href;
const excludedDomains = await GM_getValue(excludedDomainsStorageKey, []);
const isExcluded = excludedDomains.some(domain => currentUrl.includes(domain));
if (isExcluded) {
return;
}
// --- END EXCLUSION LOGIC ---
// --- Data Management using Tampermonkey's GM_ API ---
const storageKey = 'universalLinkManagerLinks';
const isBubbleHiddenStorageKey = 'isBubbleHidden';
const buttonPositionStorageKey = 'bubblePosition';
const themeStorageKey = 'universalLinkManagerTheme';
const defaultLinks = [
{ label: 'Google', url: 'https://www.google.com/' },
{ label: 'Gemini AI', url: 'https://gemini.google.com/' },
{ label: 'OpenAI', url: 'https://www.openai.com/' }
];
let isDeleteMode = false;
let isExcludeDeleteMode = false;
async function getLinks() {
return await GM_getValue(storageKey, defaultLinks);
}
async function saveLinks(links) {
await GM_setValue(storageKey, links);
}
async function getExcludedDomains() {
return await GM_getValue(excludedDomainsStorageKey, []);
}
async function saveExcludedDomains(domains) {
await GM_setValue(excludedDomainsStorageKey, domains);
}
async function getBubbleHiddenState() {
return await GM_getValue(isBubbleHiddenStorageKey, false);
}
async function saveBubbleHiddenState(isHidden) {
await GM_setValue(isBubbleHiddenStorageKey, isHidden);
}
async function getButtonPosition() {
return await GM_getValue(buttonPositionStorageKey, { vertical: 'bottom', horizontal: 'right' });
}
async function saveButtonPosition(position) {
await GM_setValue(buttonPositionStorageKey, position);
}
async function getTheme() {
return await GM_getValue(themeStorageKey, 'default');
}
async function saveTheme(theme) {
await GM_setValue(themeStorageKey, theme);
}
// --- Style and Themes ---
const themes = {
default: `
#floatingMenu, #universalLinkManagerUI, #bubbleMenu { background-color: #222; border: 2px solid #0ff; box-shadow: 0 0 10px rgba(0,255,255,0.7); }
#linkList a, .exclude-wrapper span { color: #fff; background-color: #333; border: 1px solid #0ff; }
#linkList a:hover { background-color: #0ff; color: #000; }
#menuControls button, #backupSection button, #positionControls button, .modal-button, #bubbleMenu button { color: #0ff; border: 1px solid #0ff; background-color: #444; }
#menuControls button:hover, #backupSection button:hover, #positionControls button:hover, .modal-button:hover, #bubbleMenu button:hover { background-color: #0ff; color: #000; }
#linkForm input, #excludeSection input { border: 1px solid #0ff; background-color: #333; color: #fff; }
#linkForm h3, #excludeSection h3, #backupSection h3, #positionControls h3 { color: #fff; }
.delete-link-button, .delete-exclude-button { background-color: #a00; border: 1px solid #f00; color: #fff; }
.delete-link-button:hover, .delete-exclude-button:hover { background-color: #f00; }
.active { background-color: #0ff; color: #000 !important; }
`,
highContrast: `
#floatingMenu, #universalLinkManagerUI, #bubbleMenu { background-color: #000; border: 2px solid #ffff00; box-shadow: 0 0 15px rgba(255,255,0,0.9); }
#linkList a, .exclude-wrapper span { color: #ffff00; background-color: #000; border: 1px solid #ffff00; text-shadow: 0 0 5px #ffff00; font-weight: bold; }
#linkList a:hover { background-color: #ffff00; color: #000; text-shadow: none; }
#menuControls button, #backupSection button, #positionControls button, .modal-button, #bubbleMenu button { color: #ffff00; border: 1px solid #ffff00; background-color: #333; }
#menuControls button:hover, #backupSection button:hover, #positionControls button:hover, .modal-button:hover, #bubbleMenu button:hover { background-color: #ffff00; color: #000; }
#linkForm input, #excludeSection input { border: 1px solid #ffff00; background-color: #000; color: #ffff00; }
#linkForm h3, #excludeSection h3, #backupSection h3, #positionControls h3 { color: #ffff00; }
.delete-link-button, .delete-exclude-button { background-color: #ff0000; border: 1px solid #ff0000; color: #ffff00; }
.delete-link-button:hover, .delete-exclude-button:hover { background-color: #ff5555; }
.active { background-color: #ffff00; color: #000 !important; }
`
};
const baseStyle = `
@keyframes pulse {
0% { transform: scale(1); box-shadow: 0 0 15px 3px #0ff, 0 0 30px 10px #0ff; }
50% { transform: scale(1.05); box-shadow: 0 0 20px 5px #0ff, 0 0 40px 15px #0ff; }
100% { transform: scale(1); box-shadow: 0 0 15px 3px #0ff, 0 0 30px 10px #0ff; }
}
@keyframes neonGlow {
0% { box-shadow: 0 0 10px rgba(0,255,255,0.7); }
50% { box-shadow: 0 0 15px rgba(0,255,255,0.9), 0 0 25px rgba(0,255,255,0.6); }
100% { box-shadow: 0 0 10px rgba(0,255,255,0.7); }
}
#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;
animation: pulse 3s infinite ease-in-out;
}
#customFloatingBubble:hover {
transform: scale(1.15);
box-shadow: 0 0 20px 5px #0ff, 0 0 40px 15px #0ff;
}
#universalLinkManagerUI {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 350px;
border-radius: 8px;
padding: 15px;
z-index: 9999998;
display: none;
flex-direction: column;
gap: 15px;
max-height: 90vh;
overflow-y: auto;
animation: neonGlow 4s infinite ease-in-out;
}
#bubbleMenu {
position: fixed;
z-index: 9999999;
padding: 10px;
border-radius: 8px;
display: none;
flex-direction: column;
gap: 8px;
animation: neonGlow 4s infinite ease-in-out;
}
#bubbleMenu button {
transition: background-color 0.2s, color 0.2s;
}
#ui-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #555;
padding-bottom: 10px;
}
#ui-header h2 {
margin: 0;
color: #0ff;
text-shadow: 0 0 5px #0ff;
}
#closeUI {
background: none;
border: none;
color: #fff;
font-size: 24px;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
border-radius: 50%;
transition: all 0.2s ease;
}
#closeUI:hover {
background-color: #f00;
transform: scale(1.1);
}
#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;
text-align: center;
text-decoration: none;
border-radius: 5px;
transition: background-color 0.2s ease, color 0.2s ease;
text-shadow: 0 0 3px currentColor;
font-size: 14px;
}
.delete-link-button, .delete-exclude-button {
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
font-weight: bold;
transition: background-color 0.2s ease;
display: flex;
justify-content: center;
align-items: center;
padding: 0;
}
#menuControls {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 5px;
}
#menuControls button, .modal-button {
padding: 8px 12px;
border-radius: 5px;
cursor: pointer;
flex: 1 1 45%;
font-size: 12px;
text-align: center;
}
#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 {
margin: 0;
text-align: center;
}
#linkForm input, #excludeSection input {
padding: 8px;
border-radius: 5px;
}
#backupSection button {
flex: 1;
font-size: 14px;
}
#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;
}
.import-buttons {
display: flex;
gap: 5px;
}
.import-buttons button {
flex: 1;
}
`;
GM_addStyle(baseStyle);
let bubble = null;
let mainUI = null;
let showBubbleButton = null;
let bubbleMenu = null;
function populateLinkList(links, linkListElement) {
linkListElement.innerHTML = '';
links.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', async (event) => {
event.preventDefault();
links.splice(index, 1);
await saveLinks(links);
populateLinkList(links, linkListElement);
});
linkWrapper.appendChild(deleteButton);
}
linkListElement.appendChild(linkWrapper);
});
}
function populateExcludeList(domains, excludeListElement) {
excludeListElement.innerHTML = '';
domains.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', async (event) => {
event.preventDefault();
domains.splice(index, 1);
await saveExcludedDomains(domains);
populateExcludeList(domains, excludeListElement);
});
domainWrapper.appendChild(deleteButton);
}
excludeListElement.appendChild(domainWrapper);
});
}
function applyButtonPosition(position) {
if (bubble) {
bubble.style.top = '';
bubble.style.left = '';
bubble.style.bottom = '';
bubble.style.right = '';
bubble.style[position.vertical] = '30px';
bubble.style[position.horizontal] = '30px';
}
if (showBubbleButton) {
showBubbleButton.style.top = '';
showBubbleButton.style.left = '';
showBubbleButton.style.bottom = '';
showBubbleButton.style.right = '';
showBubbleButton.style[position.vertical] = '30px';
showBubbleButton.style[position.horizontal] = '30px';
}
if (bubbleMenu) {
bubbleMenu.style.top = '';
bubbleMenu.style.left = '';
bubbleMenu.style.bottom = '';
bubbleMenu.style.right = '';
if (position.vertical === 'top') {
bubbleMenu.style.top = '100px';
} else {
bubbleMenu.style.bottom = '100px';
}
if (position.horizontal === 'left') {
bubbleMenu.style.left = '30px';
} else {
bubbleMenu.style.right = '30px';
}
}
const positionButtons = document.querySelectorAll('#positionControls button');
positionButtons.forEach(btn => {
btn.classList.remove('active');
});
const activeBtnId = `position-${position.vertical}-${position.horizontal}`;
const activeBtn = document.getElementById(activeBtnId);
if (activeBtn) {
activeBtn.classList.add('active');
}
}
function applyTheme(themeName) {
const styleElement = document.getElementById('universalLinkManagerStyle');
if (styleElement) {
styleElement.innerHTML = `${baseStyle}\n${themes[themeName]}\n`;
}
}
async function initializeScript() {
if (document.getElementById('customFloatingBubble')) {
return;
}
const buttonPosition = await getButtonPosition();
const currentTheme = await getTheme();
const style = document.createElement('style');
style.id = 'universalLinkManagerStyle';
document.head.appendChild(style);
applyTheme(currentTheme);
// Create the bubble
bubble = document.createElement('div');
bubble.id = 'customFloatingBubble';
bubble.textContent = 'λ';
document.body.appendChild(bubble);
// Create the mini bubble menu
bubbleMenu = document.createElement('div');
bubbleMenu.id = 'bubbleMenu';
bubbleMenu.innerHTML = `
<button id="instantAddButton">Add This Site</button>
<button id="showFullMenuButton">Settings</button>
`;
document.body.appendChild(bubbleMenu);
// Create the main modal UI
mainUI = document.createElement('div');
mainUI.id = 'universalLinkManagerUI';
mainUI.innerHTML = `
<div id="ui-header">
<h2>Universal Links</h2>
<button id="closeUI">X</button>
</div>
<div id="ui-content">
<div id="linkList"></div>
<div id="linkForm">
<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" class="modal-button">Save</button>
</div>
<div id="excludeSection">
<h3>Excluded Websites</h3>
<div id="excludeList"></div>
<input type="text" id="excludeUrl" placeholder="Domain (e.g. example.com)">
<button id="saveExcludeButton" class="modal-button">Add Exclude</button>
<button id="deleteExcludeButton" class="modal-button">Delete Excludes</button>
</div>
<div id="backupSection">
<h3>Backup & Restore</h3>
<div id="exportWrapper">
<button id="exportButton" class="modal-button">Export</button>
</div>
<div id="importWrapper">
<button id="importButton" class="modal-button">Import</button>
</div>
</div>
<div id="positionControls">
<h3>Button Position</h3>
<div class="position-buttons">
<button id="position-top-left" class="modal-button">Top-Left</button>
<button id="position-top-right" class="modal-button">Top-Right</button>
<button id="position-bottom-left" class="modal-button">Bottom-Left</button>
<button id="position-bottom-right" class="modal-button">Bottom-Right</button>
</div>
</div>
<div id="menuControls">
<button id="deleteLinksButton" class="modal-button">Delete Links</button>
<button id="themeButton" class="modal-button">Theme</button>
<button id="hideButton" class="modal-button">Hide Button</button>
</div>
</div>
`;
document.body.appendChild(mainUI);
// Create the invisible show bubble button
showBubbleButton = document.createElement('div');
showBubbleButton.id = 'showBubbleButton';
document.body.appendChild(showBubbleButton);
// Load initial state and position
applyButtonPosition(buttonPosition);
const isHidden = await getBubbleHiddenState();
bubble.style.display = isHidden ? 'none' : 'flex';
showBubbleButton.style.display = isHidden ? 'block' : 'none';
// --- Event Listeners ---
// NEW: Keyboard shortcut to toggle the main UI
document.addEventListener('keydown', async (event) => {
// Check for Ctrl + Alt + U
if (event.ctrlKey && event.altKey && event.key === 'u') {
const isVisible = mainUI.style.display === 'flex';
if (isVisible) {
mainUI.style.display = 'none';
} else {
const links = await getLinks();
const excluded = await getExcludedDomains();
populateLinkList(links, mainUI.querySelector('#linkList'));
populateExcludeList(excluded, mainUI.querySelector('#excludeList'));
mainUI.style.display = 'flex';
}
event.preventDefault(); // Prevents the browser's default action
}
});
// Bubble click to toggle the mini-menu
bubble.addEventListener('click', async () => {
const isVisible = bubbleMenu.style.display === 'flex';
bubbleMenu.style.display = isVisible ? 'none' : 'flex';
});
// Triple-click logic for bubble
let bubbleClickCount = 0;
let bubbleClickTimer = null;
bubble.addEventListener('click', () => {
bubbleClickCount++;
if (bubbleClickTimer) clearTimeout(bubbleClickTimer);
bubbleClickTimer = setTimeout(() => {
bubbleClickCount = 0;
}, 300);
if (bubbleClickCount === 3) {
clearTimeout(bubbleClickTimer);
bubble.style.display = 'none';
showBubbleButton.style.display = 'block';
mainUI.style.display = 'none';
bubbleMenu.style.display = 'none';
saveBubbleHiddenState(true);
bubbleClickCount = 0;
}
});
// Triple-click logic for showBubbleButton
let restoreClickCount = 0;
let restoreClickTimer = null;
showBubbleButton.addEventListener('click', () => {
restoreClickCount++;
if (restoreClickTimer) clearTimeout(restoreClickTimer);
restoreClickTimer = setTimeout(() => {
restoreClickCount = 0;
}, 400);
if (restoreClickCount === 3) {
clearTimeout(restoreClickTimer);
bubble.style.display = 'flex';
showBubbleButton.style.display = 'none';
saveBubbleHiddenState(false);
restoreClickCount = 0;
}
});
// Instant Add Button
bubbleMenu.querySelector('#instantAddButton').addEventListener('click', async () => {
const title = document.title;
const url = window.location.href;
const links = await getLinks();
links.push({ label: title, url: url });
await saveLinks(links);
bubbleMenu.style.display = 'none';
const excluded = await getExcludedDomains();
populateLinkList(links, mainUI.querySelector('#linkList'));
populateExcludeList(excluded, mainUI.querySelector('#excludeList'));
mainUI.style.display = 'flex';
});
// Show Full Menu Button
bubbleMenu.querySelector('#showFullMenuButton').addEventListener('click', async () => {
bubbleMenu.style.display = 'none';
const links = await getLinks();
const excluded = await getExcludedDomains();
populateLinkList(links, mainUI.querySelector('#linkList'));
populateExcludeList(excluded, mainUI.querySelector('#excludeList'));
mainUI.style.display = 'flex';
});
mainUI.querySelector('#closeUI').addEventListener('click', () => {
mainUI.style.display = 'none';
});
mainUI.querySelector('#hideButton').addEventListener('click', () => {
bubble.style.display = 'none';
showBubbleButton.style.display = 'block';
mainUI.style.display = 'none';
bubbleMenu.style.display = 'none';
saveBubbleHiddenState(true);
});
mainUI.querySelector('#saveLinkButton').addEventListener('click', async () => {
const labelInput = mainUI.querySelector('#linkLabel');
const urlInput = mainUI.querySelector('#linkUrl');
const label = labelInput.value.trim();
const url = urlInput.value.trim();
if (label && url) {
try {
new URL(url);
const links = await getLinks();
links.push({ label, url });
await saveLinks(links);
populateLinkList(links, mainUI.querySelector('#linkList'));
labelInput.value = '';
urlInput.value = '';
} catch (e) {
alert('Please enter a valid URL (e.g., https://example.com).');
}
} else {
alert('Please enter both a label and a URL.');
}
});
mainUI.querySelector('#deleteLinksButton').addEventListener('click', async () => {
isDeleteMode = !isDeleteMode;
const deleteButton = mainUI.querySelector('#deleteLinksButton');
deleteButton.textContent = isDeleteMode ? 'Exit Delete' : 'Delete Links';
const links = await getLinks();
populateLinkList(links, mainUI.querySelector('#linkList'));
});
mainUI.querySelector('#themeButton').addEventListener('click', async () => {
const currentTheme = await getTheme();
const newTheme = currentTheme === 'default' ? 'highContrast' : 'default';
await saveTheme(newTheme);
applyTheme(newTheme);
const themeButton = mainUI.querySelector('#themeButton');
themeButton.textContent = newTheme === 'default' ? 'Theme' : 'Default';
});
mainUI.querySelector('#saveExcludeButton').addEventListener('click', async () => {
const domainInput = mainUI.querySelector('#excludeUrl');
const domain = domainInput.value.trim();
if (domain) {
const excluded = await getExcludedDomains();
if (!excluded.includes(domain)) {
excluded.push(domain);
await saveExcludedDomains(excluded);
populateExcludeList(excluded, mainUI.querySelector('#excludeList'));
domainInput.value = '';
} else {
alert('This domain is already on the exclusion list.');
}
} else {
alert('Please enter a domain to exclude.');
}
});
mainUI.querySelector('#deleteExcludeButton').addEventListener('click', async () => {
isExcludeDeleteMode = !isExcludeDeleteMode;
const deleteExcludeButton = mainUI.querySelector('#deleteExcludeButton');
deleteExcludeButton.textContent = isExcludeDeleteMode ? 'Exit Exclude Delete' : 'Delete Excludes';
const excluded = await getExcludedDomains();
populateExcludeList(excluded, mainUI.querySelector('#excludeList'));
});
mainUI.querySelector('#positionControls').querySelectorAll('button').forEach(button => {
button.addEventListener('click', async (event) => {
const id = event.target.id;
const parts = id.split('-');
const newPosition = { vertical: parts[1], horizontal: parts[2] };
await saveButtonPosition(newPosition);
applyButtonPosition(newPosition);
});
});
mainUI.querySelector('#exportWrapper').addEventListener('click', async (event) => {
const button = event.target;
if (button.id === 'exportButton') {
if (button.textContent === 'Export') {
const links = await getLinks();
const exportData = JSON.stringify(links, null, 2);
mainUI.querySelector('#exportWrapper').innerHTML = `
<textarea readonly style="width:100%; height:100px;">${exportData}</textarea>
<button id="exportButton" class="modal-button">Close</button>
`;
} else {
mainUI.querySelector('#exportWrapper').innerHTML = `<button id="exportButton" class="modal-button">Export</button>`;
}
}
});
mainUI.querySelector('#importWrapper').addEventListener('click', async (event) => {
const button = event.target;
if (button.id === 'importButton') {
if (button.textContent === 'Import') {
mainUI.querySelector('#importWrapper').innerHTML = `
<textarea placeholder="Paste your link data here..." style="width:100%; height:100px;"></textarea>
<div class="import-buttons">
<button id="loadButton" class="modal-button">Load</button>
<button id="importButton" class="modal-button">Cancel</button>
</div>
`;
} else {
mainUI.querySelector('#importWrapper').innerHTML = `<button id="importButton" class="modal-button">Import</button>`;
}
} else if (button.id === 'loadButton') {
const dataTextarea = mainUI.querySelector('textarea');
if (dataTextarea) {
const data = dataTextarea.value.trim();
if (!data) {
alert('Please paste the link data.');
return;
}
try {
const importedLinks = JSON.parse(data);
if (Array.isArray(importedLinks)) {
await saveLinks(importedLinks);
const links = await getLinks();
populateLinkList(links, mainUI.querySelector('#linkList'));
alert('Links imported successfully!');
mainUI.querySelector('#importWrapper').innerHTML = `<button id="importButton" class="modal-button">Import</button>`;
} else {
alert('Invalid data format. Please paste the correct JSON data.');
}
} catch (e) {
alert('Invalid JSON format. Error: ' + e.message);
}
}
}
});
}
if (document.body) {
initializeScript();
} else {
document.addEventListener('DOMContentLoaded', initializeScript);
}
})();