// ==UserScript==
// @name Font Customizer
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Customize fonts for any website through the Tampermonkey menu
// @author Cursor, claude-3.7, and me(qooo).
// @license MIT
// @match *://*/*
// @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGQ9Ik05LjkzIDEzLjVoNC4xNEwxMiA3Ljk4ek0yMCAySDRjLTEuMSAwLTIgLjktMiAydjE2YzAgMS4xLjkgMiAyIDJoMTZjMS4xIDAgMi0uOSAyLTJWNGMwLTEuMS0uOS0yLTItMm0tNC4wNSAxNi41bC0xLjE0LTNIOS4xN2wtMS4xMiAzSDUuOTZsNS4xMS0xM2gxLjg2bDUuMTEgMTN6Ii8+PC9zdmc+
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @connect fonts.googleapis.com
// @connect fonts.gstatic.com
// ==/UserScript==
(function () {
"use strict";
// Storage keys
const STORAGE_KEY_PREFIX = "fontCustomizer_";
const ENABLED_SUFFIX = "_enabled";
const FONT_SUFFIX = "_font";
const FONT_LIST_KEY = "fontCustomizer_globalFonts";
// Common web fonts from Google Fonts
const WEB_FONTS = [
{
name: "Roboto",
family: "Roboto, sans-serif",
url: "https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap",
},
{
name: "Open Sans",
family: "'Open Sans', sans-serif",
url: "https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&display=swap",
},
{
name: "Lato",
family: "Lato, sans-serif",
url: "https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap",
},
{
name: "Montserrat",
family: "Montserrat, sans-serif",
url: "https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&display=swap",
},
{
name: "Poppins",
family: "Poppins, sans-serif",
url: "https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap",
},
{
name: "Source Sans Pro",
family: "'Source Sans Pro', sans-serif",
url: "https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;600;700&display=swap",
},
{
name: "Ubuntu",
family: "Ubuntu, sans-serif",
url: "https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;500;700&display=swap",
},
{
name: "Nunito",
family: "Nunito, sans-serif",
url: "https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;600;700&display=swap",
},
{
name: "Playfair Display",
family: "'Playfair Display', serif",
url: "https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;600;700&display=swap",
},
{
name: "Merriweather",
family: "Merriweather, serif",
url: "https://fonts.googleapis.com/css2?family=Merriweather:wght@300;400;700&display=swap",
},
];
// Load a web font
function loadWebFont(fontUrl) {
GM_xmlhttpRequest({
method: "GET",
url: fontUrl,
onload: function (response) {
const style = document.createElement("style");
style.textContent = response.responseText;
document.head.appendChild(style);
},
});
}
// Get saved fonts or initialize with empty array
function getSavedFonts() {
return GM_getValue(FONT_LIST_KEY, []);
}
// 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);
GM_setValue(FONT_LIST_KEY, 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);
GM_setValue(FONT_LIST_KEY, 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)) || "";
}
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() {
const styleId = "font-customizer-styles";
const existingStyle = document.getElementById(styleId);
if (existingStyle) {
existingStyle.remove();
}
applyStyles();
}
// Apply all necessary styles
function applyStyles() {
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() {
if (toggleCommandId !== null) {
GM_unregisterMenuCommand(toggleCommandId);
}
if (fontCommandId !== null) {
GM_unregisterMenuCommand(fontCommandId);
}
const enabled = isEnabledForSite();
const toggleText = enabled
? "🟢 Enabled on the site"
: "🔴 Disabled on the site";
toggleCommandId = GM_registerMenuCommand(toggleText, function () {
const newEnabledState = !enabled;
setEnabledForSite(newEnabledState);
if (newEnabledState) {
applyStyles();
} else {
removeAppliedFont();
}
registerMenuCommands();
});
const currentFont = getFontForSite();
// Truncate the font name if it's too long (more than x characters)
const truncatedFont =
currentFont && currentFont.length > 10
? currentFont.substring(0, 10) + "..."
: currentFont || "None";
fontCommandId = GM_registerMenuCommand(
`✨ Select Font (Current: ${truncatedFont})`,
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: 12px;
padding: 24px;
z-index: 9999;
min-width: 380px;
width: 400px;
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 .section-title {
font-size: 16px;
font-weight: 600;
margin: 16px 0 8px 0;
color: var(--popup-title, inherit);
display: flex;
align-items: center;
justify-content: space-between;
}
#font-customizer-popup .section-title .title-with-info {
display: flex;
align-items: center;
gap: 8px;
}
#font-customizer-popup .toggle-section{
cursor: pointer;
border: 1px solid var(--popup-border, #cccccc);
border-radius: 8px;
padding: 4px 8px;
font-size: 12px;
font-weight: 600;
color: var(--popup-text-secondary);
}
#font-customizer-popup .toggle-section:hover {
background-color: var(--popup-hover, #f5f5f5);
}
#font-customizer-popup .info-icon {
cursor: help;
color: var(--popup-text-secondary);
font-size: 14px;
position: relative;
}
#font-customizer-popup .info-tooltip {
visibility: hidden;
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: calc(100% + 8px);
color: var(--popup-tooltip-text);
padding: 8px 12px;
background-color: var(--popup-tooltip-bg);
border-radius: 6px;
font-size: 12px;
font-weight: normal;
white-space: normal;
text-wrap: wrap;
z-index: 10000;
opacity: 0;
transition: opacity 0.2s, visibility 0.2s;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
width: 250px;
line-height: 1.4;
}
#font-customizer-popup .info-tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border-width: 5px;
border-style: solid;
border-color: var(--popup-tooltip-bg) transparent transparent transparent;
}
#font-customizer-popup .info-icon:hover .info-tooltip {
visibility: visible;
opacity: 1;
}
#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: 10px 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;
}
#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);
}
#font-customizer-popup .web-font-item {
font-family: inherit;
}
#font-customizer-popup .web-font-item .font-name {
font-family: inherit;
}
/* 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;
--popup-tooltip-bg: #4a4a4a;
--popup-tooltip-text: #ffffff;
}
}
/* 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;
--popup-tooltip-bg: #ffffff;
--popup-tooltip-text: #333333;
}
}
/* 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);
// Add click event to overlay to close popup
overlay.addEventListener("click", closePopup);
// 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="section-title">
<div class="title-with-info">
<span>Your Saved Fonts</span>
<span class="info-icon">ℹ️
<span class="info-tooltip">Custom fonts will only work if they are installed on your system. Use exact font names for best results.</span>
</span>
</div>
<span class="toggle-section" id="toggle-saved">Hide</span>
</div>
<ul id="saved-fonts-list"></ul>
<div id="no-saved-fonts" class="no-fonts-message">No saved fonts or Hiden yet. Add one above!</div>
<div class="section-title">
<div class="title-with-info">
<span>Web Fonts</span>
<span class="info-icon">ℹ️
<span class="info-tooltip">These fonts will be loaded from Google Fonts when selected. They work on any system but require internet connection.</span>
</span>
</div>
<span class="toggle-section" id="toggle-web">Show</span>
</div>
<ul id="web-fonts-list" style="display: none;"></ul>
<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 saved fonts list
const savedFontsList = document.getElementById("saved-fonts-list");
const noSavedFonts = document.getElementById("no-saved-fonts");
// Show/hide no fonts message
if (savedFonts.length === 0) {
noSavedFonts.style.display = "block";
} else {
noSavedFonts.style.display = "none";
}
// Add saved fonts to the list
savedFonts.forEach((font) => {
addFontToList(font, savedFontsList, true);
});
// Add web fonts to the list
const webFontsList = document.getElementById("web-fonts-list");
WEB_FONTS.forEach((font) => {
addFontToList(font.name, webFontsList, false, font);
});
// Toggle sections
document
.getElementById("toggle-saved")
.addEventListener("click", function () {
const savedList = document.getElementById("saved-fonts-list");
const noSaved = document.getElementById("no-saved-fonts");
const isHidden = savedList.style.display === "none";
savedList.style.display = isHidden ? "block" : "none";
noSaved.style.display =
isHidden && getSavedFonts().length === 0 ? "block" : "none";
this.textContent = isHidden ? "Hide" : "Show";
});
document
.getElementById("toggle-web")
.addEventListener("click", function () {
const webList = document.getElementById("web-fonts-list");
const isHidden = webList.style.display === "none";
webList.style.display = isHidden ? "block" : "none";
this.textContent = isHidden ? "Hide" : "Show";
});
// 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 font to the list
function addFontToList(font, listElement, isSaved = false, webFont = null) {
const li = document.createElement("li");
if (webFont) {
li.classList.add("web-font-item");
li.style.fontFamily = webFont.family;
}
li.innerHTML = `
<span class="font-name">${font}</span>
${
isSaved
? `
<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) => {
if (e.target.classList.contains("delete-font")) {
return;
}
// Remove selected class from all items
document
.querySelectorAll("#font-customizer-popup li")
.forEach((item) => {
item.classList.remove("selected");
});
// Add selected class to clicked item
li.classList.add("selected");
// Set the selected font
if (webFont) {
setFontForSite(webFont.family);
loadWebFont(webFont.url);
} else {
setFontForSite(font);
}
// Apply the font if enabled
if (isEnabledForSite()) {
removeAppliedFont();
applyStyles();
}
// Update menu commands
registerMenuCommands();
});
// Delete font when delete button is clicked (only for saved fonts)
if (isSaved) {
const deleteButton = li.querySelector(".delete-font");
deleteButton.addEventListener("click", (e) => {
e.stopPropagation();
removeFontFromList(font);
li.remove();
if (font === currentFont) {
const remainingFonts = getSavedFonts();
if (remainingFonts.length > 0) {
setFontForSite(remainingFonts[0]);
const firstFont = document.querySelector("#saved-fonts-list li");
if (firstFont) {
firstFont.classList.add("selected");
}
} else {
setFontForSite("");
}
if (isEnabledForSite()) {
removeAppliedFont();
applyStyles();
}
registerMenuCommands();
}
if (savedFontsList.children.length === 0) {
noSavedFonts.style.display = "block";
}
});
}
listElement.appendChild(li);
}
// Function to add a new font
function addNewFont() {
const fontName = newFontInput.value.trim();
if (fontName) {
saveFontToList(fontName);
newFontInput.value = "";
noSavedFonts.style.display = "none";
addFontToList(fontName, savedFontsList, true);
setFontForSite(fontName);
document
.querySelectorAll("#font-customizer-popup li")
.forEach((item) => {
item.classList.remove("selected");
});
const newFontElement = Array.from(
document.querySelectorAll("#saved-fonts-list li")
).find((li) => li.querySelector(".font-name").textContent === fontName);
if (newFontElement) {
newFontElement.classList.add("selected");
}
if (isEnabledForSite()) {
removeAppliedFont();
applyStyles();
}
registerMenuCommands();
}
}
// Function to close the popup
function closePopup() {
const popup = document.getElementById("font-customizer-popup");
const overlay = document.getElementById("font-customizer-overlay");
if (popup) popup.remove();
if (overlay) 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();
});
// Allow Escape key to close the popup
document.addEventListener("keydown", function handleEscape(e) {
if (e.key === "Escape") {
closePopup();
document.removeEventListener("keydown", handleEscape);
}
});
}
// Initialize
function init() {
registerMenuCommands();
applyStyles();
const observer = new MutationObserver(function (mutations) {
if (!isEnabledForSite()) return;
const styleElement = document.getElementById("font-customizer-styles");
if (!styleElement) {
applyStyles();
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
}
// Run the script
init();
})();