98 lines
2.7 KiB
TypeScript
98 lines
2.7 KiB
TypeScript
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<number>(0);
|
|
progress$ = this._progress.asObservable();
|
|
|
|
private images = new Map<string, HTMLImageElement>();
|
|
private audio = new Map<string, HTMLAudioElement>();
|
|
|
|
async preload(): Promise<void> {
|
|
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<void>((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<void>((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<Blob> {
|
|
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)!;
|
|
}
|
|
}
|