import { Injectable, Inject } from '@angular/core'; import { DOCUMENT } from '@angular/common'; import { BehaviorSubject, Observable, from, of, forkJoin } from 'rxjs'; import { map, catchError } from 'rxjs/operators'; import { FontDefinition, FontTheme, FontManagerConfig, FontLoadingOptions } from './types/font-types'; import { GOOGLE_FONTS_PRESET, FONT_COMBINATIONS } from './presets/google-fonts-preset'; @Injectable({ providedIn: 'root' }) export class UiFontManagerService { private loadedFonts = new Set(); private activeTheme$ = new BehaviorSubject('default'); private customPresets = new Map(); private config: FontManagerConfig = { defaultTheme: 'default', preloadFonts: false, fallbackTimeout: 3000, cacheEnabled: true }; constructor(@Inject(DOCUMENT) private document: Document) {} /** * Configure the font manager */ configure(config: Partial): void { this.config = { ...this.config, ...config }; } /** * Get all available Google Fonts presets */ getGoogleFontsPresets(): { [key: string]: FontDefinition } { return GOOGLE_FONTS_PRESET; } /** * Get popular font combinations */ getFontCombinations(): typeof FONT_COMBINATIONS { return FONT_COMBINATIONS; } /** * Add a custom font preset */ addCustomFont(name: string, definition: FontDefinition): void { this.customPresets.set(name, definition); } /** * Get all available fonts (Google Fonts + Custom) */ getAllFonts(): { [key: string]: FontDefinition } { return { ...GOOGLE_FONTS_PRESET, ...Object.fromEntries(this.customPresets) }; } /** * Load a single font dynamically */ loadFont(fontName: string, options?: FontLoadingOptions): Observable { const font = this.getAllFonts()[fontName]; if (!font) { console.warn(`Font "${fontName}" not found in presets`); return of(false); } if (this.loadedFonts.has(fontName)) { return of(true); } return this.loadFontFromDefinition(font, options).pipe( map(() => { this.loadedFonts.add(fontName); return true; }), catchError(error => { console.error(`Failed to load font "${fontName}":`, error); return of(false); }) ); } /** * Load multiple fonts */ loadFonts(fontNames: string[], options?: FontLoadingOptions): Observable { const loadObservables = fontNames.map(name => this.loadFont(name, options)); return forkJoin(loadObservables); } /** * Apply a font theme */ applyTheme(theme: FontTheme): Observable { const fontsToLoad = Object.values(theme.fonts); const fontNames = fontsToLoad.map(font => font.family); return this.loadFonts(fontNames).pipe( map(results => { if (results.every(success => success)) { this.updateCSSVariables(theme); this.activeTheme$.next(theme.name); return true; } return false; }) ); } /** * Get current active theme */ getActiveTheme(): Observable { return this.activeTheme$.asObservable(); } /** * Create a theme from font combination */ createThemeFromCombination(name: string, combinationName: string): FontTheme | null { const combination = FONT_COMBINATIONS[combinationName as keyof typeof FONT_COMBINATIONS]; if (!combination) return null; const allFonts = this.getAllFonts(); return { name, fonts: { sans: allFonts[combination.sans], serif: allFonts[combination.serif], mono: allFonts[combination.mono], display: allFonts[combination.display] } }; } /** * Check if font is loaded */ isFontLoaded(fontName: string): boolean { return this.loadedFonts.has(fontName); } /** * Preload fonts for performance */ preloadFonts(fontNames: string[]): void { if (!this.config.preloadFonts) return; fontNames.forEach(fontName => { const font = this.getAllFonts()[fontName]; if (font?.url) { const link = this.document.createElement('link'); link.rel = 'preload'; link.href = font.url; link.as = 'style'; this.document.head.appendChild(link); } }); } private loadFontFromDefinition(font: FontDefinition, options?: FontLoadingOptions): Observable { return new Observable(observer => { if (!font.url) { // Assume system font or already available observer.next(); observer.complete(); return; } // Check if already loaded const existingLink = this.document.querySelector(`link[href="${font.url}"]`); if (existingLink) { observer.next(); observer.complete(); return; } // Create and load font const link = this.document.createElement('link'); link.rel = 'stylesheet'; link.href = this.buildFontURL(font, options); link.onload = () => { observer.next(); observer.complete(); }; link.onerror = (error) => { observer.error(error); }; this.document.head.appendChild(link); // Fallback timeout setTimeout(() => { if (!observer.closed) { observer.error(new Error('Font loading timeout')); } }, this.config.fallbackTimeout); }); } private buildFontURL(font: FontDefinition, options?: FontLoadingOptions): string { if (!font.url) return ''; let url = font.url; // Add display parameter for Google Fonts if (url.includes('fonts.googleapis.com') && options?.display) { const separator = url.includes('?') ? '&' : '?'; url += `${separator}display=${options.display}`; } return url; } private updateCSSVariables(theme: FontTheme): void { const root = this.document.documentElement; // Update CSS custom properties root.style.setProperty('--font-family-sans', this.buildFontStack(theme.fonts.sans)); root.style.setProperty('--font-family-serif', this.buildFontStack(theme.fonts.serif)); root.style.setProperty('--font-family-mono', this.buildFontStack(theme.fonts.mono)); root.style.setProperty('--font-family-display', this.buildFontStack(theme.fonts.display)); } private buildFontStack(font: FontDefinition): string { return [font.family, ...font.fallbacks].map(f => f.includes(' ') ? `"${f}"` : f).join(', '); } }