Font Customizer

Customize fonts for any website through the Tampermonkey menu

Ajankohdalta 16.3.2025. Katso uusin versio.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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();
})();