fix(editor): asset auto-import now lands cleanly into the media bin

Three problems were blocking the round-trip. Each fixed.

* MediaBridge.importFromURL went through the file-import service but not the Zustand store, so the media bin stayed empty. mam-bridge now calls window.__projectStore.getState().importMedia(file) which is what the actual UI uses. project-store.ts exposes useProjectStore on window for that hook.
* rustfs serves the proxy with content-type application/octet-stream; the editor rejects with DECODE_ERROR on that mime. Bridge now forces video/mp4 (or audio/wav, video/webm, etc.) based on the asset filename.
* The Recover Your Work modal and the Welcome tour blocked editor initialization. Bridge now auto-clicks Start Fresh and Skip Tour (alongside the format chooser), and waits 1.5s of project-id stability before calling importMedia so it does not get clobbered by the project-replacement cycle. One-shot guard prevents duplicate imports.
This commit is contained in:
Zac Gaetano 2026-05-17 22:20:38 -04:00
parent b68f0c6aba
commit e390f0efab
3 changed files with 50 additions and 11 deletions

View file

@ -485,6 +485,7 @@ let mediaBridgeInstance: MediaBridge | null = null;
export function getMediaBridge(): MediaBridge {
if (!mediaBridgeInstance) {
mediaBridgeInstance = new MediaBridge();
(window as any).__mediaBridge = mediaBridgeInstance;
}
return mediaBridgeInstance;
}
@ -493,6 +494,7 @@ export function getMediaBridge(): MediaBridge {
* Initialize the shared MediaBridge
*/
export async function initializeMediaBridge(): Promise<MediaBridge> {
// Z-AMPP: expose for mam-bridge.ts
const bridge = getMediaBridge();
await bridge.initialize();
return bridge;

View file

@ -25,15 +25,18 @@ function getMediaBridge(): any {
async function importAsset(assetId: string, name?: string): Promise<void> {
const url = await getStreamURL(assetId);
let bridge = getMediaBridge();
if (!bridge || typeof bridge.importFromURL !== 'function') {
await new Promise(r => setTimeout(r, 500));
bridge = getMediaBridge();
const safeName = (name || (assetId + '.mp4')).replace(/[^\w.\-]+/g, '_');
const store: any = (window as any).__projectStore;
if (store && typeof store.getState === 'function' && typeof store.getState().importMedia === 'function') {
const res = await fetch(url, { credentials: 'omit' });
if (!res.ok) throw new Error('fetch failed: ' + res.status);
const blob = await res.blob();
const inferred = (safeName.toLowerCase().endsWith('.webm') ? 'video/webm' : safeName.toLowerCase().endsWith('.mov') ? 'video/quicktime' : safeName.toLowerCase().endsWith('.mp3') ? 'audio/mpeg' : safeName.toLowerCase().endsWith('.wav') ? 'audio/wav' : 'video/mp4'); const ct = (blob.type && blob.type !== 'application/octet-stream') ? blob.type : inferred; const file = new File([blob], safeName, { type: ct });
return store.getState().importMedia(file);
}
if (!bridge || typeof bridge.importFromURL !== 'function') {
throw new Error('MediaBridge.importFromURL not available');
}
return bridge.importFromURL(url, name || (assetId + '.mp4'));
const bridge = getMediaBridge();
if (bridge && typeof bridge.importFromURL === 'function') return bridge.importFromURL(url, safeName);
throw new Error('No import target');
}
export async function pickFromMAM(): Promise<void> {
@ -69,10 +72,42 @@ export function initMAMBridge(): void {
(window as any).__zamppPickFromMAM = pickFromMAM;
const assetId = qs.get('asset');
if (!assetId) return;
// Auto-skip the startup chooser so MediaBridge initializes immediately.
const clickMatching = (matches: string[]): boolean => {
const btns = document.querySelectorAll('button,[role="button"],a');
for (const el of Array.from(btns)) {
const t = (el.textContent || '').trim().toLowerCase();
if (matches.some(m => t === m || t.startsWith(m))) { (el as HTMLElement).click(); return true; }
}
return false;
};
let dismissTries = 0;
const dismissInterval = setInterval(() => {
const acted = clickMatching(['horizontal', 'open editor', 'start fresh', 'skip tour']);
if (dismissTries++ > 80) clearInterval(dismissInterval);
if (acted) dismissTries = Math.max(0, dismissTries - 4);
}, 250);
let lastProjectId: string | null = null;
let stableSince = 0;
let imported = false;
const tryImport = async (n = 0): Promise<void> => {
try { await importAsset(assetId); console.log('[mam-bridge] imported', assetId); }
catch (e) {
if (n < 10) setTimeout(() => tryImport(n + 1), 500);
if (imported) return;
const w = window as any;
const st = w.__projectStore && w.__projectStore.getState && w.__projectStore.getState();
const p = st && st.project;
const ready = !!(p && p.mediaLibrary);
if (!ready) {
if (n < 80) { setTimeout(() => tryImport(n + 1), 750); return; }
console.error('[mam-bridge] project never loaded'); return;
}
if (p.id !== lastProjectId) { lastProjectId = p.id; stableSince = Date.now(); setTimeout(() => tryImport(n + 1), 750); return; }
if (Date.now() - stableSince < 1500) { setTimeout(() => tryImport(n + 1), 500); return; }
try {
await importAsset(assetId);
imported = true;
console.log('[mam-bridge] imported', assetId, 'into project', p.id);
} catch (e) {
if (n < 80) setTimeout(() => tryImport(n + 1), 1000);
else console.error('[mam-bridge] import retries exhausted', e);
}
};

View file

@ -5901,3 +5901,5 @@ export const useProjectStore = create<ProjectState>()(
};
}),
);
;(window as any).__projectStore = useProjectStore;