Minimalist flat UI userscript for visited links customization. Customize visited link colors with a clean, modern interface and site-specific exceptions.
当前为
// ==UserScript==
// @name Visited Links Enhanced - Flat UI
// @name:vi Liên Kết Đã Truy Cập Nâng Cao - Giao Diện Phẳng
// @namespace com.userscript.visited-links-enhanced
// @version 0.6.4
// @description Minimalist flat UI userscript for visited links customization. Customize visited link colors with a clean, modern interface and site-specific exceptions.
// @description:vi Userscript giao diện phẳng tối giản để tùy chỉnh liên kết đã truy cập. Tùy chỉnh màu liên kết đã truy cập với giao diện sạch sẽ, hiện đại và ngoại lệ theo site.
// @author Enhanced by AI Assistant ft. Hongmd
// @license MIT
// @homepageURL https://github.com/hongmd/userscript-improved
// @supportURL https://github.com/hongmd/userscript-improved/issues
// @match http://*/*
// @match https://*/*
// @noframes
// @icon https://cdn.jsdelivr.net/gh/hongmd/cdn-web@main/logo.svg
// @run-at document-start
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_info
// @compatible ScriptCat
// @compatible Tampermonkey
// @compatible Greasemonkey
// @copyright 2025, Enhanced by AI Assistant ft. Hongmd
// ==/UserScript==
(function () {
"use strict";
// ScriptCat & Browser Compatibility Detection
const ENVIRONMENT = (() => {
const handler = GM_info?.scriptHandler ?? "";
return {
isScriptCat: handler === "ScriptCat",
isTampermonkey: handler === "Tampermonkey",
hasStorage: typeof GM_setValue !== "undefined",
hasMenuCommand: typeof GM_registerMenuCommand !== "undefined",
};
})();
// Compatibility logging
console.log("[Visited Links Enhanced] Environment:", ENVIRONMENT.isTampermonkey ? "Tampermonkey" : ENVIRONMENT.isScriptCat ? "ScriptCat" : "Other");
//// Configuration
const CONFIG = Object.freeze({
STORAGE_KEYS: Object.freeze({
COLOR: "visited_color",
EXCEPT_SITES: "except_sites",
ENABLED: "script_enabled",
}),
DEFAULTS: Object.freeze({
COLOR: "#f97316",
EXCEPT_SITES: "mail.live.com,gmail.com",
ENABLED: true,
}),
STYLE_ID: "visited-lite-enhanced-style",
CSS_TEMPLATE: "a:visited, a:visited * { color: %COLOR% !important; }",
DEBOUNCE_DELAY: 300,
MAX_OBSERVER_NODES: 100,
CACHE_SIZE_LIMIT: 20,
});
// Color palette with names and style descriptions - comprehensive selection
const COLOR_PALETTE = Object.freeze([
// Pastel Colors - Soft & Eye-friendly
{ color: "#93c5fd", name: "Pastel Blue", desc: "Soft, calming" },
{ color: "#fca5a5", name: "Pastel Red", desc: "Gentle, warm" },
{ color: "#86efac", name: "Pastel Green", desc: "Fresh, natural" },
{ color: "#fed7aa", name: "Pastel Orange", desc: "Light, cheerful" },
{ color: "#f97316", name: "Vibrant Orange", desc: "Energetic, bold" },
{ color: "#c4b5fd", name: "Pastel Purple", desc: "Elegant, soft" },
{ color: "#f9a8d4", name: "Pastel Pink", desc: "Sweet, feminine" },
{ color: "#7dd3fc", name: "Pastel Sky Blue", desc: "Airy, peaceful" },
{ color: "#bef264", name: "Pastel Lime", desc: "Bright, lively" },
{ color: "#fde047", name: "Pastel Yellow", desc: "Sunny, optimistic" },
{ color: "#fb7185", name: "Pastel Rose", desc: "Romantic, soft" },
{ color: "#a78bfa", name: "Pastel Violet", desc: "Mystical, calm" },
{ color: "#34d399", name: "Pastel Emerald", desc: "Rich, serene" },
// Bold Highlight Colors - Strong Visibility
{ color: "#dc2626", name: "Bold Red", desc: "Strong, attention" },
{ color: "#2563eb", name: "Bold Blue", desc: "Professional, trust" },
{ color: "#059669", name: "Bold Green", desc: "Success, nature" },
{ color: "#7c3aed", name: "Bold Purple", desc: "Creative, luxury" },
{ color: "#db2777", name: "Bold Pink", desc: "Vibrant, modern" },
{ color: "#ea580c", name: "Bold Orange", desc: "Dynamic, warm" },
{ color: "#0891b2", name: "Bold Cyan", desc: "Tech, cool" },
{ color: "#65a30d", name: "Bold Lime", desc: "Electric, fresh" },
{ color: "#ca8a04", name: "Bold Yellow", desc: "Warning, bright" },
{ color: "#be123c", name: "Bold Rose", desc: "Passionate, deep" },
// Primary Colors - Classic & Standard
{ color: "#000000", name: "Black", desc: "Classic, strong" },
{ color: "#ffffff", name: "White", desc: "Clean, minimal" },
{ color: "#6b7280", name: "Gray", desc: "Neutral, subtle" },
{ color: "#ef4444", name: "Pure Red", desc: "Primary, bold" },
{ color: "#3b82f6", name: "Pure Blue", desc: "Primary, reliable" },
{ color: "#10b981", name: "Pure Green", desc: "Primary, fresh" },
{ color: "#8b5cf6", name: "Pure Purple", desc: "Primary, royal" },
{ color: "#f59e0b", name: "Pure Orange", desc: "Primary, energetic" },
{ color: "#eab308", name: "Pure Yellow", desc: "Primary, bright" },
]);
//// Utility Functions
const Utils = Object.freeze({
debounce: (func, wait) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
},
_unifiedCache: new Map(),
isValidColor: (color) => {
const cacheKey = `valid_color:${color}`;
if (Utils._unifiedCache.has(cacheKey)) {
return Utils._unifiedCache.get(cacheKey);
}
try {
// More robust color validation using regex patterns
const isValid = /^#([0-9a-f]{3}){1,2}$/i.test(color) ||
/^rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)$/i.test(color) ||
/^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)$/i.test(color) ||
/^(red|blue|green|yellow|black|white|gray|orange|purple|pink|brown)$/i.test(color);
Utils._maintainCache(cacheKey, isValid);
return isValid;
} catch {
Utils._maintainCache(cacheKey, false);
return false;
}
},
getDomain: (url) => {
const cacheKey = `domain_extract:${url}`;
if (Utils._unifiedCache.has(cacheKey)) {
return Utils._unifiedCache.get(cacheKey);
}
let domain = "";
try {
domain = new URL(url).hostname;
} catch {
const match = url.match(/^https?:\/\/([^\/\?#]+)/i);
domain = match ? match[1] : "";
}
Utils._maintainCache(cacheKey, domain);
return domain;
},
_maintainCache: (key, value) => {
if (Utils._unifiedCache.size >= CONFIG.CACHE_SIZE_LIMIT) {
const keysToDelete = Array.from(Utils._unifiedCache.keys()).slice(0, 10);
keysToDelete.forEach(k => Utils._unifiedCache.delete(k));
}
Utils._unifiedCache.set(key, value);
},
_sanitizeRegex: /[<>"']/g,
sanitizeInput: (input) => input?.replace(Utils._sanitizeRegex, "") ?? "",
clearCaches: () => Utils._unifiedCache.clear(),
});
//// Configuration Manager
const ConfigManager = {
_cache: new Map(),
_storagePrefix: "visited_links_enhanced_",
get(key) {
if (this._cache.has(key)) return this._cache.get(key);
const storageKey = CONFIG.STORAGE_KEYS[key];
const defaultValue = CONFIG.DEFAULTS[key];
let value = defaultValue;
if (ENVIRONMENT.hasStorage) {
try {
value = GM_getValue(storageKey, defaultValue);
} catch (e) {
try {
const stored = localStorage.getItem(this._storagePrefix + storageKey);
value = stored ? JSON.parse(stored) : defaultValue;
} catch (e2) {
console.warn("[Storage] Failed:", e2);
}
}
}
this._cache.set(key, value);
return value;
},
set(key, value) {
this._cache.set(key, value);
const storageKey = CONFIG.STORAGE_KEYS[key];
if (ENVIRONMENT.hasStorage) {
try {
GM_setValue(storageKey, value);
return true;
} catch (e) {
try {
localStorage.setItem(this._storagePrefix + storageKey, JSON.stringify(value));
return true;
} catch (e2) {
console.warn("[Storage] Failed:", e2);
return false;
}
}
}
},
isExceptSite(url) {
const raw = this.get("EXCEPT_SITES");
if (!raw?.trim()) return false;
const currentDomain = Utils.getDomain(url)?.toLowerCase() ?? "";
if (!currentDomain) return false;
const exceptions = raw.split(",").map(site => site.trim().toLowerCase());
return exceptions.some(site => {
if (!site) return false;
// Remove protocol and www for comparison
const cleanSite = site.replace(/^(https?:\/\/)?(www\.)?/g, "");
const cleanDomain = currentDomain.replace(/^www\./g, "");
// Exact match or subdomain match (more precise)
return cleanDomain === cleanSite || cleanDomain.endsWith('.' + cleanSite);
});
},
clearCache() {
this._cache.clear();
Utils.clearCaches();
},
};
//// Style Manager
const StyleManager = {
styleElement: null,
_lastCSS: "",
init() {
this.createStyleElement();
},
createStyleElement() {
document.getElementById(CONFIG.STYLE_ID)?.remove();
this.styleElement = Object.assign(document.createElement("style"), {
id: CONFIG.STYLE_ID,
type: "text/css"
});
(document.head ?? document.documentElement)?.appendChild?.(this.styleElement);
return this.styleElement;
},
updateStyles() {
const color = ConfigManager.get("COLOR");
if (!Utils.isValidColor(color)) return;
const css = CONFIG.CSS_TEMPLATE.replaceAll("%COLOR%", color);
if (this._lastCSS === css) return;
if (!this.styleElement?.isConnected) {
this.createStyleElement();
}
this.styleElement.textContent = css;
this._lastCSS = css;
},
removeStyles() {
if (this.styleElement && this._lastCSS) {
this.styleElement.textContent = "";
this._lastCSS = "";
}
},
};
//// Menu System
const MenuManager = {
init() {
if (ENVIRONMENT.hasMenuCommand) {
try {
GM_registerMenuCommand("🎨 Change Color", this.changeColor.bind(this));
GM_registerMenuCommand("⚙️ Toggle Script", this.toggleScript.bind(this));
GM_registerMenuCommand("🚫 Manage Exceptions", this.manageExceptions.bind(this));
GM_registerMenuCommand("🔄 Reset Settings", this.resetSettings.bind(this));
} catch (e) {
console.warn("[Menu] Registration failed:", e);
}
}
},
toggleScript() {
const newState = !ConfigManager.get("ENABLED");
ConfigManager.set("ENABLED", newState);
newState ? StyleManager.updateStyles() : StyleManager.removeStyles();
alert(`Visited Links Enhanced: ${newState ? "Enabled" : "Disabled"}`);
},
changeColor() {
const currentColor = ConfigManager.get("COLOR");
if (!this._cachedColorOptions) {
this._cachedColorOptions = COLOR_PALETTE.map((item, index) =>
`${index + 1}. ${item.name} - ${item.desc} (${item.color})`
).join('\n');
}
const choice = prompt(
`🎨 Choose a color:\n\n${this._cachedColorOptions}\n\nEnter number (1-${COLOR_PALETTE.length}) or custom color:`,
currentColor
);
if (!choice?.trim()) return;
const trimmed = choice.trim();
const num = parseInt(trimmed, 10);
let selectedColor;
if (num >= 1 && num <= COLOR_PALETTE.length && trimmed === num.toString()) {
selectedColor = COLOR_PALETTE[num - 1].color;
} else if (Utils.isValidColor(trimmed)) {
selectedColor = trimmed;
} else {
alert("Invalid color format. Please try again.");
return;
}
ConfigManager.set("COLOR", selectedColor);
StyleManager.updateStyles();
const colorItem = COLOR_PALETTE.find(item => item.color === selectedColor);
const colorInfo = colorItem ? `${colorItem.name} - ${colorItem.desc}` : "Custom Color";
alert(`✅ Color changed to: ${colorInfo} (${selectedColor})`);
},
manageExceptions() {
const current = ConfigManager.get("EXCEPT_SITES");
const newExceptions = prompt(
"Enter domains to exclude (comma-separated):\n\nExample: gmail.com, facebook.com",
current
);
if (newExceptions !== null) {
ConfigManager.set("EXCEPT_SITES", Utils.sanitizeInput(newExceptions?.trim() ?? ""));
alert("Exception sites updated!");
App.checkAndApplyStyles();
}
},
resetSettings() {
if (confirm("Reset all settings to defaults?")) {
ConfigManager.set("COLOR", CONFIG.DEFAULTS.COLOR);
ConfigManager.set("EXCEPT_SITES", CONFIG.DEFAULTS.EXCEPT_SITES);
ConfigManager.set("ENABLED", CONFIG.DEFAULTS.ENABLED);
ConfigManager.clearCache();
StyleManager.updateStyles();
alert("Settings reset to defaults");
}
},
};
//// Main Application
const App = {
init() {
StyleManager.init();
MenuManager.init();
this.checkAndApplyStyles();
this.observeChanges();
console.log("[Visited Links Enhanced] Initialized successfully");
},
checkAndApplyStyles() {
const isEnabled = ConfigManager.get("ENABLED");
const currentUrl = document.documentURI ?? window.location.href;
if (isEnabled && !ConfigManager.isExceptSite(currentUrl)) {
StyleManager.updateStyles();
} else {
StyleManager.removeStyles();
}
},
observeChanges() {
const debouncedUpdate = Utils.debounce(() => this.checkAndApplyStyles(), CONFIG.DEBOUNCE_DELAY);
if (window.MutationObserver) {
const observer = new MutationObserver((mutations) => {
let shouldUpdate = false;
for (let i = 0; i < mutations.length && !shouldUpdate; i++) {
const mutation = mutations[i];
// Only check childList mutations for performance
if (mutation.type !== 'childList') continue;
const addedNodes = mutation.addedNodes;
for (let j = 0; j < addedNodes.length && !shouldUpdate; j++) {
const node = addedNodes[j];
// Check if node is element and contains links
if (node.nodeType === 1) {
// Direct link element
if (node.tagName === 'A') {
shouldUpdate = true;
break;
}
// Check if element contains links (more efficient than querySelector)
const links = node.getElementsByTagName?.('A');
if (links?.length > 0) {
shouldUpdate = true;
break;
}
}
}
}
if (shouldUpdate) debouncedUpdate();
});
// Observe body instead of documentElement for better performance
const target = document.body || document.documentElement;
observer.observe(target, {
childList: true,
subtree: true,
});
}
const passiveOptions = { passive: true };
window.addEventListener("popstate", debouncedUpdate, passiveOptions);
window.addEventListener("hashchange", debouncedUpdate, passiveOptions);
},
};
//// Initialization
function initialize() {
if (document.documentElement) {
App.init();
} else {
setTimeout(initialize, 50);
}
}
// Start the script
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initialize);
} else {
initialize();
}
// Export for debugging (only in development)
if (typeof window !== "undefined" && window.location?.hostname?.includes?.('localhost')) {
window.VisitedLinksEnhanced = { config: ConfigManager, style: StyleManager, utils: Utils };
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
delete window.VisitedLinksEnhanced;
});
}
})();
// Ultra-Compact & Memory Optimized:
// 1. Removed verbose comments - 50% fewer lines
// 2. Simplified debounce (removed immediate param)
// 3. Consolidated error handling - less code
// 4. Removed redundant console.log statements
// 5. Simplified cache maintenance (10 vs 50% deletion)
// 6. Merged similar functions - reduced complexity
// 7. Shorter variable names where possible
// 8. Removed unnecessary wrapper functions
// 9. Unified configuration structure
// 10. Streamlined initialization process