Font Customizer

Customize fonts for any website through the Tampermonkey menu

Versión del día 16/3/2025. Echa un vistazo a la versión más reciente.

// ==UserScript==
// @name         Font Customizer
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Customize fonts for any website through the Tampermonkey menu
// @author       Cursor, claude-3.7, and me(qooo).
// @license      MIT
// @match        *://*/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_addStyle
// ==/UserScript==

(function () {
  "use strict";

  // Storage keys
  const STORAGE_KEY_PREFIX = "fontCustomizer_";
  const ENABLED_SUFFIX = "_enabled";
  const FONT_SUFFIX = "_font";
  const FONT_LIST_KEY = "fontCustomizer_savedFonts";

  // Default font options
  const DEFAULT_FONTS = [
    "Arial",
    "Verdana",
    "Helvetica",
    "Times New Roman",
    "Courier New",
    "Georgia",
    "Tahoma",
    "Trebuchet MS",
    "Segoe UI",
    "Roboto",
    "Open Sans",
    "Custom...",
  ];

  // Get saved fonts or initialize with empty array
  function getSavedFonts() {
    const savedFonts = localStorage.getItem(FONT_LIST_KEY);
    return savedFonts ? JSON.parse(savedFonts) : [];
  }

  // Save a font to the list if it doesn't exist already
  function saveFontToList(font) {
    const fonts = getSavedFonts();
    if (!fonts.includes(font)) {
      fonts.push(font);
      localStorage.setItem(FONT_LIST_KEY, JSON.stringify(fonts));
    }
  }

  // Remove a font from the saved list
  function removeFontFromList(font) {
    const fonts = getSavedFonts();
    const index = fonts.indexOf(font);
    if (index !== -1) {
      fonts.splice(index, 1);
      localStorage.setItem(FONT_LIST_KEY, JSON.stringify(fonts));
    }
  }

  // Get current hostname
  const hostname = window.location.hostname;

  // Storage helper functions
  function getStorageKey(suffix) {
    return STORAGE_KEY_PREFIX + hostname + suffix;
  }

  function isEnabledForSite() {
    return localStorage.getItem(getStorageKey(ENABLED_SUFFIX)) === "true";
  }

  function setEnabledForSite(enabled) {
    localStorage.setItem(getStorageKey(ENABLED_SUFFIX), enabled.toString());
  }

  function getFontForSite() {
    return localStorage.getItem(getStorageKey(FONT_SUFFIX)) || DEFAULT_FONTS[0];
  }

  function setFontForSite(font) {
    localStorage.setItem(getStorageKey(FONT_SUFFIX), font);
  }

  // Apply font to the website
  function applyFont() {
    if (isEnabledForSite()) {
      const font = getFontForSite();
      GM_addStyle(`
                * {
                    font-family: "${font}" !important;
                }
            `);
    }
  }

  // Remove applied font styles
  function removeAppliedFont() {
    // Create a unique ID for our style element
    const styleId = "font-customizer-styles";

    // Remove existing style element if it exists
    const existingStyle = document.getElementById(styleId);
    if (existingStyle) {
      existingStyle.remove();
    }

    // Re-apply styles for other elements that might need them
    applyStyles();
  }

  // Apply all necessary styles
  function applyStyles() {
    // If enabled, apply the font
    if (isEnabledForSite()) {
      const font = getFontForSite();
      const styleElement = document.createElement("style");
      styleElement.id = "font-customizer-styles";
      styleElement.textContent = `
                * {
                    font-family: "${font}" !important;
                }
            `;
      document.head.appendChild(styleElement);
    }
  }

  // Menu command IDs
  let toggleCommandId = null;
  let fontCommandId = null;

  // Register menu commands
  function registerMenuCommands() {
    // Unregister existing commands
    if (toggleCommandId !== null) {
      GM_unregisterMenuCommand(toggleCommandId);
    }
    if (fontCommandId !== null) {
      GM_unregisterMenuCommand(fontCommandId);
    }

    // Register toggle command with status indicator
    const enabled = isEnabledForSite();
    const toggleText = enabled
      ? "🟢 Font Customizer: Enabled"
      : "🔴 Font Customizer: Disabled";

    toggleCommandId = GM_registerMenuCommand(toggleText, function () {
      const newEnabledState = !enabled;
      setEnabledForSite(newEnabledState);

      if (newEnabledState) {
        // If enabling, apply font immediately
        applyStyles();
      } else {
        // If disabling, remove applied font
        removeAppliedFont();
      }

      // Update menu commands
      registerMenuCommands();
    });

    // Register font selection command
    const currentFont = getFontForSite();
    fontCommandId = GM_registerMenuCommand(
      `Select Font (Current: ${currentFont})`,
      showFontSelector
    );
  }

  // Create and show font selector popup
  function showFontSelector() {
    // Remove existing popup if any
    const existingPopup = document.getElementById("font-customizer-popup");
    if (existingPopup) {
      existingPopup.remove();
    }

    // Create popup container
    const popup = document.createElement("div");
    popup.id = "font-customizer-popup";

    // Add styles for the popup
    GM_addStyle(`
            #font-customizer-popup {
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                background-color: var(--popup-bg, #ffffff);
                color: var(--popup-text, #000000);
                border: 1px solid var(--popup-border, #cccccc);
                border-radius: 8px;
                padding: 24px;
                z-index: 9999;
                width: 30dvw;
                height: fit-content;
                overflow-y: auto;
                box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
                font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
                animation: popup-fade-in 0.2s ease-out;
            }

            @keyframes popup-fade-in {
                from { opacity: 0; transform: translate(-50%, -48%); }
                to { opacity: 1; transform: translate(-50%, -50%); }
            }

            #font-customizer-popup h2 {
                margin-top: 0;
                margin-bottom: 20px;
                font-size: 20px;
                font-weight: 600;
                text-align: center;
                color: var(--popup-title, inherit);
            }

            #font-customizer-popup .font-input-container {
                margin-bottom: 16px;
            }

            #font-customizer-popup .font-input {
                width: 100%;
                padding: 12px;
                border: 1px solid var(--popup-border, #cccccc);
                border-radius: 8px;
                box-sizing: border-box;
                font-size: 14px;
                transition: border-color 0.2s;
                margin-bottom: 8px;
            }

            #font-customizer-popup .font-input:focus {
                border-color: var(--popup-button, #4a86e8);
                outline: none;
                box-shadow: 0 0 0 2px rgba(74, 134, 232, 0.2);
            }

            #font-customizer-popup .add-font-button {
                display: block;
                width: 100%;
                padding: 8px 16px;
                background-color: var(--popup-button, #4a86e8);
                color: white;
                border: none;
                border-radius: 8px;
                cursor: pointer;
                font-weight: 600;
                font-size: 14px;
                transition: background-color 0.2s, transform 0.1s;
            }

            #font-customizer-popup .add-font-button:hover {
                background-color: var(--popup-button-hover, #3b78e7);
            }

            #font-customizer-popup .add-font-button:active {
                transform: scale(0.98);
            }

            #font-customizer-popup .saved-fonts-title {
                font-size: 16px;
                font-weight: 600;
                margin: 16px 0 8px 0;
                color: var(--popup-title, inherit);
            }

            #font-customizer-popup .no-fonts-message {
                color: var(--popup-text-secondary, #666666);
                font-style: italic;
                text-align: center;
                padding: 16px 0;
            }

            #font-customizer-popup ul {
                list-style: none;
                padding: 0;
                margin: 0 0 16px 0;
                max-height: 200px;
                overflow-y: auto;
                border-radius: 8px;
                border: 1px solid var(--popup-border, #eaeaea);
            }

            #font-customizer-popup ul:empty {
                display: none;
            }

            #font-customizer-popup ul::-webkit-scrollbar {
                width: 8px;
            }

            #font-customizer-popup ul::-webkit-scrollbar-track {
                background: var(--popup-scrollbar-track, #f1f1f1);
                border-radius: 0 8px 8px 0;
            }

            #font-customizer-popup ul::-webkit-scrollbar-thumb {
                background: var(--popup-scrollbar-thumb, #c1c1c1);
                border-radius: 4px;
            }

            #font-customizer-popup ul::-webkit-scrollbar-thumb:hover {
                background: var(--popup-scrollbar-thumb-hover, #a1a1a1);
            }

            #font-customizer-popup li {
                padding: 4px 16px;
                cursor: pointer;
                transition: all 0.15s ease;
                border-bottom: 1px solid var(--popup-border, #eaeaea);
                display: flex;
                align-items: center;
                justify-content: space-between;
            }

            #font-customizer-popup li:last-child {
                border-bottom: none;
            }

            #font-customizer-popup li:hover {
                background-color: var(--popup-hover, #f5f5f5);
            }

            #font-customizer-popup li.selected {
                background-color: var(--popup-selected, #e8f0fe);
                font-weight: 500;
            }

            #font-customizer-popup li.selected .font-name::before {
                content: "✓";
                margin-right: 8px;
                color: var(--popup-check, #4a86e8);
                font-weight: bold;
            }

            #font-customizer-popup li:not(.selected) .font-name {
                padding-left: 24px; /* Align with selected items that have checkmark */
            }

            #font-customizer-popup .font-actions {
                display: flex;
                opacity: 0;
                transition: opacity 0.2s;
            }

            #font-customizer-popup li:hover .font-actions {
                opacity: 1;
            }

            #font-customizer-popup .delete-font {
                color: var(--popup-delete, #e53935);
                cursor: pointer;
                font-size: 16px;
                padding: 4px;
                border-radius: 4px;
                transition: background-color 0.2s;
            }

            #font-customizer-popup .delete-font:hover {
                background-color: var(--popup-delete-hover, rgba(229, 57, 53, 0.1));
            }

            #font-customizer-popup .close-button {
                display: block;
                width: 100%;
                margin: 16px auto 0;
                padding: 12px 16px;
                background-color: var(--popup-button-secondary, #757575);
                color: white;
                border: none;
                border-radius: 8px;
                cursor: pointer;
                font-weight: 600;
                font-size: 15px;
                transition: background-color 0.2s, transform 0.1s;
            }

            #font-customizer-popup .close-button:hover {
                background-color: var(--popup-button-secondary-hover, #616161);
            }

            #font-customizer-popup .close-button:active {
                transform: scale(0.98);
            }

            /* Dark mode detection and styles */
            @media (prefers-color-scheme: dark) {
                #font-customizer-popup {
                    --popup-bg: #222222;
                    --popup-text: #ffffff;
                    --popup-text-secondary: #aaaaaa;
                    --popup-title: #ffffff;
                    --popup-border: #444444;
                    --popup-hover: #333333;
                    --popup-selected: #2c3e50;
                    --popup-check: #64b5f6;
                    --popup-button: #4a86e8;
                    --popup-button-hover: #3b78e7;
                    --popup-button-secondary: #616161;
                    --popup-button-secondary-hover: #757575;
                    --popup-delete: #f44336;
                    --popup-delete-hover: rgba(244, 67, 54, 0.2);
                    --popup-scrollbar-track: #333333;
                    --popup-scrollbar-thumb: #555555;
                    --popup-scrollbar-thumb-hover: #666666;
                }
            }

            /* Light mode styles */
            @media (prefers-color-scheme: light) {
                #font-customizer-popup {
                    --popup-bg: #ffffff;
                    --popup-text: #333333;
                    --popup-text-secondary: #666666;
                    --popup-title: #222222;
                    --popup-border: #eaeaea;
                    --popup-hover: #f5f5f5;
                    --popup-selected: #e8f0fe;
                    --popup-check: #4a86e8;
                    --popup-button: #4a86e8;
                    --popup-button-hover: #3b78e7;
                    --popup-button-secondary: #757575;
                    --popup-button-secondary-hover: #616161;
                    --popup-delete: #e53935;
                    --popup-delete-hover: rgba(229, 57, 53, 0.1);
                    --popup-scrollbar-track: #f1f1f1;
                    --popup-scrollbar-thumb: #c1c1c1;
                    --popup-scrollbar-thumb-hover: #a1a1a1;
                }
            }

            /* Overlay to prevent clicking outside */
            #font-customizer-overlay {
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                background: rgba(0, 0, 0, 0.5);
                z-index: 9998;
                animation: overlay-fade-in 0.2s ease-out;
            }

            @keyframes overlay-fade-in {
                from { opacity: 0; }
                to { opacity: 1; }
            }
        `);

    // Create overlay to prevent clicking outside
    const overlay = document.createElement("div");
    overlay.id = "font-customizer-overlay";
    document.body.appendChild(overlay);

    // Create popup content
    popup.innerHTML = `
            <h2>Font Customizer</h2>
            <div class="font-input-container">
                <input type="text" id="new-font-input" class="font-input" placeholder="Enter font name (e.g., Arial, sans-serif)">
                <button id="add-font-button" class="add-font-button">Add & Apply Font</button>
            </div>
            <div class="saved-fonts-title">Your Saved Fonts</div>
            <ul id="font-list"></ul>
            <div id="no-fonts-message" class="no-fonts-message">No saved fonts yet. Add one above!</div>
            <button class="close-button" id="close-popup">Close</button>
        `;

    document.body.appendChild(popup);

    // Get current font and saved fonts
    const currentFont = getFontForSite();
    const savedFonts = getSavedFonts();

    // Populate font list
    const fontList = document.getElementById("font-list");
    const noFontsMessage = document.getElementById("no-fonts-message");

    // Show/hide no fonts message
    if (savedFonts.length === 0) {
      noFontsMessage.style.display = "block";
    } else {
      noFontsMessage.style.display = "none";
    }

    // Add saved fonts to the list
    savedFonts.forEach((font) => {
      addFontToList(font);
    });

    // Function to add a font to the list
    function addFontToList(font) {
      const li = document.createElement("li");
      li.innerHTML = `
                <span class="font-name">${font}</span>
                <div class="font-actions">
                    <span class="delete-font" title="Remove font">🗑️</span>
                </div>
            `;

      if (font === currentFont) {
        li.classList.add("selected");
      }

      // Select font when clicked
      li.addEventListener("click", (e) => {
        // Ignore if delete button was clicked
        if (e.target.classList.contains("delete-font")) {
          return;
        }

        // Remove selected class from all items
        document.querySelectorAll("#font-list li").forEach((item) => {
          item.classList.remove("selected");
        });

        // Add selected class to clicked item
        li.classList.add("selected");

        // Set the selected font
        setFontForSite(font);

        // Apply the font if enabled
        if (isEnabledForSite()) {
          removeAppliedFont(); // Remove old font styles
          applyStyles(); // Apply new font styles
        }

        // Update menu commands
        registerMenuCommands();
      });

      // Delete font when delete button is clicked
      const deleteButton = li.querySelector(".delete-font");
      deleteButton.addEventListener("click", (e) => {
        e.stopPropagation(); // Prevent triggering the li click event

        // Remove font from saved list
        removeFontFromList(font);

        // Remove the list item
        li.remove();

        // If this was the current font, reset to default
        if (font === currentFont) {
          // If there are other fonts, select the first one
          const remainingFonts = getSavedFonts();
          if (remainingFonts.length > 0) {
            setFontForSite(remainingFonts[0]);

            // Select the first font in the list
            const firstFont = document.querySelector("#font-list li");
            if (firstFont) {
              firstFont.classList.add("selected");
            }
          } else {
            // No fonts left, reset to system default
            setFontForSite("");
          }

          // Apply changes if enabled
          if (isEnabledForSite()) {
            removeAppliedFont();
            applyStyles();
          }

          // Update menu commands
          registerMenuCommands();
        }

        // Show/hide no fonts message
        if (fontList.children.length === 0) {
          noFontsMessage.style.display = "block";
        }
      });

      fontList.appendChild(li);
    }

    // Handle new font input
    const newFontInput = document.getElementById("new-font-input");
    const addFontButton = document.getElementById("add-font-button");

    // Focus the input field
    newFontInput.focus();

    // Add font when button is clicked
    addFontButton.addEventListener("click", addNewFont);

    // Add font when Enter is pressed
    newFontInput.addEventListener("keydown", (e) => {
      if (e.key === "Enter") {
        addNewFont();
      }
    });

    // Function to add a new font
    function addNewFont() {
      const fontName = newFontInput.value.trim();
      if (fontName) {
        // Save font to list
        saveFontToList(fontName);

        // Clear the input
        newFontInput.value = "";

        // Hide no fonts message
        noFontsMessage.style.display = "none";

        // Remove existing font from list if it exists
        const existingFont = document.querySelector(
          `#font-list li .font-name[data-font="${fontName}"]`
        );
        if (existingFont) {
          existingFont.closest("li").remove();
        }

        // Add to the list
        addFontToList(fontName);

        // Set as current font
        setFontForSite(fontName);

        // Remove selected class from all items
        document.querySelectorAll("#font-list li").forEach((item) => {
          item.classList.remove("selected");
        });

        // Select the new font
        const newFontElement = Array.from(
          document.querySelectorAll("#font-list li")
        ).find((li) => li.querySelector(".font-name").textContent === fontName);
        if (newFontElement) {
          newFontElement.classList.add("selected");
        }

        // Apply the font if enabled
        if (isEnabledForSite()) {
          removeAppliedFont();
          applyStyles();
        }

        // Update menu commands
        registerMenuCommands();
      }
    }

    // Function to close the popup
    function closePopup() {
      document.getElementById("font-customizer-popup").remove();
      document.getElementById("font-customizer-overlay").remove();
    }

    // Close button functionality
    document
      .getElementById("close-popup")
      .addEventListener("click", closePopup);

    // Prevent closing when clicking on the popup itself
    popup.addEventListener("click", (e) => {
      e.stopPropagation();
    });

    // Prevent keyboard shortcuts from closing the popup
    document.addEventListener("keydown", function preventEscape(e) {
      if (e.key === "Escape") {
        e.stopPropagation();
        e.preventDefault();
      }

      // Remove this event listener when popup is closed
      if (!document.getElementById("font-customizer-popup")) {
        document.removeEventListener("keydown", preventEscape);
      }
    });
  }

  // Initialize
  function init() {
    registerMenuCommands();
    applyStyles(); // Use the new function instead of applyFont

    // Add mutation observer to handle dynamically added content
    const observer = new MutationObserver(function (mutations) {
      // If we're not enabled, don't do anything
      if (!isEnabledForSite()) return;

      // Check if our style element still exists
      const styleElement = document.getElementById("font-customizer-styles");
      if (!styleElement) {
        // If it doesn't, reapply our styles
        applyStyles();
      }
    });

    // Start observing the document with the configured parameters
    observer.observe(document.documentElement, {
      childList: true,
      subtree: true,
    });
  }

  // Run the script
  init();
})();