Equicord/src/equicordplugins/FontLoader/index.tsx

211 lines
6.9 KiB
TypeScript
Raw Normal View History

2025-01-31 00:54:27 +03:00
/*
* 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";
2025-02-12 22:18:51 +03:00
2025-01-31 00:54:27 +03:00
interface GoogleFontMetadata {
family: string;
displayName: string;
authors: string[];
category?: number;
popularity?: number;
variants: Array<{
axes: Array<{
tag: string;
min: number;
max: number;
}>;
}>;
}
2025-02-12 22:18:51 +03:00
const createGoogleFontUrl = (family: string, options = "") =>
`https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}${options}&display=swap`;
const loadFontStyle = (url: string) => {
document.head.insertAdjacentHTML("beforeend", `<link rel="stylesheet" href="${url}">`);
return document.createElement("style");
};
2025-01-31 00:54:27 +03:00
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"
},
body: JSON.stringify([[query, null, null, null, null, null, 1], [5], null, 16])
});
const data = await response.json();
if (!data?.[1]) return [];
2025-02-12 22:18:51 +03:00
return data[1].map(([_, fontData]: [string, any[]]) => ({
2025-01-31 00:54:27 +03:00
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
}))
}))
}));
} catch (err) {
console.error("Failed to fetch fonts:", err);
return [];
}
}
2025-02-12 22:18:51 +03:00
const preloadFont = (family: string) =>
loadFontStyle(createGoogleFontUrl(family, "&text=The quick brown fox jumps over the lazy dog"));
2025-01-31 00:54:27 +03:00
2025-02-12 22:18:51 +03:00
let styleElement: HTMLStyleElement | null = null;
2025-01-31 00:54:27 +03:00
2025-02-12 22:18:51 +03:00
const applyFont = async (fontFamily: string) => {
2025-01-31 00:54:27 +03:00
if (!fontFamily) {
2025-02-12 22:18:51 +03:00
styleElement?.remove();
styleElement = null;
2025-01-31 00:54:27 +03:00
return;
}
try {
if (!styleElement) {
styleElement = document.createElement("style");
document.head.appendChild(styleElement);
}
2025-02-12 22:18:51 +03:00
loadFontStyle(createGoogleFontUrl(fontFamily, ":wght@300;400;500;600;700"));
2025-01-31 00:54:27 +03:00
styleElement.textContent = `
2025-02-12 22:18:51 +03:00
* {
--font-primary: '${fontFamily}', sans-serif !important;
--font-display: '${fontFamily}', sans-serif !important;
--font-headline: '${fontFamily}', sans-serif !important;
--font-code: '${fontFamily}', monospace !important;
}
`;
2025-01-31 00:54:27 +03:00
} catch (err) {
console.error("Failed to load font:", err);
}
2025-02-12 22:18:51 +03:00
};
2025-01-31 00:54:27 +03:00
function GoogleFontSearch({ onSelect }: { onSelect: (font: GoogleFontMetadata) => void; }) {
const [query, setQuery] = React.useState("");
const [results, setResults] = React.useState<GoogleFontMetadata[]>([]);
const [loading, setLoading] = React.useState(false);
const previewStyles = React.useRef<HTMLStyleElement[]>([]);
2025-02-12 22:18:51 +03:00
React.useEffect(() => () => {
previewStyles.current.forEach(style => style.remove());
2025-01-31 00:54:27 +03:00
}, []);
const debouncedSearch = debounce(async (value: string) => {
setLoading(true);
if (!value) {
setResults([]);
setLoading(false);
return;
}
2025-02-12 22:18:51 +03:00
const fonts = await searchGoogleFonts(value);
2025-01-31 00:54:27 +03:00
previewStyles.current.forEach(style => style.remove());
2025-02-12 22:18:51 +03:00
previewStyles.current = await Promise.all(fonts.map(f => preloadFont(f.family)));
2025-01-31 00:54:27 +03:00
setResults(fonts);
setLoading(false);
}, 300);
2025-02-12 22:18:51 +03:00
const handleSearch = (e: string) => {
setQuery(e);
debouncedSearch(e);
};
2025-01-31 00:54:27 +03:00
return (
<Forms.FormSection>
<Forms.FormTitle tag="h3">Search Google Fonts</Forms.FormTitle>
<Forms.FormText>Click on any font to apply it.</Forms.FormText>
<TextInput
value={query}
onChange={e => handleSearch(e)}
placeholder="Search fonts..."
disabled={loading}
className={Margins.bottom16}
/>
{results.length > 0 && (
<div className={classes(Margins.top8, "eq-googlefonts-results")}>
{results.map(font => (
<Card
key={font.family}
className={classes("eq-googlefonts-card", Margins.bottom8)}
onClick={() => onSelect(font)}
>
<div className="eq-googlefonts-preview" style={{ fontFamily: font.family }}>
<Forms.FormTitle tag="h4">{font.displayName}</Forms.FormTitle>
<Forms.FormText>The quick brown fox jumps over the lazy dog</Forms.FormText>
</div>
{font.authors?.length && (
<Forms.FormText className={Margins.top8} style={{ opacity: 0.7 }}>
by {font.authors.join(", ")}
</Forms.FormText>
)}
</Card>
))}
</div>
)}
</Forms.FormSection>
);
}
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: () => (
<GoogleFontSearch
onSelect={font => {
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;
}
}
});