import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; interface AssetEntry { path: string; type: 'image' | 'audio'; size: number; } @Injectable({ providedIn: 'root' }) export class AssetPreloaderService { private totalBytes = 0; private loadedBytes = 0; private _progress = new BehaviorSubject(0); progress$ = this._progress.asObservable(); private images = new Map(); private audio = new Map(); async preload(): Promise { const manifest: AssetEntry[] = await fetch( '/assets/manifest.json' ).then((res) => res.json()); this.totalBytes = manifest.reduce((sum, asset) => sum + asset.size, 0); const loadTasks = manifest.map((entry) => this.loadAsset(entry).then((blob) => { const url = URL.createObjectURL(blob); if (entry.type === 'image') { const img = new Image(); const loadedPromise = new Promise((resolve, reject) => { img.onload = () => { URL.revokeObjectURL(url); resolve(); }; img.onerror = (e) => { console.error(`Failed to load image: ${entry.path}`, e); URL.revokeObjectURL(url); reject(e); }; }); img.src = url; return loadedPromise.then(() => { this.images.set(entry.path, img); }); } else if (entry.type === 'audio') { const audio = new Audio(); const loadedPromise = new Promise((resolve, reject) => { audio.oncanplaythrough = () => resolve(); audio.onerror = (e) => { console.error(`Failed to load audio: ${entry.path}`, e); resolve(); // Don't reject to avoid blocking all }; }); audio.src = url; return loadedPromise.then(() => { this.audio.set(entry.path, audio); }); } return Promise.resolve(); }) ); await Promise.all(loadTasks); } private async loadAsset(entry: AssetEntry): Promise { const response = await fetch(`/assets/${entry.path}`); if (!response.ok) { throw new Error(`Failed to fetch asset: ${entry.path}`); } const contentType = response.headers.get('Content-Type') || undefined; const reader = response.body?.getReader(); let loaded = 0; const chunks = []; while (true) { const { done, value } = await reader!.read(); if (done) break; chunks.push(value); loaded += value.length; this.loadedBytes += value.length; this._progress.next(this.loadedBytes / this.totalBytes); } return new Blob(chunks, contentType ? { type: contentType } : undefined); } getImage(name: string): HTMLImageElement { return this.images.get(name)!; } getAudio(name: string): HTMLAudioElement { return this.audio.get(name)!; } }