diff --git a/CHANGELOG.md b/CHANGELOG.md index 38c2720..fb5f013 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Capture the Three.js group once in `VrmAvatar` so effect cleanup detaches from the same container the model was attached to (avoids orphan scenes on avatar swap). (#34) - Drop unused `audioFile` prop from `VoicePanel` (file input is uncontrolled). (#34) - `npm run dev:desktop` now passes `concurrently -k` so closing the Electron window (or killing Vite) tears down both processes and frees port 5173. (#37, #38) +- Custom environment library blob URLs are revoked only after the replacement list is in state, so the stage/picker never briefly reference dead `blob:` URLs while a Directories folder switch loads. (#39, #40) ## [0.5.0] — 2026-08-06 diff --git a/avatar/src/components/AvatarStage.jsx b/avatar/src/components/AvatarStage.jsx index beebb3b..a0338d7 100644 --- a/avatar/src/components/AvatarStage.jsx +++ b/avatar/src/components/AvatarStage.jsx @@ -362,7 +362,12 @@ export function AvatarStage() { let cancelled = false; async function refreshEnvLibrary() { - revokeEnvironmentBlobUrls(libraryEnvironmentsRef.current); + // Revoking before the await left the stage and the picker pointing at + // urls that no longer resolve for as long as the replacement took to + // load. Already-decoded images survive revocation, so nothing changed on + // screen — but a remount in that window loaded nothing. Hold the old list + // and drop it only once its replacement is in state (#39). + const previous = libraryEnvironmentsRef.current; const useLibrary = desktopMode && directories.environments.mode === 'custom' && @@ -370,7 +375,10 @@ export function AvatarStage() { if (!useLibrary) { setLibraryCustomEnvironments([]); - if (!cancelled) setLibraryEnvironments([]); + if (!cancelled) { + setLibraryEnvironments([]); + revokeEnvironmentBlobUrls(previous); + } return; } @@ -378,11 +386,13 @@ export function AvatarStage() { directories.environments.path, ); if (cancelled) { + // A newer run captured the same `previous` and owns retiring it. revokeEnvironmentBlobUrls(next); return; } setLibraryEnvironments(next); setLibraryCustomEnvironments(next); + revokeEnvironmentBlobUrls(previous); if (error) { setDirectoriesNotice(error); setDirectories((prev) => ({ @@ -391,6 +401,7 @@ export function AvatarStage() { })); setLibraryEnvironments([]); setLibraryCustomEnvironments([]); + revokeEnvironmentBlobUrls(next); setSelectedBg((prev) => prev.type === 'env' && String(prev.id).startsWith('lib-env-') ? { type: 'color', value: defaultColor } @@ -408,6 +419,7 @@ export function AvatarStage() { })); setLibraryEnvironments([]); setLibraryCustomEnvironments([]); + revokeEnvironmentBlobUrls(next); setSelectedBg((prev) => prev.type === 'env' && String(prev.id).startsWith('lib-env-') ? { type: 'color', value: defaultColor }