2026-05-18 10:17:31 -04:00
// Z-AMPP in-house editor — Phase A
// Vanilla JS, no framework. Single track, single video preview, in/out markers per clip.
// Reads assets from /api/v1/assets, streams via /api/v1/assets/:id/stream.
( function ( ) {
const $ = ( id ) => document . getElementById ( id ) ;
// ── State ────────────────────────────────────────────────
const state = {
assets : [ ] , // [{ id, display_name, status, duration_ms, ...}]
filtered : [ ] ,
clips : [ ] , // [{ uid, assetId, name, src, inSec, outSec, durSec, thumbUrl }]
selectedClipUid : null ,
isPlaying : false ,
nextUid : 1 ,
drafts : loadDrafts ( ) ,
} ;
function loadDrafts ( ) {
try { return JSON . parse ( localStorage . getItem ( 'zampp-edit-drafts' ) || '[]' ) ; }
catch { return [ ] ; }
}
function persistDrafts ( ) {
try { localStorage . setItem ( 'zampp-edit-drafts' , JSON . stringify ( state . drafts ) ) ; } catch { }
}
2026-05-18 10:25:53 -04:00
// Thumbnails: /api/v1/assets/:id/thumbnail returns JSON { url }, not the image itself.
const _thumbCache = new Map ( ) ;
async function _thumbFor ( id ) {
if ( _thumbCache . has ( id ) ) return _thumbCache . get ( id ) ;
const p = ( async ( ) => {
try {
const r = await fetch ( '/api/v1/assets/' + id + '/thumbnail' , { credentials : 'include' } ) ;
if ( ! r . ok ) return null ;
const j = await r . json ( ) ;
return j . url || null ;
} catch { return null ; }
} ) ( ) ;
_thumbCache . set ( id , p ) ;
return p ;
}
function hydrateThumbnails ( root ) {
( root || document ) . querySelectorAll ( 'img[data-asset-thumb]' ) . forEach ( async ( img ) => {
if ( img . dataset . hydrated === '1' ) return ;
img . dataset . hydrated = '1' ;
const id = img . dataset . assetThumb ;
const url = await _thumbFor ( id ) ;
if ( url ) img . src = url ;
else img . style . opacity = '0.2' ;
} ) ;
}
2026-05-18 10:17:31 -04:00
// ── API ──────────────────────────────────────────────────
async function apiGet ( path ) {
const r = await fetch ( '/api/v1' + path , { credentials : 'include' } ) ;
if ( ! r . ok ) throw new Error ( 'HTTP ' + r . status + ' on ' + path ) ;
return r . json ( ) ;
}
async function loadAssets ( ) {
try {
const j = await apiGet ( '/assets?limit=500' ) ;
state . assets = ( j . assets || [ ] ) . filter ( a => a . media _type === 'video' && a . status === 'ready' ) ;
renderAssets ( ) ;
} catch ( e ) {
$ ( 'assetList' ) . innerHTML = '<div class="edit-inspector-empty" style="color:var(--status-red)">Failed: ' + esc ( e . message ) + '</div>' ;
}
}
async function streamUrlFor ( assetId ) {
const j = await apiGet ( '/assets/' + assetId + '/stream' ) ;
return j . url ;
}
// ── Helpers ──────────────────────────────────────────────
function esc ( s ) {
return String ( s ) . replace ( /[&<>"']/g , c => ( { '&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' , "'" : "'" } [ c ] ) ) ;
}
function fmtTime ( sec ) {
if ( sec == null || isNaN ( sec ) ) return '--:--.--' ;
sec = Math . max ( 0 , sec ) ;
const m = Math . floor ( sec / 60 ) ;
const s = sec - m * 60 ;
return String ( m ) . padStart ( 2 , '0' ) + ':' + s . toFixed ( 2 ) . padStart ( 5 , '0' ) ;
}
function thumbUrl ( assetId ) { return '/api/v1/assets/' + assetId + '/thumbnail' ; }
// ── Render: asset library ────────────────────────────────
function renderAssets ( ) {
const q = ( $ ( 'assetSearch' ) . value || '' ) . trim ( ) . toLowerCase ( ) ;
state . filtered = state . assets . filter ( a => ! q || ( a . display _name || '' ) . toLowerCase ( ) . includes ( q ) ) ;
const list = $ ( 'assetList' ) ;
if ( state . filtered . length === 0 ) {
list . innerHTML = '<div class="edit-inspector-empty">' + ( q ? 'No matches.' : 'No video assets yet. Upload from Ingest.' ) + '</div>' ;
return ;
}
list . innerHTML = state . filtered . map ( a => {
const name = a . display _name || a . filename || a . id ;
return '<div class="edit-asset" draggable="true" data-asset-id="' + a . id + '" title="' + esc ( name ) + '">' +
2026-05-18 10:25:53 -04:00
'<img class="edit-asset-thumb" data-asset-thumb="' + a . id + '"/>' +
2026-05-18 10:17:31 -04:00
'<div class="edit-asset-name">' + esc ( name ) + '</div>' +
'</div>' ;
} ) . join ( '' ) ;
2026-05-18 10:25:53 -04:00
hydrateThumbnails ( list ) ;
2026-05-18 10:17:31 -04:00
// Wire drag
list . querySelectorAll ( '.edit-asset' ) . forEach ( el => {
el . addEventListener ( 'dragstart' , ( e ) => {
e . dataTransfer . setData ( 'asset-id' , el . dataset . assetId ) ;
e . dataTransfer . effectAllowed = 'copy' ;
} ) ;
// Double-click also adds the clip
el . addEventListener ( 'dblclick' , ( ) => addClipById ( el . dataset . assetId ) ) ;
} ) ;
}
// ── Render: timeline track ───────────────────────────────
function renderTrack ( ) {
const track = $ ( 'track' ) ;
const empty = $ ( 'trackEmpty' ) ;
if ( state . clips . length === 0 ) {
track . innerHTML = '' ;
track . appendChild ( empty ) ;
empty . classList . remove ( 'drag-over' ) ;
$ ( 'timelineTotal' ) . textContent = '0 clips · 00:00.00' ;
return ;
}
let total = 0 ;
track . innerHTML = state . clips . map ( c => {
const trimmed = Math . max ( 0 , c . outSec - c . inSec ) ;
total += trimmed ;
const inPct = c . durSec > 0 ? ( c . inSec / c . durSec ) * 100 : 0 ;
const outPct = c . durSec > 0 ? ( c . outSec / c . durSec ) * 100 : 100 ;
const isActive = c . uid === state . selectedClipUid ;
return '<div class="edit-clip ' + ( isActive ? 'active' : '' ) + '" data-uid="' + c . uid + '" title="' + esc ( c . name ) + '">' +
'<button class="edit-clip-remove" data-remove="' + c . uid + '" title="Remove from timeline">✕</button>' +
2026-05-18 10:25:53 -04:00
'<img class="edit-clip-thumb" data-asset-thumb="' + c . assetId + '"/>' +
2026-05-18 10:17:31 -04:00
'<div class="edit-clip-bar"><div class="edit-clip-bar-fill" style="left:' + inPct + '%; right:' + ( 100 - outPct ) + '%"></div></div>' +
'<div class="edit-clip-name">' + esc ( c . name ) + '</div>' +
'<div class="edit-clip-meta"><span>' + fmtTime ( c . inSec ) + '</span><span>' + fmtTime ( c . outSec ) + '</span></div>' +
'</div>' ;
} ) . join ( '' ) ;
2026-05-18 10:25:53 -04:00
hydrateThumbnails ( track ) ;
2026-05-18 10:17:31 -04:00
$ ( 'timelineTotal' ) . textContent = state . clips . length + ' clip' + ( state . clips . length === 1 ? '' : 's' ) + ' · ' + fmtTime ( total ) ;
// Wire clicks
track . querySelectorAll ( '.edit-clip' ) . forEach ( el => {
el . addEventListener ( 'click' , ( e ) => {
if ( e . target . matches ( '[data-remove]' ) ) return ;
selectClip ( el . dataset . uid ) ;
} ) ;
} ) ;
track . querySelectorAll ( '[data-remove]' ) . forEach ( btn => {
btn . addEventListener ( 'click' , ( e ) => {
e . stopPropagation ( ) ;
removeClip ( btn . dataset . remove ) ;
} ) ;
} ) ;
}
// ── Render: inspector ────────────────────────────────────
function renderInspector ( ) {
const host = $ ( 'inspector' ) ;
const c = currentClip ( ) ;
if ( ! c ) {
host . innerHTML = '<div class="edit-inspector-empty">Select a clip on the timeline to inspect.</div>' ;
return ;
}
const trimmed = Math . max ( 0 , c . outSec - c . inSec ) ;
host . innerHTML =
'<div class="edit-inspector-section">' +
'<span class="edit-inspector-label">Clip</span>' +
'<div class="edit-inspector-clipname">' + esc ( c . name ) + '</div>' +
'</div>' +
'<div class="edit-inspector-section">' +
'<span class="edit-inspector-label">Trim</span>' +
'<dl class="edit-inspector-stats">' +
'<dt>In</dt><dd>' + fmtTime ( c . inSec ) + '</dd>' +
'<dt>Out</dt><dd>' + fmtTime ( c . outSec ) + '</dd>' +
'<dt>Length</dt><dd>' + fmtTime ( trimmed ) + '</dd>' +
'<dt>Source</dt><dd>' + fmtTime ( c . durSec ) + '</dd>' +
'</dl>' +
'</div>' +
'<div class="edit-inspector-section">' +
'<span class="edit-inspector-label">Actions</span>' +
'<div class="edit-inspector-actions">' +
'<button class="btn btn-ghost btn-sm" id="ipReset">Reset trim</button>' +
'<button class="btn btn-ghost btn-sm" id="ipMoveL">← Move</button>' +
'<button class="btn btn-ghost btn-sm" id="ipMoveR">Move →</button>' +
'<button class="btn btn-ghost btn-sm" id="ipDup">Duplicate</button>' +
2026-05-18 10:25:53 -04:00
'<button class="btn btn-ghost btn-sm" id="ipSplit">Split at playhead</button>' +
2026-05-18 10:17:31 -04:00
'<button class="btn btn-danger btn-sm" id="ipRemove" style="margin-left:auto">Remove</button>' +
'</div>' +
'</div>' ;
$ ( 'ipReset' ) . onclick = ( ) => { c . inSec = 0 ; c . outSec = c . durSec ; refreshAll ( ) ; } ;
$ ( 'ipRemove' ) . onclick = ( ) => removeClip ( c . uid ) ;
$ ( 'ipMoveL' ) . onclick = ( ) => moveClip ( c . uid , - 1 ) ;
$ ( 'ipMoveR' ) . onclick = ( ) => moveClip ( c . uid , 1 ) ;
$ ( 'ipDup' ) . onclick = ( ) => duplicateClip ( c . uid ) ;
2026-05-18 10:25:53 -04:00
const sp = $ ( 'ipSplit' ) ; if ( sp ) sp . onclick = ( ) => splitClip ( c . uid , $ ( 'previewVideo' ) . currentTime ) ;
2026-05-18 10:17:31 -04:00
}
// ── Selection / preview ──────────────────────────────────
function currentClip ( ) {
return state . clips . find ( c => c . uid === state . selectedClipUid ) || null ;
}
async function selectClip ( uid ) {
state . selectedClipUid = uid ;
renderTrack ( ) ;
renderInspector ( ) ;
const c = currentClip ( ) ;
if ( ! c ) return showEmptyPreview ( ) ;
if ( ! c . src ) {
try { c . src = await streamUrlFor ( c . assetId ) ; }
catch ( e ) { console . warn ( 'Stream lookup failed:' , e ) ; }
}
showPreview ( c ) ;
}
function showEmptyPreview ( ) {
$ ( 'previewEmpty' ) . style . display = 'flex' ;
const v = $ ( 'previewVideo' ) ;
v . style . display = 'none' ;
v . removeAttribute ( 'src' ) ;
v . pause ( ) ;
$ ( 'btnPlay' ) . disabled = true ;
$ ( 'btnSetIn' ) . disabled = true ;
$ ( 'btnSetOut' ) . disabled = true ;
2026-05-18 10:25:53 -04:00
$ ( 'btnSplit' ) . disabled = true ;
2026-05-18 10:17:31 -04:00
$ ( 'scrubber' ) . disabled = true ;
$ ( 'timeDisplay' ) . textContent = '--:--.-- / --:--.--' ;
}
function showPreview ( c ) {
$ ( 'previewEmpty' ) . style . display = 'none' ;
const v = $ ( 'previewVideo' ) ;
v . style . display = 'block' ;
if ( v . dataset . src !== c . src ) {
v . src = c . src ;
v . dataset . src = c . src ;
}
$ ( 'btnPlay' ) . disabled = false ;
$ ( 'btnSetIn' ) . disabled = false ;
$ ( 'btnSetOut' ) . disabled = false ;
2026-05-18 10:25:53 -04:00
$ ( 'btnSplit' ) . disabled = false ;
2026-05-18 10:17:31 -04:00
$ ( 'scrubber' ) . disabled = false ;
updateScrubberMarkers ( c ) ;
}
function updateScrubberMarkers ( c ) {
const sc = $ ( 'scrubber' ) ;
const dur = c . durSec || $ ( 'previewVideo' ) . duration || 0 ;
const inPct = dur > 0 ? ( c . inSec / dur ) * 100 : 0 ;
const outPct = dur > 0 ? ( c . outSec / dur ) * 100 : 100 ;
sc . style . setProperty ( '--in-pct' , inPct + '%' ) ;
sc . style . setProperty ( '--out-pct' , outPct + '%' ) ;
}
// ── Mutations ────────────────────────────────────────────
async function addClipById ( assetId ) {
const a = state . assets . find ( x => x . id === assetId ) ;
if ( ! a ) return ;
let src ;
try { src = await streamUrlFor ( assetId ) ; } catch ( e ) { console . warn ( e ) ; }
const durSec = ( a . duration _ms || 0 ) / 1000 ;
const uid = 'c' + ( state . nextUid ++ ) ;
const c = {
uid , assetId ,
name : a . display _name || a . filename || a . id ,
src ,
inSec : 0 ,
outSec : durSec || 0 ,
durSec : durSec || 0 ,
} ;
state . clips . push ( c ) ;
state . selectedClipUid = uid ;
refreshAll ( ) ;
// If we didn't have duration metadata, load the video to grab it
if ( ! c . durSec && src ) {
try {
const probe = document . createElement ( 'video' ) ;
probe . preload = 'metadata' ;
probe . src = src ;
await new Promise ( ( res , rej ) => { probe . onloadedmetadata = res ; probe . onerror = rej ; setTimeout ( rej , 8000 ) ; } ) ;
c . durSec = probe . duration || 0 ;
c . outSec = c . outSec || c . durSec ;
refreshAll ( ) ;
} catch { }
}
}
function removeClip ( uid ) {
state . clips = state . clips . filter ( c => c . uid !== uid ) ;
if ( state . selectedClipUid === uid ) state . selectedClipUid = null ;
refreshAll ( ) ;
}
2026-05-18 10:25:53 -04:00
function splitClip ( uid , atSec ) {
const i = state . clips . findIndex ( c => c . uid === uid ) ;
if ( i < 0 ) return ;
const c = state . clips [ i ] ;
const dur = c . durSec || ( atSec + 0.05 ) ;
const cut = Math . max ( c . inSec + 0.05 , Math . min ( c . outSec - 0.05 , atSec ) ) ;
const left = Object . assign ( { } , c , { uid : 'c' + ( state . nextUid ++ ) , outSec : cut } ) ;
const right = Object . assign ( { } , c , { uid : 'c' + ( state . nextUid ++ ) , inSec : cut } ) ;
state . clips . splice ( i , 1 , left , right ) ;
state . selectedClipUid = right . uid ;
refreshAll ( ) ;
}
2026-05-18 10:17:31 -04:00
function moveClip ( uid , dir ) {
const i = state . clips . findIndex ( c => c . uid === uid ) ;
if ( i < 0 ) return ;
const j = i + dir ;
if ( j < 0 || j >= state . clips . length ) return ;
[ state . clips [ i ] , state . clips [ j ] ] = [ state . clips [ j ] , state . clips [ i ] ] ;
refreshAll ( ) ;
}
function duplicateClip ( uid ) {
const c = state . clips . find ( x => x . uid === uid ) ;
if ( ! c ) return ;
const i = state . clips . indexOf ( c ) ;
const copy = Object . assign ( { } , c , { uid : 'c' + ( state . nextUid ++ ) } ) ;
state . clips . splice ( i + 1 , 0 , copy ) ;
state . selectedClipUid = copy . uid ;
refreshAll ( ) ;
}
function refreshAll ( ) {
renderTrack ( ) ;
renderInspector ( ) ;
const c = currentClip ( ) ;
if ( c ) updateScrubberMarkers ( c ) ;
}
// ── Preview transport ────────────────────────────────────
function bindPreview ( ) {
const v = $ ( 'previewVideo' ) ;
const sc = $ ( 'scrubber' ) ;
const t = $ ( 'timeDisplay' ) ;
v . addEventListener ( 'loadedmetadata' , ( ) => {
const c = currentClip ( ) ;
if ( ! c ) return ;
if ( ! c . durSec ) { c . durSec = v . duration || 0 ; if ( ! c . outSec ) c . outSec = c . durSec ; refreshAll ( ) ; }
sc . max = v . duration ;
t . textContent = fmtTime ( v . currentTime ) + ' / ' + fmtTime ( v . duration ) ;
updateScrubberMarkers ( c ) ;
} ) ;
v . addEventListener ( 'timeupdate' , ( ) => {
const c = currentClip ( ) ;
if ( ! c ) return ;
sc . value = v . currentTime ;
t . textContent = fmtTime ( v . currentTime ) + ' / ' + fmtTime ( v . duration ) ;
// Loop within in/out markers (soft — pause at out)
if ( state . isPlaying && c . outSec > 0 && v . currentTime >= c . outSec ) {
v . pause ( ) ;
state . isPlaying = false ;
updatePlayButton ( ) ;
}
} ) ;
v . addEventListener ( 'play' , ( ) => { state . isPlaying = true ; updatePlayButton ( ) ; } ) ;
v . addEventListener ( 'pause' , ( ) => { state . isPlaying = false ; updatePlayButton ( ) ; } ) ;
sc . addEventListener ( 'input' , ( ) => { v . currentTime = parseFloat ( sc . value ) || 0 ; } ) ;
$ ( 'btnPlay' ) . addEventListener ( 'click' , ( ) => {
if ( v . paused ) { if ( v . currentTime < ( currentClip ( ) ? . inSec || 0 ) ) v . currentTime = currentClip ( ) . inSec ; v . play ( ) ; }
else v . pause ( ) ;
} ) ;
$ ( 'btnSetIn' ) . addEventListener ( 'click' , ( ) => {
const c = currentClip ( ) ; if ( ! c ) return ;
c . inSec = Math . min ( v . currentTime , c . outSec - 0.05 ) ;
refreshAll ( ) ;
} ) ;
2026-05-18 10:25:53 -04:00
$ ( 'btnSplit' ) . addEventListener ( 'click' , ( ) => { const c = currentClip ( ) ; if ( c ) splitClip ( c . uid , $ ( 'previewVideo' ) . currentTime ) ; } ) ;
2026-05-18 10:17:31 -04:00
$ ( 'btnSetOut' ) . addEventListener ( 'click' , ( ) => {
const c = currentClip ( ) ; if ( ! c ) return ;
c . outSec = Math . max ( v . currentTime , c . inSec + 0.05 ) ;
refreshAll ( ) ;
} ) ;
document . addEventListener ( 'keydown' , ( e ) => {
if ( e . target . tagName === 'INPUT' || e . target . tagName === 'TEXTAREA' ) return ;
if ( e . key === ' ' ) { e . preventDefault ( ) ; $ ( 'btnPlay' ) . click ( ) ; }
else if ( e . key === 'i' || e . key === 'I' ) { $ ( 'btnSetIn' ) . click ( ) ; }
else if ( e . key === 'o' || e . key === 'O' ) { $ ( 'btnSetOut' ) . click ( ) ; }
2026-05-18 10:25:53 -04:00
else if ( e . key === 'b' || e . key === 'B' || e . key === 's' || e . key === 'S' ) { const c = currentClip ( ) ; if ( c ) splitClip ( c . uid , $ ( 'previewVideo' ) . currentTime ) ; }
2026-05-18 10:17:31 -04:00
else if ( e . key === 'Delete' || e . key === 'Backspace' ) {
if ( state . selectedClipUid ) removeClip ( state . selectedClipUid ) ;
} else if ( e . key === 'ArrowLeft' && e . altKey ) { if ( state . selectedClipUid ) moveClip ( state . selectedClipUid , - 1 ) ; }
else if ( e . key === 'ArrowRight' && e . altKey ) { if ( state . selectedClipUid ) moveClip ( state . selectedClipUid , 1 ) ; }
} ) ;
}
function updatePlayButton ( ) {
const btn = $ ( 'btnPlay' ) ;
btn . innerHTML = state . isPlaying
? '<svg viewBox="0 0 16 16" fill="currentColor" width="12" height="12"><rect x="3" y="3" width="4" height="10"/><rect x="9" y="3" width="4" height="10"/></svg>'
: '<svg viewBox="0 0 16 16" fill="currentColor" width="12" height="12"><polygon points="4,3 13,8 4,13"/></svg>' ;
}
// ── Drop target ──────────────────────────────────────────
function bindDrop ( ) {
const track = $ ( 'track' ) ;
const empty = $ ( 'trackEmpty' ) ;
[ 'dragenter' , 'dragover' ] . forEach ( ev => {
track . addEventListener ( ev , ( e ) => {
e . preventDefault ( ) ;
e . dataTransfer . dropEffect = 'copy' ;
if ( empty . parentNode === track ) empty . classList . add ( 'drag-over' ) ;
} ) ;
} ) ;
[ 'dragleave' ] . forEach ( ev => {
track . addEventListener ( ev , ( ) => empty . classList . remove ( 'drag-over' ) ) ;
} ) ;
track . addEventListener ( 'drop' , ( e ) => {
e . preventDefault ( ) ;
empty . classList . remove ( 'drag-over' ) ;
const id = e . dataTransfer . getData ( 'asset-id' ) ;
if ( id ) addClipById ( id ) ;
} ) ;
}
// ── Draft save ───────────────────────────────────────────
2026-05-18 10:25:53 -04:00
window . exportEDL = async function ( ) {
if ( state . clips . length === 0 ) { alert ( 'Add some clips to the timeline first.' ) ; return ; }
const total = state . clips . reduce ( ( a , c ) => a + ( c . outSec - c . inSec ) , 0 ) ;
const ok = confirm ( 'Queue this edit for render?\n\n' + state . clips . length + ' clips, ' + fmtTime ( total ) + ' total.\n\nServer-side render is queued for the next phase; for now your edit is saved as a draft you can restore.' ) ;
if ( ! ok ) return ;
saveEDL ( ) ;
} ;
2026-05-18 10:17:31 -04:00
window . saveEDL = function ( ) {
const edl = {
created _at : new Date ( ) . toISOString ( ) ,
clips : state . clips . map ( c => ( {
assetId : c . assetId ,
name : c . name ,
inSec : c . inSec ,
outSec : c . outSec ,
} ) ) ,
} ;
state . drafts . unshift ( edl ) ;
state . drafts = state . drafts . slice ( 0 , 20 ) ;
persistDrafts ( ) ;
// Tiny inline toast
const t = document . createElement ( 'div' ) ;
t . textContent = 'Draft saved locally (' + edl . clips . length + ' clip' + ( edl . clips . length === 1 ? '' : 's' ) + ')' ;
Object . assign ( t . style , {
position : 'fixed' , bottom : '24px' , right : '24px' ,
padding : '10px 14px' , background : 'oklch(15% 0.025 250)' ,
border : '1px solid oklch(45% 0.20 266 / 0.5)' , borderRadius : '6px' ,
color : 'var(--text-primary)' , fontSize : '13px' , zIndex : '999' ,
boxShadow : '0 10px 30px -8px oklch(0% 0 0 / 0.5)' ,
} ) ;
document . body . appendChild ( t ) ;
setTimeout ( ( ) => t . remove ( ) , 2500 ) ;
} ;
// ── Init ─────────────────────────────────────────────────
$ ( 'assetSearch' ) . addEventListener ( 'input' , renderAssets ) ;
bindPreview ( ) ;
bindDrop ( ) ;
loadAssets ( ) ;
// Optional: read ?asset=<id> from URL and auto-add it
const params = new URLSearchParams ( location . search ) ;
const seed = params . get ( 'asset' ) ;
if ( seed ) {
// Wait for assets to load
const check = setInterval ( ( ) => {
if ( state . assets . length > 0 ) {
clearInterval ( check ) ;
addClipById ( seed ) ;
}
} , 200 ) ;
setTimeout ( ( ) => clearInterval ( check ) , 5000 ) ;
}
} ) ( ) ;