diff --git a/src/equicordplugins/FontLoader/index.tsx b/src/equicordplugins/FontLoader/index.tsx new file mode 100644 index 00000000..a934acfb --- /dev/null +++ b/src/equicordplugins/FontLoader/index.tsx @@ -0,0 +1,238 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./styles.css"; + +import { definePluginSettings } from "@api/Settings"; +import { debounce } from "@shared/debounce"; +import { EquicordDevs } from "@utils/constants"; +import { Margins } from "@utils/margins"; +import { classes } from "@utils/misc"; +import definePlugin, { OptionType } from "@utils/types"; +import { Card, Forms, React, TextInput } from "@webpack/common"; +interface GoogleFontMetadata { + family: string; + displayName: string; + authors: string[]; + category?: number; + popularity?: number; + variants: Array<{ + axes: Array<{ + tag: string; + min: number; + max: number; + }>; + }>; +} +const userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36(KHTML, like Gecko) Chrome / 128.0.0.0 Safari / 537.36"; +async function searchGoogleFonts(query: string) { + try { + const response = await fetch("https://fonts.google.com/$rpc/fonts.fe.catalog.actions.metadata.MetadataService/FontSearch", { + method: "POST", + headers: { + "content-type": "application/json+protobuf", + "x-user-agent": "grpc-web-javascript/0.1" + }, + // the nulls are optional filters + body: JSON.stringify([[query, null, null, null, null, null, 1], [5], null, 16]) + }); + + const data = await response.json(); + if (!data?.[1]) return []; + // god please help me + const fonts = data[1].map(([_, fontData]: [string, any[]]) => ({ + family: fontData[0], + displayName: fontData[1], + authors: fontData[2], + category: fontData[3], + variants: fontData[6].map((variant: any[]) => ({ + axes: variant[0].map(([tag, min, max]: [string, number, number]) => ({ + tag, min, max + })) + })) + })); + return fonts; + // LETS GO IT FUCKING WORKSSSSSSSSSSSS + } catch (err) { + console.error("Failed to fetch fonts:", err); + return []; + } +} + +async function preloadFont(family: string) { + // https://developers.google.com/fonts/docs/css2 + const url = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}&text=The quick brown fox jumps over the lazy dog&display=swap`; + const css = await fetch(url, { + headers: { + "User-Agent": userAgent + } + }).then(r => r.text()); + + const style = document.createElement("style"); + style.textContent = css; + document.head.appendChild(style); + return style; +} + +async function applyFont(fontFamily: string) { + if (!fontFamily) { + if (styleElement) { + styleElement.remove(); + styleElement = null; + } + return; + } + + try { + const response = await fetch( + `https://fonts.googleapis.com/css2?family=${encodeURIComponent(fontFamily)}:wght@300;400;500;600;700&display=swap`, + { + headers: { + "User-Agent": userAgent + } + } + ); + const css = await response.text(); + + if (!styleElement) { + styleElement = document.createElement("style"); + document.head.appendChild(styleElement); + } + + styleElement.textContent = ` + ${css} + * { + --font-primary: '${fontFamily}', sans-serif !important; + --font-display: '${fontFamily}', sans-serif !important; + --font-headline: '${fontFamily}', sans-serif !important; + --font-code: '${fontFamily}', monospace !important; + } + `; + } catch (err) { + console.error("Failed to load font:", err); + } +} + +function GoogleFontSearch({ onSelect }: { onSelect: (font: GoogleFontMetadata) => void; }) { + const [query, setQuery] = React.useState(""); + const [results, setResults] = React.useState([]); + const [loading, setLoading] = React.useState(false); + + + const previewStyles = React.useRef([]); + + + React.useEffect(() => { + return () => { + previewStyles.current.forEach(style => style.remove()); + }; + }, []); + + const debouncedSearch = debounce(async (value: string) => { + setLoading(true); + if (!value) { + setResults([]); + setLoading(false); + return; + } + const fonts = await searchGoogleFonts(value); + + previewStyles.current.forEach(style => style.remove()); + previewStyles.current = []; + + const styles = await Promise.all(fonts.map(f => preloadFont(f.family))); + previewStyles.current = styles; + + setResults(fonts); + setLoading(false); + }, 300); + + const handleSearch = React.useCallback((value: string) => { + setQuery(value); + debouncedSearch(value); + }, []); + + return ( + + Search Google Fonts + Click on any font to apply it. + + handleSearch(e)} + placeholder="Search fonts..." + disabled={loading} + className={Margins.bottom16} + /> + + {results.length > 0 && ( +
+ {results.map(font => ( + onSelect(font)} + > +
+ {font.displayName} + The quick brown fox jumps over the lazy dog +
+ {font.authors?.length && ( + + by {font.authors.join(", ")} + + )} +
+ ))} +
+ )} +
+ ); +} + +let styleElement: HTMLStyleElement | null = null; + +const settings = definePluginSettings({ + selectedFont: { + type: OptionType.STRING, + description: "Currently selected font", + default: "", + hidden: true + }, + fontSearch: { + type: OptionType.COMPONENT, + description: "Search and select Google Fonts", + component: () => ( + { + settings.store.selectedFont = font.family; + applyFont(font.family); + }} + /> + ) + } +}); + +export default definePlugin({ + name: "FontLoader", + description: "Loads any font from Google Fonts", + authors: [EquicordDevs.Crxa, EquicordDevs.vmohammad], // Crxa's only here because he came up with the idea + settings, + + async start() { + const savedFont = settings.store.selectedFont; + if (savedFont) { + await applyFont(savedFont); + } + }, + + stop() { + if (styleElement) { + styleElement.remove(); + styleElement = null; + } + } +}); diff --git a/src/equicordplugins/FontLoader/styles.css b/src/equicordplugins/FontLoader/styles.css new file mode 100644 index 00000000..5ea095fa --- /dev/null +++ b/src/equicordplugins/FontLoader/styles.css @@ -0,0 +1,23 @@ +.eq-googlefonts-results { + max-height: 400px; + overflow-y: auto; +} + +.eq-googlefonts-card { + padding: 16px; + cursor: pointer !important; + background: var(--background-secondary); + border: 1px solid var(--background-tertiary); +} + +.eq-googlefonts-card:hover { + background: var(--background-modifier-hover); + cursor: pointer !important; +} + +.eq-googlefonts-preview { + display: flex; + flex-direction: column; + gap: 8px; + cursor: pointer !important; +}