Visited Links Enhanced - Flat UI

Minimalist flat UI userscript for visited links customization. Customize visited link colors with a clean, modern interface and site-specific exceptions.

当前为 2025-09-14 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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