182 lines
4.9 KiB
TypeScript
182 lines
4.9 KiB
TypeScript
// Google Fonts loading and management utility
|
|
|
|
export interface CustomFont {
|
|
id: string;
|
|
name: string; // Display name
|
|
fontFamily: string; // CSS font-family value
|
|
importUrl: string; // Google Fonts @import URL
|
|
}
|
|
|
|
const STORAGE_KEY = "openscreen_custom_fonts";
|
|
const loadedFonts = new Set<string>();
|
|
|
|
// Load custom fonts from localStorage
|
|
export function getCustomFonts(): CustomFont[] {
|
|
try {
|
|
const stored = localStorage.getItem(STORAGE_KEY);
|
|
return stored ? JSON.parse(stored) : [];
|
|
} catch (error) {
|
|
console.error("Failed to load custom fonts from storage:", error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Save custom fonts to localStorage
|
|
export function saveCustomFonts(fonts: CustomFont[]): void {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(fonts));
|
|
} catch (error) {
|
|
console.error("Failed to save custom fonts to storage:", error);
|
|
}
|
|
}
|
|
|
|
// Add a new custom font (throws error if font fails to load)
|
|
export async function addCustomFont(font: CustomFont): Promise<CustomFont[]> {
|
|
const fonts = getCustomFonts();
|
|
const exists = fonts.some((f) => f.id === font.id || f.fontFamily === font.fontFamily);
|
|
|
|
if (exists) {
|
|
return fonts;
|
|
}
|
|
|
|
// Try to load the font first - this will throw if it fails
|
|
await loadFont(font);
|
|
|
|
// Only add to storage if font loaded successfully
|
|
fonts.push(font);
|
|
saveCustomFonts(fonts);
|
|
|
|
return fonts;
|
|
}
|
|
|
|
// Remove a custom font
|
|
export function removeCustomFont(fontId: string): CustomFont[] {
|
|
const fonts = getCustomFonts();
|
|
const filtered = fonts.filter((f) => f.id !== fontId);
|
|
saveCustomFonts(filtered);
|
|
|
|
// Remove the style element
|
|
const styleEl = document.getElementById(`custom-font-${fontId}`);
|
|
if (styleEl) {
|
|
styleEl.remove();
|
|
}
|
|
|
|
loadedFonts.delete(fontId);
|
|
return filtered;
|
|
}
|
|
|
|
// Load a Google Font into the document
|
|
export function loadFont(font: CustomFont): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
// Skip if already loaded
|
|
if (loadedFonts.has(font.id)) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const styleId = `custom-font-${font.id}`;
|
|
|
|
// Remove existing style if present
|
|
const existing = document.getElementById(styleId);
|
|
if (existing) {
|
|
existing.remove();
|
|
}
|
|
|
|
// Create style element with @import
|
|
const style = document.createElement("style");
|
|
style.id = styleId;
|
|
style.textContent = `@import url('${font.importUrl}');`;
|
|
document.head.appendChild(style);
|
|
|
|
// Wait for font to load
|
|
waitForFont(font.fontFamily)
|
|
.then(() => {
|
|
loadedFonts.add(font.id);
|
|
resolve();
|
|
})
|
|
.catch(reject);
|
|
} catch (error) {
|
|
console.error("Failed to load font:", font, error);
|
|
reject(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Wait for a font to be available and verify it loaded
|
|
function waitForFont(fontFamily: string, timeout = 5000): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
// Use CSS Font Loading API if available
|
|
if ("fonts" in document) {
|
|
Promise.race([
|
|
document.fonts.load(`16px "${fontFamily}"`),
|
|
new Promise((_, rej) => setTimeout(() => rej(new Error("Font load timeout")), timeout)),
|
|
])
|
|
.then(() => {
|
|
// Verify the font actually loaded by checking if it's available
|
|
const isAvailable = document.fonts.check(`16px "${fontFamily}"`);
|
|
if (isAvailable) {
|
|
resolve();
|
|
} else {
|
|
reject(new Error(`Font "${fontFamily}" failed to load`));
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
reject(error);
|
|
});
|
|
} else {
|
|
// Fallback for browsers without Font Loading API
|
|
// Wait a bit and hope for the best
|
|
setTimeout(() => resolve(), 1000);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Load all stored custom fonts on app initialization
|
|
export function loadAllCustomFonts(): Promise<void[]> {
|
|
const fonts = getCustomFonts();
|
|
return Promise.all(
|
|
fonts.map((font) =>
|
|
loadFont(font).catch((err) => {
|
|
console.error("Failed to load custom font:", font.name, err);
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Generate a unique ID for a font
|
|
export function generateFontId(name: string): string {
|
|
return `${name.toLowerCase().replace(/\s+/g, "-")}-${Date.now()}`;
|
|
}
|
|
|
|
// Parse Google Fonts @import URL to extract font family name
|
|
export function parseFontFamilyFromImport(importUrl: string): string | null {
|
|
try {
|
|
// Extract from URL like: https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap
|
|
const url = new URL(importUrl);
|
|
const familyParam = url.searchParams.get("family");
|
|
|
|
if (familyParam) {
|
|
// Remove weight/style info: "Roboto:wght@400;700" -> "Roboto"
|
|
const fontName = familyParam.split(":")[0];
|
|
// Replace + with spaces: "Open+Sans" -> "Open Sans"
|
|
return fontName.replace(/\+/g, " ");
|
|
}
|
|
|
|
return null;
|
|
} catch (error) {
|
|
console.error("Failed to parse font family from import URL:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Validate if a string looks like a Google Fonts import URL
|
|
export function isValidGoogleFontsUrl(url: string): boolean {
|
|
try {
|
|
const urlObj = new URL(url);
|
|
return urlObj.hostname === "fonts.googleapis.com" && urlObj.searchParams.has("family");
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|