2026-05-17 21:44:15 -04:00
// Z-AMPP MAM bridge. On boot, reads ?asset=<uuid> from URL and imports it.
// Also exports pickFromMAM() for an in-app picker.
type MAMAsset = { id : string ; display_name? : string ; filename? : string ; } ;
async function getStreamURL ( assetId : string ) : Promise < string > {
const r = await fetch ( '/api/v1/assets/' + assetId + '/stream' , { credentials : 'omit' } ) ;
if ( ! r . ok ) throw new Error ( 'stream lookup failed: ' + r . status ) ;
const j = await r . json ( ) ;
if ( ! j || ! j . url ) throw new Error ( 'stream lookup: no url' ) ;
return j . url ;
}
async function listAssets ( limit = 200 ) : Promise < MAMAsset [ ] > {
const r = await fetch ( '/api/v1/assets?limit=' + limit , { credentials : 'omit' } ) ;
if ( ! r . ok ) throw new Error ( 'list assets failed: ' + r . status ) ;
const j = await r . json ( ) ;
return ( j && j . assets ) || [ ] ;
}
function getMediaBridge ( ) : any {
const w = window as any ;
return w . __mediaBridge || w . mediaBridge || w . MediaBridge || ( w . editor && w . editor . mediaBridge ) || null ;
}
async function importAsset ( assetId : string , name? : string ) : Promise < void > {
const url = await getStreamURL ( assetId ) ;
2026-05-17 22:20:38 -04:00
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 ) ;
2026-05-17 21:44:15 -04:00
}
2026-05-17 22:20:38 -04:00
const bridge = getMediaBridge ( ) ;
if ( bridge && typeof bridge . importFromURL === 'function' ) return bridge . importFromURL ( url , safeName ) ;
throw new Error ( 'No import target' ) ;
2026-05-17 21:44:15 -04:00
}
export async function pickFromMAM ( ) : Promise < void > {
const assets = await listAssets ( 200 ) ;
const overlay = document . createElement ( 'div' ) ;
Object . assign ( overlay . style , { position : 'fixed' , inset : '0' , background : 'rgba(0,0,0,0.72)' , zIndex : '999999' , display : 'flex' , alignItems : 'center' , justifyContent : 'center' } as CSSStyleDeclaration ) ;
const modal = document . createElement ( 'div' ) ;
Object . assign ( modal . style , { width : 'min(960px,92vw)' , maxHeight : '84vh' , background : '#161618' , color : '#e8e8ea' , borderRadius : '12px' , padding : '16px' , display : 'flex' , flexDirection : 'column' , gap : '12px' , overflow : 'hidden' } as CSSStyleDeclaration ) ;
modal . innerHTML = '<div style="display:flex;justify-content:space-between;align-items:center"><strong>Import from Z-AMPP MAM</strong><button data-close style="background:#2a2a2e;color:#fff;border:0;border-radius:6px;padding:6px 10px;cursor:pointer">Close</button></div>' ;
const grid = document . createElement ( 'div' ) ;
Object . assign ( grid . style , { display : 'grid' , gridTemplateColumns : 'repeat(auto-fill,minmax(180px,1fr))' , gap : '10px' , overflowY : 'auto' , padding : '4px' } as CSSStyleDeclaration ) ;
for ( const a of assets ) {
const card = document . createElement ( 'button' ) ;
Object . assign ( card . style , { background : '#1f1f23' , border : '1px solid #2c2c32' , borderRadius : '8px' , padding : '6px' , color : '#e8e8ea' , cursor : 'pointer' , display : 'flex' , flexDirection : 'column' , gap : '6px' , textAlign : 'left' } as CSSStyleDeclaration ) ;
const label = a . display_name || a . filename || a . id ;
card . innerHTML = '<img loading="lazy" src="/api/v1/assets/' + a . id + '/thumbnail" style="width:100%;aspect-ratio:16/9;object-fit:cover;background:#000;border-radius:4px" onerror="this.style.opacity=0.2"/><span style="font-size:12px;line-height:1.2;word-break:break-word">' + label + '</span>' ;
card . addEventListener ( 'click' , async ( ) = > { card . disabled = true ; card . style . opacity = '0.5' ; try { await importAsset ( a . id , label ) ; } catch ( e ) { alert ( 'Import failed: ' + ( e as Error ) . message ) ; } finally { card . disabled = false ; card . style . opacity = '1' ; } } ) ;
grid . appendChild ( card ) ;
}
modal . appendChild ( grid ) ;
overlay . appendChild ( modal ) ;
document . body . appendChild ( overlay ) ;
const close = ( ) = > overlay . remove ( ) ;
overlay . addEventListener ( 'click' , ( e ) = > { if ( e . target === overlay ) close ( ) ; } ) ;
modal . querySelector ( '[data-close]' ) ? . addEventListener ( 'click' , close ) ;
}
2026-05-17 22:43:55 -04:00
function showBanner ( text : string ) : void {
let el = document . getElementById ( 'zampp-import-banner' ) ;
if ( ! el ) {
el = document . createElement ( 'div' ) ;
el . id = 'zampp-import-banner' ;
el . style . cssText = 'position:fixed;top:14px;left:50%;transform:translateX(-50%);background:rgba(31,58,208,0.95);color:#fff;font:500 13px/1.4 system-ui,sans-serif;padding:10px 18px;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.4);z-index:2147483647;display:flex;align-items:center;gap:10px;letter-spacing:0.02em' ;
document . body . appendChild ( el ) ;
}
el . innerHTML = '<span style="width:10px;height:10px;border-radius:50%;background:#fff;animation:zamppPulse 1.2s ease-in-out infinite;display:inline-block"></span>' + text ;
if ( ! document . getElementById ( 'zampp-banner-keyframes' ) ) {
const st = document . createElement ( 'style' ) ; st . id = 'zampp-banner-keyframes' ;
st . textContent = '@keyframes zamppPulse{0%,100%{opacity:0.4}50%{opacity:1}}' ;
document . head . appendChild ( st ) ;
}
}
function hideBanner ( ) : void { const el = document . getElementById ( 'zampp-import-banner' ) ; if ( el ) el . remove ( ) ; }
2026-05-17 21:44:15 -04:00
export function initMAMBridge ( ) : void {
try {
const qs = new URLSearchParams ( window . location . search ) ;
const projectId = qs . get ( 'project' ) ;
if ( projectId ) { try { window . localStorage . setItem ( 'mamProjectId' , projectId ) ; } catch { } }
( window as any ) . __zamppPickFromMAM = pickFromMAM ;
const assetId = qs . get ( 'asset' ) ;
if ( ! assetId ) return ;
2026-05-17 22:43:55 -04:00
showBanner ( 'Loading clip from Z-AMPP MAM…' ) ;
2026-05-17 22:20:38 -04:00
// 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 ;
2026-05-17 21:44:15 -04:00
const tryImport = async ( n = 0 ) : Promise < void > = > {
2026-05-17 22:20:38 -04:00
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 ; }
2026-05-17 22:43:55 -04:00
if ( Date . now ( ) - stableSince < 600 ) { setTimeout ( ( ) = > tryImport ( n + 1 ) , 500 ) ; return ; }
2026-05-17 22:20:38 -04:00
try {
await importAsset ( assetId ) ;
imported = true ;
console . log ( '[mam-bridge] imported' , assetId , 'into project' , p . id ) ;
2026-05-17 22:43:55 -04:00
showBanner ( 'Clip ready in media bin' ) ;
setTimeout ( hideBanner , 2500 ) ;
2026-05-17 22:20:38 -04:00
} catch ( e ) {
if ( n < 80 ) setTimeout ( ( ) = > tryImport ( n + 1 ) , 1000 ) ;
2026-05-17 22:43:55 -04:00
else { console . error ( '[mam-bridge] import retries exhausted' , e ) ; showBanner ( 'Could not auto-import clip. Use Add Media instead.' ) ; setTimeout ( hideBanner , 6000 ) ; }
2026-05-17 21:44:15 -04:00
}
} ;
if ( document . readyState === 'complete' ) tryImport ( ) ;
else window . addEventListener ( 'load' , ( ) = > tryImport ( ) ) ;
} catch ( e ) { console . error ( '[mam-bridge] init failed' , e ) ; }
}
initMAMBridge ( ) ;