Font Customizer

Customize fonts for any website through the Tampermonkey menu

目前为 2025-03-16 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Font Customizer
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description Customize fonts for any website through the Tampermonkey menu
  6. // @author Cursor, claude-3.7, and me(qooo).
  7. // @license MIT
  8. // @match *://*/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net
  10. // @grant GM_registerMenuCommand
  11. // @grant GM_unregisterMenuCommand
  12. // @grant GM_addStyle
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. "use strict";
  17.  
  18. // Storage keys
  19. const STORAGE_KEY_PREFIX = "fontCustomizer_";
  20. const ENABLED_SUFFIX = "_enabled";
  21. const FONT_SUFFIX = "_font";
  22. const FONT_LIST_KEY = "fontCustomizer_savedFonts";
  23.  
  24. // Default font options
  25. const DEFAULT_FONTS = [
  26. "Arial",
  27. "Verdana",
  28. "Helvetica",
  29. "Times New Roman",
  30. "Courier New",
  31. "Georgia",
  32. "Tahoma",
  33. "Trebuchet MS",
  34. "Segoe UI",
  35. "Roboto",
  36. "Open Sans",
  37. "Custom...",
  38. ];
  39.  
  40. // Get saved fonts or initialize with empty array
  41. function getSavedFonts() {
  42. const savedFonts = localStorage.getItem(FONT_LIST_KEY);
  43. return savedFonts ? JSON.parse(savedFonts) : [];
  44. }
  45.  
  46. // Save a font to the list if it doesn't exist already
  47. function saveFontToList(font) {
  48. const fonts = getSavedFonts();
  49. if (!fonts.includes(font)) {
  50. fonts.push(font);
  51. localStorage.setItem(FONT_LIST_KEY, JSON.stringify(fonts));
  52. }
  53. }
  54.  
  55. // Remove a font from the saved list
  56. function removeFontFromList(font) {
  57. const fonts = getSavedFonts();
  58. const index = fonts.indexOf(font);
  59. if (index !== -1) {
  60. fonts.splice(index, 1);
  61. localStorage.setItem(FONT_LIST_KEY, JSON.stringify(fonts));
  62. }
  63. }
  64.  
  65. // Get current hostname
  66. const hostname = window.location.hostname;
  67.  
  68. // Storage helper functions
  69. function getStorageKey(suffix) {
  70. return STORAGE_KEY_PREFIX + hostname + suffix;
  71. }
  72.  
  73. function isEnabledForSite() {
  74. return localStorage.getItem(getStorageKey(ENABLED_SUFFIX)) === "true";
  75. }
  76.  
  77. function setEnabledForSite(enabled) {
  78. localStorage.setItem(getStorageKey(ENABLED_SUFFIX), enabled.toString());
  79. }
  80.  
  81. function getFontForSite() {
  82. return localStorage.getItem(getStorageKey(FONT_SUFFIX)) || DEFAULT_FONTS[0];
  83. }
  84.  
  85. function setFontForSite(font) {
  86. localStorage.setItem(getStorageKey(FONT_SUFFIX), font);
  87. }
  88.  
  89. // Apply font to the website
  90. function applyFont() {
  91. if (isEnabledForSite()) {
  92. const font = getFontForSite();
  93. GM_addStyle(`
  94. * {
  95. font-family: "${font}" !important;
  96. }
  97. `);
  98. }
  99. }
  100.  
  101. // Remove applied font styles
  102. function removeAppliedFont() {
  103. // Create a unique ID for our style element
  104. const styleId = "font-customizer-styles";
  105.  
  106. // Remove existing style element if it exists
  107. const existingStyle = document.getElementById(styleId);
  108. if (existingStyle) {
  109. existingStyle.remove();
  110. }
  111.  
  112. // Re-apply styles for other elements that might need them
  113. applyStyles();
  114. }
  115.  
  116. // Apply all necessary styles
  117. function applyStyles() {
  118. // If enabled, apply the font
  119. if (isEnabledForSite()) {
  120. const font = getFontForSite();
  121. const styleElement = document.createElement("style");
  122. styleElement.id = "font-customizer-styles";
  123. styleElement.textContent = `
  124. * {
  125. font-family: "${font}" !important;
  126. }
  127. `;
  128. document.head.appendChild(styleElement);
  129. }
  130. }
  131.  
  132. // Menu command IDs
  133. let toggleCommandId = null;
  134. let fontCommandId = null;
  135.  
  136. // Register menu commands
  137. function registerMenuCommands() {
  138. // Unregister existing commands
  139. if (toggleCommandId !== null) {
  140. GM_unregisterMenuCommand(toggleCommandId);
  141. }
  142. if (fontCommandId !== null) {
  143. GM_unregisterMenuCommand(fontCommandId);
  144. }
  145.  
  146. // Register toggle command with status indicator
  147. const enabled = isEnabledForSite();
  148. const toggleText = enabled
  149. ? "🟢 Font Customizer: Enabled"
  150. : "🔴 Font Customizer: Disabled";
  151.  
  152. toggleCommandId = GM_registerMenuCommand(toggleText, function () {
  153. const newEnabledState = !enabled;
  154. setEnabledForSite(newEnabledState);
  155.  
  156. if (newEnabledState) {
  157. // If enabling, apply font immediately
  158. applyStyles();
  159. } else {
  160. // If disabling, remove applied font
  161. removeAppliedFont();
  162. }
  163.  
  164. // Update menu commands
  165. registerMenuCommands();
  166. });
  167.  
  168. // Register font selection command
  169. const currentFont = getFontForSite();
  170. fontCommandId = GM_registerMenuCommand(
  171. `Select Font (Current: ${currentFont})`,
  172. showFontSelector
  173. );
  174. }
  175.  
  176. // Create and show font selector popup
  177. function showFontSelector() {
  178. // Remove existing popup if any
  179. const existingPopup = document.getElementById("font-customizer-popup");
  180. if (existingPopup) {
  181. existingPopup.remove();
  182. }
  183.  
  184. // Create popup container
  185. const popup = document.createElement("div");
  186. popup.id = "font-customizer-popup";
  187.  
  188. // Add styles for the popup
  189. GM_addStyle(`
  190. #font-customizer-popup {
  191. position: fixed;
  192. top: 50%;
  193. left: 50%;
  194. transform: translate(-50%, -50%);
  195. background-color: var(--popup-bg, #ffffff);
  196. color: var(--popup-text, #000000);
  197. border: 1px solid var(--popup-border, #cccccc);
  198. border-radius: 8px;
  199. padding: 24px;
  200. z-index: 9999;
  201. width: 30dvw;
  202. height: fit-content;
  203. overflow-y: auto;
  204. box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
  205. font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  206. animation: popup-fade-in 0.2s ease-out;
  207. }
  208.  
  209. @keyframes popup-fade-in {
  210. from { opacity: 0; transform: translate(-50%, -48%); }
  211. to { opacity: 1; transform: translate(-50%, -50%); }
  212. }
  213.  
  214. #font-customizer-popup h2 {
  215. margin-top: 0;
  216. margin-bottom: 20px;
  217. font-size: 20px;
  218. font-weight: 600;
  219. text-align: center;
  220. color: var(--popup-title, inherit);
  221. }
  222.  
  223. #font-customizer-popup .font-input-container {
  224. margin-bottom: 16px;
  225. }
  226.  
  227. #font-customizer-popup .font-input {
  228. width: 100%;
  229. padding: 12px;
  230. border: 1px solid var(--popup-border, #cccccc);
  231. border-radius: 8px;
  232. box-sizing: border-box;
  233. font-size: 14px;
  234. transition: border-color 0.2s;
  235. margin-bottom: 8px;
  236. }
  237.  
  238. #font-customizer-popup .font-input:focus {
  239. border-color: var(--popup-button, #4a86e8);
  240. outline: none;
  241. box-shadow: 0 0 0 2px rgba(74, 134, 232, 0.2);
  242. }
  243.  
  244. #font-customizer-popup .add-font-button {
  245. display: block;
  246. width: 100%;
  247. padding: 8px 16px;
  248. background-color: var(--popup-button, #4a86e8);
  249. color: white;
  250. border: none;
  251. border-radius: 8px;
  252. cursor: pointer;
  253. font-weight: 600;
  254. font-size: 14px;
  255. transition: background-color 0.2s, transform 0.1s;
  256. }
  257.  
  258. #font-customizer-popup .add-font-button:hover {
  259. background-color: var(--popup-button-hover, #3b78e7);
  260. }
  261.  
  262. #font-customizer-popup .add-font-button:active {
  263. transform: scale(0.98);
  264. }
  265.  
  266. #font-customizer-popup .saved-fonts-title {
  267. font-size: 16px;
  268. font-weight: 600;
  269. margin: 16px 0 8px 0;
  270. color: var(--popup-title, inherit);
  271. }
  272.  
  273. #font-customizer-popup .no-fonts-message {
  274. color: var(--popup-text-secondary, #666666);
  275. font-style: italic;
  276. text-align: center;
  277. padding: 16px 0;
  278. }
  279.  
  280. #font-customizer-popup ul {
  281. list-style: none;
  282. padding: 0;
  283. margin: 0 0 16px 0;
  284. max-height: 200px;
  285. overflow-y: auto;
  286. border-radius: 8px;
  287. border: 1px solid var(--popup-border, #eaeaea);
  288. }
  289.  
  290. #font-customizer-popup ul:empty {
  291. display: none;
  292. }
  293.  
  294. #font-customizer-popup ul::-webkit-scrollbar {
  295. width: 8px;
  296. }
  297.  
  298. #font-customizer-popup ul::-webkit-scrollbar-track {
  299. background: var(--popup-scrollbar-track, #f1f1f1);
  300. border-radius: 0 8px 8px 0;
  301. }
  302.  
  303. #font-customizer-popup ul::-webkit-scrollbar-thumb {
  304. background: var(--popup-scrollbar-thumb, #c1c1c1);
  305. border-radius: 4px;
  306. }
  307.  
  308. #font-customizer-popup ul::-webkit-scrollbar-thumb:hover {
  309. background: var(--popup-scrollbar-thumb-hover, #a1a1a1);
  310. }
  311.  
  312. #font-customizer-popup li {
  313. padding: 4px 16px;
  314. cursor: pointer;
  315. transition: all 0.15s ease;
  316. border-bottom: 1px solid var(--popup-border, #eaeaea);
  317. display: flex;
  318. align-items: center;
  319. justify-content: space-between;
  320. }
  321.  
  322. #font-customizer-popup li:last-child {
  323. border-bottom: none;
  324. }
  325.  
  326. #font-customizer-popup li:hover {
  327. background-color: var(--popup-hover, #f5f5f5);
  328. }
  329.  
  330. #font-customizer-popup li.selected {
  331. background-color: var(--popup-selected, #e8f0fe);
  332. font-weight: 500;
  333. }
  334.  
  335. #font-customizer-popup li.selected .font-name::before {
  336. content: "✓";
  337. margin-right: 8px;
  338. color: var(--popup-check, #4a86e8);
  339. font-weight: bold;
  340. }
  341.  
  342. #font-customizer-popup li:not(.selected) .font-name {
  343. padding-left: 24px; /* Align with selected items that have checkmark */
  344. }
  345.  
  346. #font-customizer-popup .font-actions {
  347. display: flex;
  348. opacity: 0;
  349. transition: opacity 0.2s;
  350. }
  351.  
  352. #font-customizer-popup li:hover .font-actions {
  353. opacity: 1;
  354. }
  355.  
  356. #font-customizer-popup .delete-font {
  357. color: var(--popup-delete, #e53935);
  358. cursor: pointer;
  359. font-size: 16px;
  360. padding: 4px;
  361. border-radius: 4px;
  362. transition: background-color 0.2s;
  363. }
  364.  
  365. #font-customizer-popup .delete-font:hover {
  366. background-color: var(--popup-delete-hover, rgba(229, 57, 53, 0.1));
  367. }
  368.  
  369. #font-customizer-popup .close-button {
  370. display: block;
  371. width: 100%;
  372. margin: 16px auto 0;
  373. padding: 12px 16px;
  374. background-color: var(--popup-button-secondary, #757575);
  375. color: white;
  376. border: none;
  377. border-radius: 8px;
  378. cursor: pointer;
  379. font-weight: 600;
  380. font-size: 15px;
  381. transition: background-color 0.2s, transform 0.1s;
  382. }
  383.  
  384. #font-customizer-popup .close-button:hover {
  385. background-color: var(--popup-button-secondary-hover, #616161);
  386. }
  387.  
  388. #font-customizer-popup .close-button:active {
  389. transform: scale(0.98);
  390. }
  391.  
  392. /* Dark mode detection and styles */
  393. @media (prefers-color-scheme: dark) {
  394. #font-customizer-popup {
  395. --popup-bg: #222222;
  396. --popup-text: #ffffff;
  397. --popup-text-secondary: #aaaaaa;
  398. --popup-title: #ffffff;
  399. --popup-border: #444444;
  400. --popup-hover: #333333;
  401. --popup-selected: #2c3e50;
  402. --popup-check: #64b5f6;
  403. --popup-button: #4a86e8;
  404. --popup-button-hover: #3b78e7;
  405. --popup-button-secondary: #616161;
  406. --popup-button-secondary-hover: #757575;
  407. --popup-delete: #f44336;
  408. --popup-delete-hover: rgba(244, 67, 54, 0.2);
  409. --popup-scrollbar-track: #333333;
  410. --popup-scrollbar-thumb: #555555;
  411. --popup-scrollbar-thumb-hover: #666666;
  412. }
  413. }
  414.  
  415. /* Light mode styles */
  416. @media (prefers-color-scheme: light) {
  417. #font-customizer-popup {
  418. --popup-bg: #ffffff;
  419. --popup-text: #333333;
  420. --popup-text-secondary: #666666;
  421. --popup-title: #222222;
  422. --popup-border: #eaeaea;
  423. --popup-hover: #f5f5f5;
  424. --popup-selected: #e8f0fe;
  425. --popup-check: #4a86e8;
  426. --popup-button: #4a86e8;
  427. --popup-button-hover: #3b78e7;
  428. --popup-button-secondary: #757575;
  429. --popup-button-secondary-hover: #616161;
  430. --popup-delete: #e53935;
  431. --popup-delete-hover: rgba(229, 57, 53, 0.1);
  432. --popup-scrollbar-track: #f1f1f1;
  433. --popup-scrollbar-thumb: #c1c1c1;
  434. --popup-scrollbar-thumb-hover: #a1a1a1;
  435. }
  436. }
  437.  
  438. /* Overlay to prevent clicking outside */
  439. #font-customizer-overlay {
  440. position: fixed;
  441. top: 0;
  442. left: 0;
  443. width: 100%;
  444. height: 100%;
  445. background: rgba(0, 0, 0, 0.5);
  446. z-index: 9998;
  447. animation: overlay-fade-in 0.2s ease-out;
  448. }
  449.  
  450. @keyframes overlay-fade-in {
  451. from { opacity: 0; }
  452. to { opacity: 1; }
  453. }
  454. `);
  455.  
  456. // Create overlay to prevent clicking outside
  457. const overlay = document.createElement("div");
  458. overlay.id = "font-customizer-overlay";
  459. document.body.appendChild(overlay);
  460.  
  461. // Create popup content
  462. popup.innerHTML = `
  463. <h2>Font Customizer</h2>
  464. <div class="font-input-container">
  465. <input type="text" id="new-font-input" class="font-input" placeholder="Enter font name (e.g., Arial, sans-serif)">
  466. <button id="add-font-button" class="add-font-button">Add & Apply Font</button>
  467. </div>
  468. <div class="saved-fonts-title">Your Saved Fonts</div>
  469. <ul id="font-list"></ul>
  470. <div id="no-fonts-message" class="no-fonts-message">No saved fonts yet. Add one above!</div>
  471. <button class="close-button" id="close-popup">Close</button>
  472. `;
  473.  
  474. document.body.appendChild(popup);
  475.  
  476. // Get current font and saved fonts
  477. const currentFont = getFontForSite();
  478. const savedFonts = getSavedFonts();
  479.  
  480. // Populate font list
  481. const fontList = document.getElementById("font-list");
  482. const noFontsMessage = document.getElementById("no-fonts-message");
  483.  
  484. // Show/hide no fonts message
  485. if (savedFonts.length === 0) {
  486. noFontsMessage.style.display = "block";
  487. } else {
  488. noFontsMessage.style.display = "none";
  489. }
  490.  
  491. // Add saved fonts to the list
  492. savedFonts.forEach((font) => {
  493. addFontToList(font);
  494. });
  495.  
  496. // Function to add a font to the list
  497. function addFontToList(font) {
  498. const li = document.createElement("li");
  499. li.innerHTML = `
  500. <span class="font-name">${font}</span>
  501. <div class="font-actions">
  502. <span class="delete-font" title="Remove font">🗑️</span>
  503. </div>
  504. `;
  505.  
  506. if (font === currentFont) {
  507. li.classList.add("selected");
  508. }
  509.  
  510. // Select font when clicked
  511. li.addEventListener("click", (e) => {
  512. // Ignore if delete button was clicked
  513. if (e.target.classList.contains("delete-font")) {
  514. return;
  515. }
  516.  
  517. // Remove selected class from all items
  518. document.querySelectorAll("#font-list li").forEach((item) => {
  519. item.classList.remove("selected");
  520. });
  521.  
  522. // Add selected class to clicked item
  523. li.classList.add("selected");
  524.  
  525. // Set the selected font
  526. setFontForSite(font);
  527.  
  528. // Apply the font if enabled
  529. if (isEnabledForSite()) {
  530. removeAppliedFont(); // Remove old font styles
  531. applyStyles(); // Apply new font styles
  532. }
  533.  
  534. // Update menu commands
  535. registerMenuCommands();
  536. });
  537.  
  538. // Delete font when delete button is clicked
  539. const deleteButton = li.querySelector(".delete-font");
  540. deleteButton.addEventListener("click", (e) => {
  541. e.stopPropagation(); // Prevent triggering the li click event
  542.  
  543. // Remove font from saved list
  544. removeFontFromList(font);
  545.  
  546. // Remove the list item
  547. li.remove();
  548.  
  549. // If this was the current font, reset to default
  550. if (font === currentFont) {
  551. // If there are other fonts, select the first one
  552. const remainingFonts = getSavedFonts();
  553. if (remainingFonts.length > 0) {
  554. setFontForSite(remainingFonts[0]);
  555.  
  556. // Select the first font in the list
  557. const firstFont = document.querySelector("#font-list li");
  558. if (firstFont) {
  559. firstFont.classList.add("selected");
  560. }
  561. } else {
  562. // No fonts left, reset to system default
  563. setFontForSite("");
  564. }
  565.  
  566. // Apply changes if enabled
  567. if (isEnabledForSite()) {
  568. removeAppliedFont();
  569. applyStyles();
  570. }
  571.  
  572. // Update menu commands
  573. registerMenuCommands();
  574. }
  575.  
  576. // Show/hide no fonts message
  577. if (fontList.children.length === 0) {
  578. noFontsMessage.style.display = "block";
  579. }
  580. });
  581.  
  582. fontList.appendChild(li);
  583. }
  584.  
  585. // Handle new font input
  586. const newFontInput = document.getElementById("new-font-input");
  587. const addFontButton = document.getElementById("add-font-button");
  588.  
  589. // Focus the input field
  590. newFontInput.focus();
  591.  
  592. // Add font when button is clicked
  593. addFontButton.addEventListener("click", addNewFont);
  594.  
  595. // Add font when Enter is pressed
  596. newFontInput.addEventListener("keydown", (e) => {
  597. if (e.key === "Enter") {
  598. addNewFont();
  599. }
  600. });
  601.  
  602. // Function to add a new font
  603. function addNewFont() {
  604. const fontName = newFontInput.value.trim();
  605. if (fontName) {
  606. // Save font to list
  607. saveFontToList(fontName);
  608.  
  609. // Clear the input
  610. newFontInput.value = "";
  611.  
  612. // Hide no fonts message
  613. noFontsMessage.style.display = "none";
  614.  
  615. // Remove existing font from list if it exists
  616. const existingFont = document.querySelector(
  617. `#font-list li .font-name[data-font="${fontName}"]`
  618. );
  619. if (existingFont) {
  620. existingFont.closest("li").remove();
  621. }
  622.  
  623. // Add to the list
  624. addFontToList(fontName);
  625.  
  626. // Set as current font
  627. setFontForSite(fontName);
  628.  
  629. // Remove selected class from all items
  630. document.querySelectorAll("#font-list li").forEach((item) => {
  631. item.classList.remove("selected");
  632. });
  633.  
  634. // Select the new font
  635. const newFontElement = Array.from(
  636. document.querySelectorAll("#font-list li")
  637. ).find((li) => li.querySelector(".font-name").textContent === fontName);
  638. if (newFontElement) {
  639. newFontElement.classList.add("selected");
  640. }
  641.  
  642. // Apply the font if enabled
  643. if (isEnabledForSite()) {
  644. removeAppliedFont();
  645. applyStyles();
  646. }
  647.  
  648. // Update menu commands
  649. registerMenuCommands();
  650. }
  651. }
  652.  
  653. // Function to close the popup
  654. function closePopup() {
  655. document.getElementById("font-customizer-popup").remove();
  656. document.getElementById("font-customizer-overlay").remove();
  657. }
  658.  
  659. // Close button functionality
  660. document
  661. .getElementById("close-popup")
  662. .addEventListener("click", closePopup);
  663.  
  664. // Prevent closing when clicking on the popup itself
  665. popup.addEventListener("click", (e) => {
  666. e.stopPropagation();
  667. });
  668.  
  669. // Prevent keyboard shortcuts from closing the popup
  670. document.addEventListener("keydown", function preventEscape(e) {
  671. if (e.key === "Escape") {
  672. e.stopPropagation();
  673. e.preventDefault();
  674. }
  675.  
  676. // Remove this event listener when popup is closed
  677. if (!document.getElementById("font-customizer-popup")) {
  678. document.removeEventListener("keydown", preventEscape);
  679. }
  680. });
  681. }
  682.  
  683. // Initialize
  684. function init() {
  685. registerMenuCommands();
  686. applyStyles(); // Use the new function instead of applyFont
  687.  
  688. // Add mutation observer to handle dynamically added content
  689. const observer = new MutationObserver(function (mutations) {
  690. // If we're not enabled, don't do anything
  691. if (!isEnabledForSite()) return;
  692.  
  693. // Check if our style element still exists
  694. const styleElement = document.getElementById("font-customizer-styles");
  695. if (!styleElement) {
  696. // If it doesn't, reapply our styles
  697. applyStyles();
  698. }
  699. });
  700.  
  701. // Start observing the document with the configured parameters
  702. observer.observe(document.documentElement, {
  703. childList: true,
  704. subtree: true,
  705. });
  706. }
  707.  
  708. // Run the script
  709. init();
  710. })();