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 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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