2026-04-27 17:33:52 +03:00
const boxPanel = document . querySelector ( ".box-panel[data-box-id]" ) ;
const boxStatus = document . querySelector ( ".box-statusbar span:first-child" ) ;
2026-04-29 02:29:49 +03:00
const boxAddress = document . querySelector ( "#box-address" ) ;
const boxExpiryMeta = document . querySelector ( ".box-meta[data-expires-at]" ) ;
const boxExpiryText = document . querySelector ( "#box-expiry-text" ) ;
const contextMenu = document . querySelector ( "#box-context-menu" ) ;
const docPopup = document . querySelector ( "#doc-popup" ) ;
const docPopupTitle = document . querySelector ( "#doc-popup-title" ) ;
const docPopupBody = document . querySelector ( "#doc-popup-body" ) ;
const docPopupClose = document . querySelector ( "#doc-popup-close" ) ;
const modalBackdrop = document . querySelector ( "#modal-backdrop" ) ;
const toast = document . querySelector ( "#toast" ) ;
2026-04-28 19:41:23 +03:00
const zipOnly = boxPanel && boxPanel . dataset . zipOnly === "true" ;
2026-04-27 17:33:52 +03:00
2026-04-29 02:29:49 +03:00
let contextFile = null ;
2026-04-29 02:35:23 +03:00
let lastStatusSignature = "" ;
2026-04-29 02:29:49 +03:00
function htmlEscape ( value ) {
return String ( value || "" )
. replaceAll ( "&" , "&" )
. replaceAll ( "<" , "<" )
. replaceAll ( ">" , ">" )
. replaceAll ( '"' , """ )
. replaceAll ( "'" , "'" ) ;
}
function showToast ( message , type = "info" ) {
window . WarpBoxUI . toast ( message , type , { target : toast } ) ;
}
function openPopup ( title , html , options = { } ) {
window . WarpBoxUI . openPopup ( title , html , {
... options ,
popup : docPopup ,
title : docPopupTitle ,
body : docPopupBody ,
backdrop : modalBackdrop ,
2026-04-27 17:33:52 +03:00
} ) ;
2026-04-29 02:29:49 +03:00
}
2026-04-27 17:33:52 +03:00
2026-04-29 02:29:49 +03:00
function closePopup ( ) {
window . WarpBoxUI . closePopup ( { popup : docPopup , backdrop : modalBackdrop } ) ;
}
function currentExpiryDate ( ) {
const value = boxExpiryMeta ? . dataset . expiresAt || "" ;
if ( ! value ) return null ;
const date = new Date ( value ) ;
return Number . isNaN ( date . getTime ( ) ) ? null : date ;
}
function formatDuration ( ms ) {
if ( ms <= 0 ) return "expired" ;
const totalSeconds = Math . ceil ( ms / 1000 ) ;
const days = Math . floor ( totalSeconds / 86400 ) ;
const hours = Math . floor ( ( totalSeconds % 86400 ) / 3600 ) ;
const minutes = Math . floor ( ( totalSeconds % 3600 ) / 60 ) ;
const seconds = totalSeconds % 60 ;
if ( days ) return ` ${ days } d ${ hours } h ${ minutes } m ` ;
if ( hours ) return ` ${ hours } h ${ minutes } m ${ seconds } s ` ;
if ( minutes ) return ` ${ minutes } m ${ seconds } s ` ;
return ` ${ seconds } s ` ;
}
function updateExpiryCountdown ( ) {
if ( ! boxExpiryText || ! boxExpiryMeta ) return ;
const expiry = currentExpiryDate ( ) ;
if ( ! expiry ) {
boxExpiryText . textContent = "Expires after one-time download" ;
return ;
}
boxExpiryText . textContent = ` Expires in ${ formatDuration ( expiry . getTime ( ) - Date . now ( ) ) } ` ;
boxExpiryText . title = ` Expires at ${ expiry . toLocaleString ( ) } ` ;
}
function closeContextMenu ( ) {
contextMenu ? . classList . remove ( "is-visible" ) ;
contextMenu ? . setAttribute ( "aria-hidden" , "true" ) ;
}
function showContextMenu ( file , x , y ) {
if ( ! contextMenu ) return ;
contextFile = file ;
contextMenu . style . left = ` ${ Math . min ( x , window . innerWidth - 190 ) } px ` ;
contextMenu . style . top = ` ${ Math . min ( y , window . innerHeight - 98 ) } px ` ;
contextMenu . classList . add ( "is-visible" ) ;
contextMenu . setAttribute ( "aria-hidden" , "false" ) ;
}
function fileData ( item ) {
return {
id : item . dataset . fileId || "" ,
name : item . dataset . name || item . querySelector ( ".box-file-name" ) ? . textContent || "" ,
size : item . dataset . size || "" ,
mime : item . dataset . mime || "" ,
status : item . dataset . status || "" ,
statusLabel : item . querySelector ( ".box-file-meta" ) ? . textContent || "" ,
downloadPath : item . dataset . downloadPath || item . getAttribute ( "href" ) || "" ,
thumbnail : item . dataset . thumbnail || "" ,
canDownload : item . getAttribute ( "aria-disabled" ) !== "true" && item . getAttribute ( "href" ) !== "#" ,
} ;
}
function downloadFile ( item ) {
const data = fileData ( item ) ;
if ( ! data . canDownload ) {
showToast ( zipOnly ? "Individual file downloads are disabled for one-time boxes. Use Download Zip." : "This file is not ready for download yet." , "warning" ) ;
return ;
}
window . location . href = data . downloadPath ;
setTimeout ( refreshBoxStatus , 900 ) ;
}
function previewURL ( data ) {
return data . canDownload ? data . downloadPath : "" ;
}
async function previewFile ( item ) {
const data = fileData ( item ) ;
if ( zipOnly ) {
showToast ( "Previews are disabled for one-time boxes. Use Download Zip." , "warning" ) ;
return ;
}
const url = previewURL ( data ) ;
if ( ! url ) {
showToast ( "This file is not ready to preview yet." , "warning" ) ;
return ;
}
const mime = data . mime . toLowerCase ( ) ;
const name = htmlEscape ( data . name ) ;
if ( mime . startsWith ( "image/" ) ) {
openPopup ( ` ${ data . name } preview ` , ` <img class="preview-frame" src=" ${ htmlEscape ( url ) } " alt=" ${ name } "> ` , { preview : true } ) ;
return ;
}
if ( mime . startsWith ( "video/" ) ) {
openPopup ( ` ${ data . name } preview ` , ` <video class="preview-frame" src=" ${ htmlEscape ( url ) } " controls></video> ` , { preview : true } ) ;
2026-04-27 17:33:52 +03:00
return ;
}
2026-04-29 02:29:49 +03:00
if ( mime . startsWith ( "audio/" ) ) {
openPopup ( ` ${ data . name } preview ` , ` <audio class="preview-frame" src=" ${ htmlEscape ( url ) } " controls></audio> ` , { preview : true } ) ;
return ;
}
if ( mime === "application/pdf" ) {
openPopup ( ` ${ data . name } preview ` , ` <iframe class="preview-frame" src=" ${ htmlEscape ( url ) } " title=" ${ name } "></iframe> ` , { preview : true } ) ;
return ;
}
if ( mime . startsWith ( "text/" ) || /\.(txt|md|json|csv|log|html|css|js)$/i . test ( data . name ) ) {
try {
const response = await fetch ( url ) ;
if ( ! response . ok ) throw new Error ( "Preview failed" ) ;
const text = await response . text ( ) ;
2026-04-29 02:35:23 +03:00
openPopup ( ` ${ data . name } preview ` , ` <pre class="code-block preview-frame is-text"><code> ${ htmlEscape ( text . slice ( 0 , 120000 ) ) } </code></pre> ` , { preview : true } ) ;
2026-04-29 02:29:49 +03:00
} catch ( _ ) {
showToast ( "The browser could not load a text preview." , "error" ) ;
}
return ;
}
showToast ( "This file type cannot be previewed in the browser." , "warning" ) ;
}
function showProperties ( item ) {
const data = fileData ( item ) ;
const url = data . downloadPath ? new URL ( data . downloadPath , window . location . origin ) . toString ( ) : "Not ready" ;
openPopup ( ` ${ data . name } Properties ` , `
< h3 > $ { htmlEscape ( data . name ) } < / h 3 >
< dl class = "properties-grid" >
< dt > Name < / d t > < d d > $ { h t m l E s c a p e ( d a t a . n a m e ) } < / d d >
< dt > Size < / d t > < d d > $ { h t m l E s c a p e ( d a t a . s i z e | | " U n k n o w n " ) } < / d d >
< dt > Type < / d t > < d d > $ { h t m l E s c a p e ( d a t a . m i m e | | " U n k n o w n " ) } < / d d >
< dt > Status < / d t > < d d > $ { h t m l E s c a p e ( d a t a . s t a t u s L a b e l | | d a t a . s t a t u s | | " U n k n o w n " ) } < / d d >
< dt > File ID < / d t > < d d > $ { h t m l E s c a p e ( d a t a . i d ) } < / d d >
< dt > Location < / d t > < d d > $ { h t m l E s c a p e ( u r l ) } < / d d >
< / d l >
` , { properties: true });
}
function updateBoxFile ( file ) {
const item = document . querySelector ( ` .box-file[data-file-id=" ${ file . id } "] ` ) ;
if ( ! item ) return ;
2026-04-27 17:33:52 +03:00
const meta = item . querySelector ( ".box-file-meta" ) ;
2026-04-28 18:44:16 +03:00
const icon = item . querySelector ( ".box-file-icon" ) ;
2026-04-27 17:33:52 +03:00
const isComplete = file . status === "complete" ;
const isFailed = file . status === "failed" ;
item . classList . toggle ( "is-complete" , isComplete ) ;
item . classList . toggle ( "is-failed" , isFailed ) ;
item . classList . toggle ( "is-loading" , ! isComplete && ! isFailed ) ;
2026-04-28 18:44:16 +03:00
item . classList . toggle ( "has-thumbnail" , Boolean ( file . thumbnail _path ) ) ;
2026-04-27 17:33:52 +03:00
item . dataset . status = file . status ;
2026-04-29 02:29:49 +03:00
item . dataset . name = file . name || item . dataset . name || "" ;
item . dataset . size = file . size _label || item . dataset . size || "" ;
item . dataset . mime = file . mime _type || item . dataset . mime || "" ;
item . dataset . downloadPath = file . download _path || item . dataset . downloadPath || "" ;
item . dataset . thumbnail = file . thumbnail _path || "" ;
2026-04-27 17:33:52 +03:00
item . title = file . title ;
2026-04-28 19:41:23 +03:00
if ( isComplete && ! zipOnly ) {
2026-04-27 17:33:52 +03:00
item . href = file . download _path ;
item . setAttribute ( "download" , "" ) ;
item . removeAttribute ( "aria-disabled" ) ;
} else {
item . href = "#" ;
item . removeAttribute ( "download" ) ;
item . setAttribute ( "aria-disabled" , "true" ) ;
}
2026-04-29 02:29:49 +03:00
if ( meta ) meta . textContent = ` ${ file . status _label } · ${ file . size _label } ` ;
if ( icon ) icon . src = file . thumbnail _path || file . icon _path ;
2026-04-27 17:33:52 +03:00
}
async function refreshBoxStatus ( ) {
2026-04-29 02:29:49 +03:00
if ( ! boxPanel ) return false ;
2026-04-27 17:33:52 +03:00
const boxID = boxPanel . dataset . boxId ;
const response = await fetch ( ` /box/ ${ boxID } /status ` ) ;
2026-04-29 02:35:23 +03:00
if ( ! response . ok ) return { changed : false , hasLoadingFiles : true } ;
2026-04-27 17:33:52 +03:00
const result = await response . json ( ) ;
2026-04-29 02:35:23 +03:00
const signature = statusSignature ( result ) ;
const changed = signature !== lastStatusSignature ;
lastStatusSignature = signature ;
2026-04-29 02:29:49 +03:00
if ( boxExpiryMeta && typeof result . expires _at === "string" ) {
boxExpiryMeta . dataset . expiresAt = result . expires _at ;
updateExpiryCountdown ( ) ;
}
2026-04-27 17:33:52 +03:00
result . files . forEach ( updateBoxFile ) ;
if ( boxStatus ) {
const completeCount = result . files . filter ( ( file ) => file . status === "complete" ) . length ;
boxStatus . textContent = ` ${ completeCount } / ${ result . files . length } ready ` ;
}
2026-04-29 02:35:23 +03:00
const hasLoadingFiles = result . files . some ( ( file ) => {
2026-04-28 18:44:16 +03:00
const isUploading = file . status === "pending" || file . status === "uploading" ;
const isWaitingForThumbnail = file . status === "complete" && ! file . thumbnail _status && ! file . thumbnail _path ;
return isUploading || isWaitingForThumbnail || file . thumbnail _status === "processing" ;
} ) ;
2026-04-29 02:35:23 +03:00
return { changed , hasLoadingFiles } ;
}
function statusSignature ( result ) {
const files = Array . isArray ( result . files ) ? result . files : [ ] ;
return JSON . stringify ( {
expiresAt : result . expires _at || "" ,
files : files . map ( ( file ) => ( {
id : file . id ,
status : file . status ,
size : file . size ,
thumbnailPath : file . thumbnail _path || "" ,
thumbnailStatus : file . thumbnail _status || "" ,
downloadPath : file . download _path || "" ,
} ) ) ,
} ) ;
}
function pollingStages ( baseMS ) {
return [
{ interval : baseMS , attempts : 10 } ,
{ interval : baseMS * 2 , attempts : 20 } ,
{ interval : baseMS * 10 , attempts : 100 } ,
] ;
}
function startStagedPolling ( baseMS ) {
const stages = pollingStages ( baseMS ) ;
let stageIndex = 0 ;
let attemptsInStage = 0 ;
let stopped = false ;
const tick = async ( ) => {
if ( stopped ) return ;
const stage = stages [ stageIndex ] ;
try {
const result = await refreshBoxStatus ( ) ;
if ( result . changed ) {
stageIndex = 0 ;
attemptsInStage = 0 ;
} else {
attemptsInStage += 1 ;
if ( attemptsInStage >= stage . attempts ) {
stageIndex += 1 ;
attemptsInStage = 0 ;
if ( stageIndex >= stages . length ) {
stopped = true ;
return ;
}
}
}
} catch ( _ ) {
attemptsInStage += 1 ;
}
if ( ! stopped ) {
window . setTimeout ( tick , stages [ stageIndex ] . interval ) ;
}
} ;
window . setTimeout ( tick , stages [ 0 ] . interval ) ;
2026-04-27 17:33:52 +03:00
}
2026-04-29 02:29:49 +03:00
document . addEventListener ( "click" , ( event ) => {
const action = event . target . closest ( "[data-action]" ) ? . dataset . action ;
if ( action === "fake-close" ) showToast ( "Close clicked. The download window is emotionally attached." , "warning" ) ;
if ( action === "minimize" ) showToast ( "Minimize clicked. WarpBox refuses to disappear quietly." ) ;
if ( action === "toggle-fit" ) {
document . body . classList . toggle ( "fit-window" ) ;
showToast ( "Maximize clicked. The window is doing its best." ) ;
}
const contextAction = event . target . closest ( "[data-context-action]" ) ? . dataset . contextAction ;
if ( contextAction && contextFile ) {
event . preventDefault ( ) ;
const item = contextFile ;
closeContextMenu ( ) ;
if ( contextAction === "preview" ) previewFile ( item ) ;
if ( contextAction === "download" ) downloadFile ( item ) ;
if ( contextAction === "properties" ) showProperties ( item ) ;
return ;
}
if ( ! event . target . closest ( "#box-context-menu" ) ) closeContextMenu ( ) ;
} ) ;
document . querySelectorAll ( ".box-file" ) . forEach ( ( item ) => {
item . addEventListener ( "click" , ( event ) => {
if ( item . getAttribute ( "aria-disabled" ) === "true" ) {
event . preventDefault ( ) ;
showToast ( zipOnly ? "Individual file downloads are disabled for one-time boxes. Use Download Zip." : "This file is not ready yet." , "warning" ) ;
return ;
}
setTimeout ( refreshBoxStatus , 900 ) ;
} ) ;
item . addEventListener ( "contextmenu" , ( event ) => {
event . preventDefault ( ) ;
showContextMenu ( item , event . clientX , event . clientY ) ;
} ) ;
} ) ;
boxAddress ? . addEventListener ( "click" , async ( ) => {
try {
await navigator . clipboard . writeText ( window . location . href ) ;
showToast ( "Current box URL copied." ) ;
} catch ( _ ) {
openPopup ( "Copy box URL" , ` <p>Clipboard access failed. Copy this URL manually.</p><textarea class="copy-fallback-text" readonly> ${ htmlEscape ( window . location . href ) } </textarea> ` ) ;
}
} ) ;
docPopupClose ? . addEventListener ( "click" , closePopup ) ;
modalBackdrop ? . addEventListener ( "click" , closePopup ) ;
document . addEventListener ( "keydown" , ( event ) => {
if ( event . key === "Escape" ) {
closePopup ( ) ;
closeContextMenu ( ) ;
}
} ) ;
updateExpiryCountdown ( ) ;
setInterval ( updateExpiryCountdown , 1000 ) ;
2026-04-27 17:33:52 +03:00
if ( boxPanel ) {
2026-04-27 17:49:19 +03:00
const pollMS = Number . parseInt ( boxPanel . dataset . pollMs , 10 ) || 5000 ;
2026-04-29 02:35:23 +03:00
startStagedPolling ( pollMS ) ;
2026-04-27 17:33:52 +03:00
}