2026-04-29 01:16:17 +03:00
const SETTINGS _KEY = "warpbox.upload.settings.v1" ;
const el = {
form : document . querySelector ( "#upload-form" ) ,
fileInput : document . querySelector ( "#file-upload" ) ,
dropSurface : document . querySelector ( "#drop-surface" ) ,
dropzone : document . querySelector ( "#dropzone" ) ,
fileList : document . querySelector ( "#file-list" ) ,
queueLabel : document . querySelector ( "#queue-label" ) ,
queueSize : document . querySelector ( "#queue-size" ) ,
limitHint : document . querySelector ( "#limit-hint" ) ,
boxSpaceText : document . querySelector ( "#box-space-text" ) ,
boxSpaceBar : document . querySelector ( "#box-space-bar" ) ,
overallBar : document . querySelector ( "#overall-bar" ) ,
overallPercent : document . querySelector ( "#overall-percent" ) ,
shareLink : document . querySelector ( "#share-link" ) ,
copyButton : document . querySelector ( "#copy-button" ) ,
startButton : document . querySelector ( "#start-button" ) ,
statusText : document . querySelector ( "#status-text" ) ,
toast : document . querySelector ( "#toast" ) ,
terminal : document . querySelector ( "#terminal-box" ) ,
copyCurlButton : document . querySelector ( "#copy-curl-button" ) ,
docPopup : document . querySelector ( "#doc-popup" ) ,
modalBackdrop : document . querySelector ( "#modal-backdrop" ) ,
docPopupTitle : document . querySelector ( "#doc-popup-title" ) ,
docPopupBody : document . querySelector ( "#doc-popup-body" ) ,
docPopupClose : document . querySelector ( "#doc-popup-close" ) ,
expiry : document . querySelector ( "#expiry-select" ) ,
password : document . querySelector ( "#password-input" ) ,
optionsForm : document . querySelector ( "#box-options-form" ) ,
maxViews : document . querySelector ( "#max-views" ) ,
boxName : document . querySelector ( "#box-name" ) ,
customSlug : document . querySelector ( "#custom-slug" ) ,
downloadPage : document . querySelector ( "#download-page" ) ,
allowZip : document . querySelector ( "#allow-zip" ) ,
allowPreview : document . querySelector ( "#allow-preview" ) ,
keepFilenames : document . querySelector ( "#keep-filenames" ) ,
privateBox : document . querySelector ( "#private-box" ) ,
apiKeyMode : document . querySelector ( "#api-key-mode" ) ,
apiKeyInput : document . querySelector ( "#api-key-input" ) ,
apiKeyRow : document . querySelector ( "#api-key-row" ) ,
apiKeyState : document . querySelector ( "#api-key-state" ) ,
} ;
const uploadsEnabled = el . form ? . dataset . uploadsEnabled === "true" ;
const defaultRetention = el . form ? . dataset . defaultRetention || "10s" ;
const maxFileBytes = numberFromDataset ( el . form ? . dataset . maxFileBytes ) ;
const maxBoxBytes = numberFromDataset ( el . form ? . dataset . maxBoxBytes ) ;
2026-04-28 19:41:23 +03:00
const oneTimeRetentionKey = "one-time" ;
2026-04-25 18:57:08 +03:00
2026-04-29 01:16:17 +03:00
let files = [ ] ;
let shareUrl = "" ;
let uploadLocked = false ;
2026-04-25 18:57:08 +03:00
let statusTimer = null ;
2026-04-29 01:16:17 +03:00
let pendingDuplicateFiles = [ ] ;
let apiKeyTimer = null ;
2026-04-29 01:42:41 +03:00
let completedImpactKeys = new Set ( ) ;
let overallImpactDone = false ;
2026-04-25 18:46:16 +03:00
2026-04-29 01:16:17 +03:00
function numberFromDataset ( value ) {
const number = Number . parseInt ( value || "0" , 10 ) ;
return Number . isFinite ( number ) && number > 0 ? number : 0 ;
2026-04-28 18:44:16 +03:00
}
2026-04-25 18:46:16 +03:00
function formatBytes ( bytes ) {
2026-04-29 01:16:17 +03:00
if ( ! bytes ) return "0 B" ;
const units = [ "B" , "KB" , "MB" , "GB" , "TB" ] ;
let value = bytes ;
let unit = 0 ;
while ( value >= 1024 && unit < units . length - 1 ) {
value /= 1024 ;
unit += 1 ;
2026-04-25 18:46:16 +03:00
}
2026-04-29 01:16:17 +03:00
return ` ${ value . toFixed ( value >= 10 || unit === 0 ? 0 : 1 ) } ${ units [ unit ] } ` ;
}
2026-04-25 18:46:16 +03:00
2026-04-29 01:16:17 +03:00
function htmlEscape ( value ) {
return String ( value )
. replaceAll ( "&" , "&" )
. replaceAll ( "<" , "<" )
. replaceAll ( ">" , ">" )
. replaceAll ( '"' , """ )
. replaceAll ( "'" , "'" ) ;
}
2026-04-25 18:46:16 +03:00
2026-04-29 01:16:17 +03:00
function shellQuote ( value ) {
return ` ' ${ String ( value ) . replaceAll ( "'" , "'\\''" ) } ' ` ;
2026-04-25 18:46:16 +03:00
}
2026-04-29 01:16:17 +03:00
function totalBytes ( ) {
return files . reduce ( ( sum , item ) => sum + item . file . size , 0 ) ;
}
2026-04-27 18:37:05 +03:00
2026-04-29 01:16:17 +03:00
function uploadedBytes ( ) {
return files . reduce ( ( sum , item ) => sum + item . loaded , 0 ) ;
}
2026-04-27 18:37:05 +03:00
2026-04-29 01:16:17 +03:00
function overallProgress ( ) {
const total = totalBytes ( ) ;
return total ? Math . round ( ( uploadedBytes ( ) / total ) * 100 ) : 0 ;
}
2026-04-27 18:37:05 +03:00
2026-04-29 01:16:17 +03:00
function oversizedFiles ( ) {
return maxFileBytes ? files . filter ( ( item ) => item . file . size > maxFileBytes ) : [ ] ;
}
2026-04-27 18:37:05 +03:00
2026-04-29 01:16:17 +03:00
function isOverBoxQuota ( ) {
return maxBoxBytes ? totalBytes ( ) > maxBoxBytes : false ;
}
2026-04-27 18:37:05 +03:00
2026-04-29 01:16:17 +03:00
function hasQuotaError ( ) {
return isOverBoxQuota ( ) || oversizedFiles ( ) . length > 0 ;
}
2026-04-27 18:37:05 +03:00
2026-04-29 01:16:17 +03:00
function normalizedFileName ( name ) {
return String ( name || "" ) . trim ( ) . toLowerCase ( ) ;
}
2026-04-27 18:37:05 +03:00
2026-04-29 01:16:17 +03:00
function splitNameForIncrement ( name ) {
const value = String ( name || "file" ) ;
const dot = value . lastIndexOf ( "." ) ;
if ( dot > 0 && dot < value . length - 1 ) return [ value . slice ( 0 , dot ) , value . slice ( dot ) ] ;
return [ value , "" ] ;
}
2026-04-27 18:37:05 +03:00
2026-04-29 01:16:17 +03:00
function nextIncrementedFileName ( name , usedNames ) {
const [ base , ext ] = splitNameForIncrement ( name ) ;
let index = 2 ;
let candidate = ` ${ base } ( ${ index } ) ${ ext } ` ;
while ( usedNames . has ( normalizedFileName ( candidate ) ) ) {
index += 1 ;
candidate = ` ${ base } ( ${ index } ) ${ ext } ` ;
2026-04-27 18:37:05 +03:00
}
2026-04-29 01:16:17 +03:00
usedNames . add ( normalizedFileName ( candidate ) ) ;
return candidate ;
}
2026-04-27 18:37:05 +03:00
2026-04-29 01:16:17 +03:00
function makeQueuedFile ( file , displayName = file . name ) {
return {
file ,
displayName ,
loaded : 0 ,
uploaded : false ,
failed : false ,
error : "" ,
row : null ,
boxID : "" ,
boxFile : null ,
previewURL : file . type ? . startsWith ( "image/" ) ? URL . createObjectURL ( file ) : "" ,
} ;
}
function iconForFile ( file ) {
const filename = file . name || "" ;
const mimeType = file . type || "" ;
const extension = filename . includes ( "." ) ? filename . slice ( filename . lastIndexOf ( "." ) ) . toLowerCase ( ) : "" ;
if ( extension === ".exe" ) return "/static/img/icons/Program Files Icons - PNG/MSONSEXT.DLL_14_6-0.png" ;
if ( mimeType . startsWith ( "image/" ) ) return "/static/img/sprites/bitmap.png" ;
if ( mimeType . startsWith ( "video/" ) || mimeType . startsWith ( "audio/" ) ) return "/static/img/icons/netshow_notransm-1.png" ;
if ( mimeType . startsWith ( "text/" ) || extension === ".md" ) return "/static/img/sprites/notepad_file-1.png" ;
if ( mimeType . includes ( "zip" ) || mimeType . includes ( "compressed" ) || [ ".rar" , ".7z" , ".tar" , ".gz" ] . includes ( extension ) ) return "/static/img/icons/Windows Icons - PNG/zipfldr.dll_14_101-0.png" ;
if ( [ ".ttf" , ".otf" , ".woff" , ".woff2" ] . includes ( extension ) ) return "/static/img/sprites/font.png" ;
if ( extension === ".pdf" ) return "/static/img/sprites/journal.png" ;
if ( [ ".html" , ".css" , ".js" ] . includes ( extension ) ) return "/static/img/sprites/frame_web-0.png" ;
2026-04-27 18:37:05 +03:00
return "/static/img/icons/Windows Icons - PNG/ole2.dll_14_DEFICON.png" ;
}
2026-04-29 01:16:17 +03:00
function setStatus ( message ) {
if ( el . statusText ) el . statusText . textContent = message ;
}
function showToast ( message , type = "info" ) {
2026-04-29 02:29:49 +03:00
window . WarpBoxUI . toast ( message , type , { target : el . toast } ) ;
}
function closeMenus ( ) {
document . querySelectorAll ( ".menu-item.is-open" ) . forEach ( ( node ) => {
node . classList . remove ( "is-open" ) ;
node . querySelector ( ".menu-button" ) ? . setAttribute ( "aria-expanded" , "false" ) ;
} ) ;
2026-04-25 18:46:16 +03:00
}
2026-04-29 01:42:41 +03:00
function disabledReasonFor ( target ) {
2026-04-29 02:29:49 +03:00
const control = target . closest ( "[data-disabled-reason], button, input, select, textarea, .upload-dropzone, .option-check, .option-row" ) ;
2026-04-29 01:42:41 +03:00
if ( ! control ) return "" ;
2026-04-29 02:29:49 +03:00
if ( control . classList . contains ( "option-check" ) || control . classList . contains ( "option-row" ) ) {
const nested = control . querySelector ( "input, select, textarea" ) ;
if ( nested ? . disabled || nested ? . readOnly || nested ? . getAttribute ( "aria-disabled" ) === "true" ) {
return nested . dataset . disabledReason || "This option is disabled right now." ;
}
}
2026-04-29 01:42:41 +03:00
if ( control . classList . contains ( "upload-dropzone" ) && uploadLocked ) {
return control . dataset . disabledReason || "The current box is sealed after upload. Press Clear to start a new box." ;
}
if ( control . disabled || control . readOnly || control . getAttribute ( "aria-disabled" ) === "true" ) {
return control . dataset . disabledReason || control . title || "This control is disabled right now." ;
}
return "" ;
}
function announceDisabledReason ( event ) {
const reason = disabledReasonFor ( event . target ) ;
if ( ! reason ) return false ;
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
2026-04-29 02:29:49 +03:00
closeMenus ( ) ;
2026-04-29 01:42:41 +03:00
showToast ( reason , "warning" ) ;
setStatus ( reason ) ;
return true ;
}
2026-04-25 18:57:08 +03:00
function stopStatusAnimation ( ) {
if ( statusTimer ) {
clearInterval ( statusTimer ) ;
statusTimer = null ;
}
}
function animateUploadStatus ( getPrefix ) {
let dotCount = 0 ;
stopStatusAnimation ( ) ;
statusTimer = setInterval ( ( ) => {
dotCount = ( dotCount % 3 ) + 1 ;
2026-04-29 01:16:17 +03:00
setStatus ( ` ${ getPrefix ( ) } Uploading ${ "." . repeat ( dotCount ) } ` ) ;
2026-04-25 18:57:08 +03:00
} , 350 ) ;
}
2026-04-29 01:16:17 +03:00
function setShareUrl ( url ) {
shareUrl = url ? new URL ( url , window . location . origin ) . toString ( ) : "" ;
if ( ! el . shareLink || ! el . copyButton ) return ;
el . shareLink . textContent = shareUrl || "Not created yet" ;
el . shareLink . href = shareUrl || "#" ;
el . shareLink . title = shareUrl ;
el . shareLink . classList . toggle ( "is-empty" , ! shareUrl ) ;
el . shareLink . setAttribute ( "aria-disabled" , shareUrl ? "false" : "true" ) ;
2026-04-29 02:29:49 +03:00
el . copyButton . disabled = false ;
el . copyButton . setAttribute ( "aria-disabled" , shareUrl ? "false" : "true" ) ;
2026-04-29 01:16:17 +03:00
el . copyButton . dataset . disabledReason = shareUrl ? "" : "There is no share URL yet. Start an upload first." ;
2026-04-29 02:29:49 +03:00
updateDisabledReasons ( ) ;
2026-04-29 01:16:17 +03:00
updateTerminal ( ) ;
updateCurrentStep ( ) ;
2026-04-25 18:57:08 +03:00
}
2026-04-25 18:46:16 +03:00
2026-04-29 01:16:17 +03:00
function setOverallProgress ( percent ) {
const clamped = Math . max ( 0 , Math . min ( 100 , percent ) ) ;
const display = ` ${ Math . round ( clamped ) } % ` ;
if ( el . overallBar ) el . overallBar . style . width = display ;
if ( el . overallPercent ) el . overallPercent . textContent = display ;
2026-04-28 19:41:23 +03:00
}
2026-04-29 01:42:41 +03:00
function flashProgressBar ( bar ) {
if ( ! bar ) return ;
bar . classList . remove ( "just-completed" ) ;
void bar . offsetWidth ;
bar . classList . add ( "just-completed" ) ;
setTimeout ( ( ) => bar . classList . remove ( "just-completed" ) , 620 ) ;
}
2026-04-29 01:16:17 +03:00
function setRowProgress ( item , percent ) {
const bar = item . row ? . querySelector ( ".upload-progress-bar" ) ;
if ( bar ) bar . style . width = ` ${ Math . max ( 0 , Math . min ( 100 , percent ) ) } % ` ;
2026-04-28 19:41:23 +03:00
}
2026-04-29 01:16:17 +03:00
function updateCurrentStep ( ) {
const hasFiles = files . length > 0 ;
const allDone = hasFiles && files . every ( ( item ) => item . uploaded ) ;
el . dropzone ? . classList . toggle ( "is-current-step" , uploadsEnabled && ! hasFiles && ! uploadLocked ) ;
el . startButton ? . classList . toggle ( "is-current-step" , uploadsEnabled && hasFiles && ! allDone && ! uploadLocked && ! hasQuotaError ( ) ) ;
document . querySelector ( ".upload-result" ) ? . classList . toggle ( "is-current-step" , allDone && Boolean ( shareUrl ) ) ;
2026-04-27 17:20:57 +03:00
}
2026-04-29 01:16:17 +03:00
function quotaWarningMessage ( incoming = [ ] ) {
const combined = [ ... files , ... incoming ] ;
const tooBig = maxFileBytes ? combined . filter ( ( item ) => item . file . size > maxFileBytes ) : [ ] ;
const total = combined . reduce ( ( sum , item ) => sum + item . file . size , 0 ) ;
if ( tooBig . length ) {
const list = tooBig . slice ( 0 , 4 ) . map ( ( item ) => ` ${ item . displayName } ( ${ formatBytes ( item . file . size ) } ) ` ) . join ( ", " ) ;
const more = tooBig . length > 4 ? ` and ${ tooBig . length - 4 } more ` : "" ;
return ` These files are over the single-file limit of ${ formatBytes ( maxFileBytes ) } : ${ list } ${ more } . Remove them before uploading. ` ;
2026-04-27 17:26:57 +03:00
}
2026-04-29 01:16:17 +03:00
if ( maxBoxBytes && total > maxBoxBytes ) {
return ` This box is ${ formatBytes ( total - maxBoxBytes ) } over the ${ formatBytes ( maxBoxBytes ) } limit. Remove some files before uploading. ` ;
2026-04-27 17:26:57 +03:00
}
2026-04-29 01:16:17 +03:00
return "" ;
2026-04-27 17:26:57 +03:00
}
2026-04-29 01:16:17 +03:00
function updateLimitHint ( ) {
if ( ! el . limitHint ) return ;
const parts = [ ] ;
if ( maxBoxBytes ) parts . push ( ` Max box: ${ formatBytes ( maxBoxBytes ) } ` ) ;
if ( maxFileBytes ) parts . push ( ` max file: ${ formatBytes ( maxFileBytes ) } ` ) ;
parts . push ( "links expire automatically" ) ;
el . limitHint . textContent = parts . join ( " · " ) ;
2026-04-27 17:26:57 +03:00
}
2026-04-29 01:16:17 +03:00
function updateQuota ( ) {
const used = totalBytes ( ) ;
const limitText = maxBoxBytes ? ` / ${ formatBytes ( maxBoxBytes ) } ` : "" ;
const overQuota = isOverBoxQuota ( ) ;
const overFile = oversizedFiles ( ) . length > 0 ;
const percent = maxBoxBytes ? Math . min ( 100 , Math . round ( ( used / maxBoxBytes ) * 100 ) ) : 0 ;
document . querySelector ( ".upload-quota" ) ? . classList . toggle ( "is-quota-warning" , overQuota || overFile ) ;
if ( el . boxSpaceText ) el . boxSpaceText . textContent = ` ${ formatBytes ( used ) } ${ limitText } ${ overQuota ? " - over quota" : "" } ` ;
if ( el . boxSpaceBar ) {
el . boxSpaceBar . style . width = ` ${ percent } % ` ;
el . boxSpaceBar . classList . toggle ( "is-over-quota" , overQuota || overFile ) ;
2026-04-25 18:46:16 +03:00
}
2026-04-25 18:57:08 +03:00
}
2026-04-29 01:16:17 +03:00
function updateQueueSummary ( ) {
const count = files . length ;
if ( el . queueLabel ) el . queueLabel . textContent = count ? ` ${ count } file ${ count === 1 ? "" : "s" } selected ` : "No files selected" ;
if ( el . queueSize ) el . queueSize . textContent = ` ${ formatBytes ( totalBytes ( ) ) } total ` ;
}
function updateOverallProgress ( ) {
const uploadedCount = files . filter ( ( item ) => item . uploaded ) . length ;
const percent = overallProgress ( ) ;
setOverallProgress ( percent >= 100 && uploadedCount < files . length ? 99 : percent ) ;
2026-04-29 01:42:41 +03:00
if ( percent >= 100 && files . length && ! overallImpactDone ) {
overallImpactDone = true ;
flashProgressBar ( el . overallBar ) ;
}
2026-04-25 18:57:08 +03:00
}
2026-04-29 01:16:17 +03:00
function createFileRow ( item , index ) {
2026-04-25 18:57:08 +03:00
const row = document . createElement ( "div" ) ;
row . className = "upload-file-row" ;
2026-04-29 01:16:17 +03:00
row . dataset . index = String ( index ) ;
row . classList . toggle ( "has-thumbnail" , Boolean ( item . previewURL ) ) ;
row . classList . toggle ( "is-too-large" , maxFileBytes > 0 && item . file . size > maxFileBytes ) ;
row . classList . toggle ( "is-working" , item . loaded > 0 && ! item . uploaded && ! item . failed ) ;
row . classList . toggle ( "is-uploaded" , item . uploaded ) ;
row . classList . toggle ( "is-failed" , item . failed ) ;
row . title = item . error || "" ;
2026-04-25 18:57:08 +03:00
2026-04-27 18:37:05 +03:00
const icon = document . createElement ( "img" ) ;
2026-04-25 18:57:08 +03:00
icon . className = "upload-file-icon" ;
2026-04-29 01:16:17 +03:00
icon . src = item . previewURL || iconForFile ( item . file ) ;
2026-04-27 18:37:05 +03:00
icon . alt = "" ;
2026-04-25 18:57:08 +03:00
icon . setAttribute ( "aria-hidden" , "true" ) ;
const name = document . createElement ( "span" ) ;
name . className = "upload-file-name" ;
2026-04-29 01:16:17 +03:00
name . textContent = item . displayName ;
name . title = item . displayName ;
2026-04-25 18:57:08 +03:00
const size = document . createElement ( "span" ) ;
size . className = "upload-file-size" ;
2026-04-29 01:16:17 +03:00
size . textContent = formatBytes ( item . file . size ) ;
const remove = document . createElement ( "button" ) ;
remove . className = "win98-button upload-file-remove" ;
remove . type = "button" ;
remove . textContent = "× " ;
remove . dataset . remove = String ( index ) ;
remove . title = uploadLocked ? "This file cannot be removed because this upload box was already created." : "Remove file" ;
2026-04-29 02:29:49 +03:00
remove . disabled = false ;
remove . setAttribute ( "aria-disabled" , uploadLocked ? "true" : "false" ) ;
2026-04-29 01:42:41 +03:00
remove . dataset . disabledReason = uploadLocked ? "Files cannot be removed after the box is created. Press Clear to start another upload." : "" ;
2026-04-25 18:57:08 +03:00
const progress = document . createElement ( "span" ) ;
progress . className = "upload-progress" ;
2026-04-29 01:16:17 +03:00
progress . setAttribute ( "aria-label" , ` Upload progress ${ Math . round ( item . file . size ? ( item . loaded / item . file . size ) * 100 : 0 ) } percent ` ) ;
2026-04-25 18:57:08 +03:00
const progressBar = document . createElement ( "span" ) ;
progressBar . className = "upload-progress-bar" ;
2026-04-29 01:16:17 +03:00
progressBar . style . width = ` ${ item . uploaded ? 100 : item . failed ? 100 : Math . max ( 0 , Math . min ( 100 , item . file . size ? ( item . loaded / item . file . size ) * 100 : 0 ) ) } % ` ;
2026-04-25 18:57:08 +03:00
progress . append ( progressBar ) ;
2026-04-29 01:16:17 +03:00
row . append ( icon , name , size , remove , progress ) ;
item . row = row ;
2026-04-25 18:57:08 +03:00
return row ;
}
2026-04-29 01:16:17 +03:00
function renderFiles ( ) {
if ( ! el . fileList ) return ;
el . fileList . replaceChildren ( ) ;
if ( ! files . length ) {
const empty = document . createElement ( "p" ) ;
empty . className = "upload-empty-state" ;
empty . textContent = uploadsEnabled
? "No files in the box yet. Drop files here, use File > Add files, or click the dropzone."
: "Guest uploads are disabled." ;
el . fileList . append ( empty ) ;
} else {
const fragment = document . createDocumentFragment ( ) ;
files . forEach ( ( item , index ) => fragment . append ( createFileRow ( item , index ) ) ) ;
el . fileList . append ( fragment ) ;
}
2026-04-25 18:57:08 +03:00
2026-04-29 01:16:17 +03:00
updateQueueSummary ( ) ;
updateQuota ( ) ;
updateOverallProgress ( ) ;
updateTerminal ( ) ;
updateDisabledReasons ( ) ;
updateCurrentStep ( ) ;
}
function duplicateFileReport ( incoming = [ ] ) {
const used = new Set ( files . map ( ( item ) => normalizedFileName ( item . displayName ) ) ) ;
const duplicates = [ ] ;
const unique = [ ] ;
incoming . forEach ( ( item ) => {
const key = normalizedFileName ( item . displayName ) ;
if ( used . has ( key ) ) {
duplicates . push ( item ) ;
return ;
}
used . add ( key ) ;
unique . push ( item ) ;
} ) ;
return { unique , duplicates } ;
}
2026-04-25 18:46:16 +03:00
2026-04-29 01:16:17 +03:00
function addFiles ( fileList ) {
if ( ! uploadsEnabled ) {
showToast ( "Guest uploads are disabled." , "warning" ) ;
return ;
}
if ( uploadLocked ) {
showToast ( "This box is sealed. Clear it to create a fresh upload." , "warning" ) ;
2026-04-25 18:46:16 +03:00
return ;
}
2026-04-29 01:16:17 +03:00
const incoming = Array . from ( fileList || [ ] ) . map ( ( file ) => makeQueuedFile ( file ) ) ;
if ( ! incoming . length ) return ;
const { unique , duplicates } = duplicateFileReport ( incoming ) ;
if ( unique . length ) {
files . push ( ... unique ) ;
setShareUrl ( "" ) ;
renderFiles ( ) ;
const warning = quotaWarningMessage ( ) ;
if ( warning ) showWarningDialog ( "Quota warning" , warning ) ;
}
if ( duplicates . length ) showDuplicateDialog ( duplicates ) ;
2026-04-25 18:46:16 +03:00
2026-04-29 01:16:17 +03:00
if ( unique . length ) setStatus ( ` ${ unique . length } file ${ unique . length === 1 ? "" : "s" } added to queue ` ) ;
if ( duplicates . length && ! unique . length ) setStatus ( ` ${ duplicates . length } duplicate file ${ duplicates . length === 1 ? "" : "s" } need your choice ` ) ;
}
2026-04-25 18:46:16 +03:00
2026-04-29 01:16:17 +03:00
function showDuplicateDialog ( duplicates ) {
pendingDuplicateFiles = duplicates ;
const list = duplicates . map ( ( item ) => ` <li><strong> ${ htmlEscape ( item . displayName ) } </strong> <span> ${ formatBytes ( item . file . size ) } </span></li> ` ) . join ( "" ) ;
2026-04-29 01:42:41 +03:00
showTemplatePopup ( "Duplicate file names" , "duplicate" , { list } )
. then ( ( ) => document . querySelector ( "#duplicate-append" ) ? . focus ( ) ) ;
2026-04-29 01:16:17 +03:00
showToast ( "Duplicate names found. Choose skip or append numbers." , "warning" ) ;
}
function appendPendingDuplicates ( ) {
if ( ! pendingDuplicateFiles . length ) return ;
const used = new Set ( files . map ( ( item ) => normalizedFileName ( item . displayName ) ) ) ;
pendingDuplicateFiles . forEach ( ( item ) => {
item . displayName = nextIncrementedFileName ( item . displayName , used ) ;
files . push ( item ) ;
} ) ;
const count = pendingDuplicateFiles . length ;
pendingDuplicateFiles = [ ] ;
closeDoc ( ) ;
setShareUrl ( "" ) ;
renderFiles ( ) ;
showToast ( "Duplicate files added with numbered names." , "info" ) ;
setStatus ( ` ${ count } duplicate file ${ count === 1 ? "" : "s" } added with numbered names ` ) ;
}
function removeFile ( index ) {
if ( uploadLocked ) {
showToast ( "Box already created. Clear it before editing the queue." , "warning" ) ;
2026-04-25 18:46:16 +03:00
return ;
}
2026-04-29 01:16:17 +03:00
const [ removed ] = files . splice ( index , 1 ) ;
if ( removed ? . previewURL ) URL . revokeObjectURL ( removed . previewURL ) ;
setShareUrl ( "" ) ;
renderFiles ( ) ;
setStatus ( "File removed from queue" ) ;
}
2026-04-25 18:46:16 +03:00
2026-04-29 01:16:17 +03:00
function clearQueue ( ) {
files . forEach ( ( item ) => {
if ( item . previewURL ) URL . revokeObjectURL ( item . previewURL ) ;
2026-04-25 18:57:08 +03:00
} ) ;
2026-04-29 01:16:17 +03:00
files = [ ] ;
pendingDuplicateFiles = [ ] ;
uploadLocked = false ;
2026-04-29 01:42:41 +03:00
completedImpactKeys = new Set ( ) ;
overallImpactDone = false ;
2026-04-29 01:16:17 +03:00
stopStatusAnimation ( ) ;
setBoxOptionsLocked ( false ) ;
setShareUrl ( "" ) ;
if ( el . fileInput ) {
el . fileInput . value = "" ;
el . fileInput . disabled = ! uploadsEnabled ;
}
el . dropzone ? . classList . remove ( "is-locked" ) ;
renderFiles ( ) ;
setStatus ( uploadsEnabled ? "Queue cleared" : "Guest uploads are disabled" ) ;
showToast ( "Queue cleared." ) ;
}
2026-04-25 18:57:08 +03:00
2026-04-29 01:16:17 +03:00
function confirmClearQueue ( ) {
if ( ! files . length && ! shareUrl ) {
showToast ( "Nothing to clear." ) ;
return ;
}
2026-04-29 01:42:41 +03:00
showTemplatePopup ( "Clear WarpBox?" , "clear" )
. then ( ( ) => document . querySelector ( "#confirm-clear-no" ) ? . focus ( ) ) ;
2026-04-25 18:57:08 +03:00
}
2026-04-25 18:46:16 +03:00
2026-04-25 18:57:08 +03:00
async function createBox ( ) {
2026-04-27 17:33:52 +03:00
const response = await fetch ( "/box" , {
method : "POST" ,
2026-04-29 01:16:17 +03:00
headers : { "Content-Type" : "application/json" } ,
2026-04-27 17:33:52 +03:00
body : JSON . stringify ( {
2026-04-29 01:16:17 +03:00
retention _key : el . expiry ? . value || defaultRetention ,
password : el . password ? . value || "" ,
allow _zip : isOneTimeDownloadSelected ( ) || ! el . allowZip || el . allowZip . checked ,
files : files . map ( ( item ) => ( { name : item . displayName , size : item . file . size } ) ) ,
2026-04-27 17:33:52 +03:00
} ) ,
} ) ;
2026-04-25 18:46:16 +03:00
2026-04-29 01:16:17 +03:00
const result = await readJSON ( response ) ;
if ( ! response . ok ) throw new Error ( result . error || "Could not create upload box" ) ;
return result ;
2026-04-25 18:57:08 +03:00
}
2026-04-25 18:46:16 +03:00
2026-04-29 01:16:17 +03:00
async function readJSON ( response ) {
try {
return await response . json ( ) ;
} catch ( _ ) {
return { } ;
2026-04-27 17:33:52 +03:00
}
2026-04-29 01:16:17 +03:00
}
2026-04-27 17:33:52 +03:00
2026-04-29 01:16:17 +03:00
async function markFileStatus ( item , status ) {
if ( ! item . boxID || ! item . boxFile ) return ;
try {
await fetch ( ` /box/ ${ item . boxID } /files/ ${ item . boxFile . id } /status ` , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON . stringify ( { status } ) ,
} ) ;
} catch ( _ ) {
// Best effort only. The upload endpoint also marks hard failures.
}
}
function setFileFailed ( item , message ) {
item . failed = true ;
item . uploaded = false ;
item . error = message || "Failed to upload" ;
item . loaded = item . file . size ;
item . row ? . classList . remove ( "is-working" , "is-uploaded" ) ;
item . row ? . classList . add ( "is-failed" ) ;
if ( item . row ) item . row . title = item . error ;
setRowProgress ( item , 100 ) ;
updateOverallProgress ( ) ;
2026-04-27 17:33:52 +03:00
}
2026-04-29 01:42:41 +03:00
function markCompletedImpact ( item ) {
const key = item . boxFile ? . id || item . displayName ;
if ( completedImpactKeys . has ( key ) ) return ;
completedImpactKeys . add ( key ) ;
flashProgressBar ( item . row ? . querySelector ( ".upload-progress-bar" ) ) ;
}
2026-04-29 01:16:17 +03:00
function uploadFile ( item , onComplete ) {
2026-04-25 18:57:08 +03:00
return new Promise ( ( resolve , reject ) => {
const xhr = new XMLHttpRequest ( ) ;
const formData = new FormData ( ) ;
2026-04-29 01:16:17 +03:00
formData . append ( "file" , item . file , item . displayName ) ;
2026-04-25 18:46:16 +03:00
2026-04-29 01:16:17 +03:00
xhr . open ( "POST" , item . boxFile . upload _path ) ;
2026-04-25 18:46:16 +03:00
2026-04-25 18:57:08 +03:00
xhr . upload . addEventListener ( "loadstart" , ( ) => {
2026-04-29 01:16:17 +03:00
item . loaded = 0 ;
item . failed = false ;
item . uploaded = false ;
item . row ? . classList . remove ( "is-failed" , "is-uploaded" ) ;
item . row ? . classList . add ( "is-working" ) ;
setRowProgress ( item , 2 ) ;
2026-04-27 17:26:57 +03:00
updateOverallProgress ( ) ;
2026-04-25 18:57:08 +03:00
} ) ;
2026-04-25 18:46:16 +03:00
2026-04-25 18:57:08 +03:00
xhr . upload . addEventListener ( "progress" , ( event ) => {
2026-04-29 01:16:17 +03:00
if ( ! event . lengthComputable ) return ;
item . loaded = Math . min ( event . loaded , item . file . size ) ;
2026-04-27 17:33:52 +03:00
const percent = ( event . loaded / event . total ) * 100 ;
2026-04-29 01:16:17 +03:00
setRowProgress ( item , percent >= 100 ? 99 : percent ) ;
updateOverallProgress ( ) ;
2026-04-25 18:57:08 +03:00
} ) ;
2026-04-29 01:16:17 +03:00
xhr . addEventListener ( "load" , async ( ) => {
2026-04-25 18:57:08 +03:00
if ( xhr . status < 200 || xhr . status >= 300 ) {
2026-04-29 01:16:17 +03:00
let message = "Upload failed" ;
try {
message = JSON . parse ( xhr . responseText ) . error || message ;
} catch ( _ ) { }
setFileFailed ( item , message ) ;
await markFileStatus ( item , "failed" ) ;
reject ( new Error ( message ) ) ;
2026-04-25 18:57:08 +03:00
return ;
}
2026-04-29 01:16:17 +03:00
item . uploaded = true ;
item . failed = false ;
item . loaded = item . file . size ;
item . row ? . classList . remove ( "is-working" , "is-failed" ) ;
item . row ? . classList . add ( "is-uploaded" ) ;
if ( item . row ) item . row . title = "Uploaded" ;
setRowProgress ( item , 100 ) ;
2026-04-29 01:42:41 +03:00
markCompletedImpact ( item ) ;
2026-04-29 01:16:17 +03:00
try {
const result = JSON . parse ( xhr . responseText ) ;
if ( result . file ) {
item . boxFile = result . file ;
const icon = item . row ? . querySelector ( ".upload-file-icon" ) ;
if ( icon && result . file . thumbnail _path ) {
item . row . classList . add ( "has-thumbnail" ) ;
icon . src = result . file . thumbnail _path ;
} else if ( icon && result . file . icon _path && ! item . previewURL ) {
icon . src = result . file . icon _path ;
}
}
} catch ( _ ) { }
2026-04-27 17:26:57 +03:00
updateOverallProgress ( ) ;
2026-04-25 18:57:08 +03:00
onComplete ( ) ;
resolve ( ) ;
} ) ;
2026-04-29 01:16:17 +03:00
xhr . addEventListener ( "error" , async ( ) => {
setFileFailed ( item , "Network error while uploading" ) ;
await markFileStatus ( item , "failed" ) ;
reject ( new Error ( "Network error while uploading" ) ) ;
2026-04-25 18:57:08 +03:00
} ) ;
2026-04-29 01:16:17 +03:00
xhr . addEventListener ( "abort" , async ( ) => {
setFileFailed ( item , "Upload cancelled" ) ;
await markFileStatus ( item , "failed" ) ;
2026-04-27 17:33:52 +03:00
reject ( new Error ( "Upload cancelled" ) ) ;
} ) ;
2026-04-29 01:16:17 +03:00
markFileStatus ( item , "uploading" ) ;
2026-04-25 18:57:08 +03:00
xhr . send ( formData ) ;
} ) ;
2026-04-25 18:46:16 +03:00
}
2026-04-29 01:16:17 +03:00
async function startUpload ( ) {
if ( ! uploadsEnabled ) {
showToast ( "Guest uploads are disabled." , "warning" ) ;
return ;
}
if ( uploadLocked ) {
showToast ( "Upload already started. Press Clear to create another box." , "warning" ) ;
return ;
}
if ( ! files . length ) {
showWarningDialog ( "No files selected" , "There are no files selected. Please select files to upload." ) ;
showToast ( "No files selected. Please select files to upload." , "warning" ) ;
setStatus ( "No files selected" ) ;
return ;
}
if ( hasQuotaError ( ) ) {
showWarningDialog ( "Over maximum upload size" , quotaWarningMessage ( ) || "Over maximum upload size." ) ;
showToast ( "Over maximum upload size." , "error" ) ;
return ;
}
uploadLocked = true ;
setBoxOptionsLocked ( true ) ;
if ( el . fileInput ) el . fileInput . disabled = true ;
el . dropzone ? . classList . add ( "is-locked" ) ;
setShareUrl ( "" ) ;
files . forEach ( ( item ) => {
item . loaded = 0 ;
item . uploaded = false ;
item . failed = false ;
item . error = "" ;
2026-04-25 18:46:16 +03:00
} ) ;
2026-04-29 01:42:41 +03:00
completedImpactKeys = new Set ( ) ;
overallImpactDone = false ;
2026-04-29 01:16:17 +03:00
renderFiles ( ) ;
let completedCount = 0 ;
const totalCount = files . length ;
const statusPrefix = ( ) => ` ${ completedCount } / ${ totalCount } ` ;
setStatus ( ` ${ statusPrefix ( ) } Uploading. ` ) ;
animateUploadStatus ( statusPrefix ) ;
try {
const box = await createBox ( ) ;
setShareUrl ( box . box _url ) ;
files . forEach ( ( item , index ) => {
item . boxID = box . box _id ;
item . boxFile = box . files [ index ] ;
item . displayName = item . boxFile ? . name || item . displayName ;
const icon = item . row ? . querySelector ( ".upload-file-icon" ) ;
if ( icon && item . boxFile ? . thumbnail _path ) {
item . row . classList . add ( "has-thumbnail" ) ;
icon . src = item . boxFile . thumbnail _path ;
} else if ( icon && item . boxFile ? . icon _path && ! item . previewURL ) {
icon . src = item . boxFile . icon _path ;
}
} ) ;
const results = await Promise . allSettled ( files . map ( ( item ) => uploadFile ( item , ( ) => { completedCount += 1 ; } ) ) ) ;
stopStatusAnimation ( ) ;
2026-04-25 18:46:16 +03:00
2026-04-29 01:16:17 +03:00
const failedCount = results . filter ( ( result ) => result . status === "rejected" ) . length ;
if ( failedCount > 0 ) {
setStatus ( ` ${ completedCount } / ${ totalCount } uploaded, ${ failedCount } failed ` ) ;
showToast ( ` ${ failedCount } file ${ failedCount === 1 ? "" : "s" } failed. The share URL contains the successful files. ` , "error" ) ;
renderFiles ( ) ;
2026-04-27 18:18:53 +03:00
return ;
}
2026-04-29 01:16:17 +03:00
setOverallProgress ( 100 ) ;
setStatus ( ` ${ completedCount } / ${ totalCount } uploaded. Share URL created. Press Clear to start another upload. ` ) ;
showToast ( "Upload complete. Share URL created." ) ;
renderFiles ( ) ;
} catch ( error ) {
stopStatusAnimation ( ) ;
uploadLocked = false ;
setBoxOptionsLocked ( false ) ;
if ( el . fileInput ) el . fileInput . disabled = ! uploadsEnabled ;
el . dropzone ? . classList . remove ( "is-locked" ) ;
setShareUrl ( "" ) ;
setStatus ( error . message || "Upload failed" ) ;
showToast ( error . message || "Upload failed" , "error" ) ;
renderFiles ( ) ;
}
2026-04-27 18:18:53 +03:00
}
2026-04-29 01:16:17 +03:00
function isOneTimeDownloadSelected ( ) {
return el . expiry ? . value === oneTimeRetentionKey ;
2026-04-28 19:41:23 +03:00
}
2026-04-29 01:16:17 +03:00
function syncZipForRetention ( ) {
if ( ! el . allowZip ) return ;
if ( isOneTimeDownloadSelected ( ) ) {
el . allowZip . checked = true ;
el . allowZip . disabled = true ;
} else if ( ! uploadLocked ) {
el . allowZip . disabled = false ;
}
}
2026-04-25 18:46:16 +03:00
2026-04-29 01:16:17 +03:00
function setBoxOptionsLocked ( locked ) {
const controls = [ el . expiry , el . password , el . maxViews , el . boxName , el . customSlug , el . downloadPage , el . allowZip , el . allowPreview , el . keepFilenames , el . privateBox , el . apiKeyMode , el . apiKeyInput ] . filter ( Boolean ) ;
el . optionsForm ? . classList . toggle ( "is-locked" , locked ) ;
controls . forEach ( ( control ) => {
control . dataset . disabledReason = locked ? "Box Options are locked because this box was already created. Press Clear to start another upload." : "" ;
if ( control . tagName === "INPUT" && ! [ "checkbox" , "radio" , "file" ] . includes ( control . type ) ) {
control . readOnly = locked ;
} else {
control . disabled = locked ;
}
2026-04-25 18:46:16 +03:00
} ) ;
2026-04-29 01:16:17 +03:00
if ( el . password ) el . password . type = locked ? "password" : "text" ;
if ( ! locked ) {
syncZipForRetention ( ) ;
syncApiKeyField ( ) ;
}
updateDisabledReasons ( ) ;
}
2026-04-25 18:46:16 +03:00
2026-04-29 01:16:17 +03:00
function updateDisabledReasons ( ) {
if ( el . startButton ) {
let reason = "" ;
if ( ! uploadsEnabled ) reason = "Guest uploads are disabled." ;
else if ( uploadLocked ) reason = "This upload already started. Press Clear to create another box." ;
else if ( hasQuotaError ( ) ) reason = "Over maximum upload size. Remove highlighted files or clear some files." ;
else if ( ! files . length ) reason = "There are no files selected. Please select files to upload." ;
2026-04-29 02:29:49 +03:00
el . startButton . disabled = false ;
el . startButton . setAttribute ( "aria-disabled" , reason ? "true" : "false" ) ;
2026-04-29 01:16:17 +03:00
el . startButton . dataset . disabledReason = reason ;
el . startButton . title = reason ;
}
2026-04-29 01:42:41 +03:00
if ( el . fileInput ) {
el . fileInput . dataset . disabledReason = uploadLocked ? "The current box is sealed after upload. Press Clear to start a new box." : ( ! uploadsEnabled ? "Guest uploads are disabled." : "" ) ;
}
if ( el . dropzone ) {
el . dropzone . dataset . disabledReason = uploadLocked ? "The current box is sealed after upload. Press Clear to start a new box." : ( ! uploadsEnabled ? "Guest uploads are disabled." : "" ) ;
}
2026-04-29 02:29:49 +03:00
document . querySelectorAll ( '[data-action="start-upload"]' ) . forEach ( ( button ) => {
const reason = el . startButton ? . dataset . disabledReason || "" ;
button . setAttribute ( "aria-disabled" , reason ? "true" : "false" ) ;
button . dataset . disabledReason = reason ;
} ) ;
document . querySelectorAll ( '[data-action="browse"]' ) . forEach ( ( button ) => {
const reason = uploadLocked ? "The current box is sealed after upload. Press Clear to start a new box." : ( ! uploadsEnabled ? "Guest uploads are disabled." : "" ) ;
button . setAttribute ( "aria-disabled" , reason ? "true" : "false" ) ;
button . dataset . disabledReason = reason ;
} ) ;
document . querySelectorAll ( '[data-action="copy-link"]' ) . forEach ( ( button ) => {
button . setAttribute ( "aria-disabled" , shareUrl ? "false" : "true" ) ;
button . dataset . disabledReason = shareUrl ? "" : "There is no share URL yet. Start an upload first." ;
} ) ;
2026-04-29 01:16:17 +03:00
}
2026-04-25 18:46:16 +03:00
2026-04-29 01:16:17 +03:00
function saveSettings ( ) {
2026-04-29 02:29:49 +03:00
const apiKey = el . apiKeyMode ? . checked && validApiKey ( el . apiKeyInput ? . value || "" ) ? el . apiKeyInput . value . trim ( ) : "" ;
2026-04-29 01:16:17 +03:00
const settings = {
maxViews : el . maxViews ? . value || "" ,
allowPreview : Boolean ( el . allowPreview ? . checked ) ,
keepFilenames : Boolean ( el . keepFilenames ? . checked ) ,
privateBox : Boolean ( el . privateBox ? . checked ) ,
apiKeyMode : Boolean ( el . apiKeyMode ? . checked ) ,
2026-04-29 02:29:49 +03:00
apiKey ,
2026-04-29 01:16:17 +03:00
} ;
localStorage . setItem ( SETTINGS _KEY , JSON . stringify ( settings ) ) ;
}
2026-04-25 18:46:16 +03:00
2026-04-29 01:16:17 +03:00
function loadSettings ( ) {
let settings = { } ;
try {
settings = JSON . parse ( localStorage . getItem ( SETTINGS _KEY ) || "{}" ) ;
} catch ( _ ) { }
if ( el . maxViews ) el . maxViews . value = settings . maxViews || "" ;
if ( el . allowPreview ) el . allowPreview . checked = settings . allowPreview !== false ;
if ( el . keepFilenames ) el . keepFilenames . checked = settings . keepFilenames !== false ;
if ( el . privateBox ) el . privateBox . checked = Boolean ( settings . privateBox ) ;
if ( el . apiKeyMode ) el . apiKeyMode . checked = Boolean ( settings . apiKeyMode ) ;
2026-04-29 02:29:49 +03:00
if ( el . apiKeyInput ) el . apiKeyInput . value = validApiKey ( settings . apiKey || "" ) ? settings . apiKey : "" ;
2026-04-29 01:16:17 +03:00
syncZipForRetention ( ) ;
syncApiKeyField ( ) ;
2026-04-29 02:29:49 +03:00
saveSettings ( ) ;
2026-04-29 01:16:17 +03:00
}
function syncMenuChecks ( ) {
2026-04-29 02:29:49 +03:00
updateDisabledReasons ( ) ;
2026-04-25 18:46:16 +03:00
}
2026-04-29 01:16:17 +03:00
function syncApiKeyField ( ) {
const enabled = Boolean ( el . apiKeyMode ? . checked ) && ! uploadLocked ;
el . apiKeyRow ? . classList . toggle ( "is-visible" , Boolean ( el . apiKeyMode ? . checked ) ) ;
if ( el . apiKeyInput ) {
el . apiKeyInput . disabled = ! enabled ;
el . apiKeyInput . dataset . disabledReason = enabled ? "" : "Enable Use API key for larger quota before typing an API key." ;
}
validateApiKeyField ( ) ;
}
2026-04-25 18:46:16 +03:00
2026-04-29 01:16:17 +03:00
function validateApiKeyField ( ) {
if ( ! el . apiKeyInput || ! el . apiKeyState ) return ;
clearTimeout ( apiKeyTimer ) ;
const wrapper = el . apiKeyInput . closest ( ".api-key-field" ) ;
wrapper ? . classList . remove ( "is-checking" ) ;
2026-04-25 18:46:16 +03:00
2026-04-29 01:16:17 +03:00
if ( ! el . apiKeyMode ? . checked ) {
el . apiKeyState . textContent = "" ;
return ;
}
const value = el . apiKeyInput . value . trim ( ) ;
if ( ! value ) {
el . apiKeyState . textContent = "waiting" ;
2026-04-29 02:29:49 +03:00
saveSettings ( ) ;
2026-04-29 01:16:17 +03:00
return ;
}
2026-04-27 18:18:53 +03:00
2026-04-29 01:16:17 +03:00
el . apiKeyInput . disabled = true ;
wrapper ? . classList . add ( "is-checking" ) ;
el . apiKeyState . textContent = "checking" ;
apiKeyTimer = setTimeout ( ( ) => {
wrapper ? . classList . remove ( "is-checking" ) ;
el . apiKeyInput . disabled = uploadLocked ;
2026-04-29 02:29:49 +03:00
if ( validApiKey ( value ) ) {
el . apiKeyState . textContent = "saved locally" ;
saveSettings ( ) ;
} else {
el . apiKeyInput . value = "" ;
el . apiKeyState . textContent = "invalid" ;
saveSettings ( ) ;
showToast ( "Invalid API key removed. Paste a valid API key to save it." , "warning" ) ;
}
2026-04-29 01:16:17 +03:00
} , 650 ) ;
}
2026-04-25 18:57:08 +03:00
2026-04-29 02:29:49 +03:00
function validApiKey ( value ) {
return /^[A-Za-z0-9._-]{12,}$/ . test ( String ( value || "" ) . trim ( ) ) ;
}
2026-04-29 01:16:17 +03:00
function slugify ( value ) {
return String ( value || "" )
. toLowerCase ( )
. replace ( /[^a-z0-9-]+/g , "-" )
. replace ( /-+/g , "-" )
. replace ( /^-|-$/g , "" )
. slice ( 0 , 32 ) ;
}
2026-04-25 18:46:16 +03:00
2026-04-29 01:42:41 +03:00
function sanitizeSlugInput ( value ) {
return String ( value || "" )
. toLowerCase ( )
. replace ( /[^a-z0-9-]/g , "" )
. replace ( /-+/g , "-" )
. slice ( 0 , 32 ) ;
}
2026-04-29 01:16:17 +03:00
function syncSlugFromName ( force = false ) {
if ( ! el . customSlug || ! el . boxName ) return ;
if ( force || ! el . customSlug . value || el . customSlug . dataset . auto === "true" ) {
el . customSlug . value = slugify ( el . boxName . value ) ;
el . customSlug . dataset . auto = "true" ;
}
saveSettings ( ) ;
updateTerminal ( ) ;
2026-04-25 18:46:16 +03:00
}
2026-04-27 17:20:57 +03:00
2026-04-29 01:16:17 +03:00
function randomPassword ( ) {
if ( ! el . password || uploadLocked ) return ;
el . password . value = ` ${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 6 ) } ` ;
saveSettings ( ) ;
updateTerminal ( ) ;
setStatus ( "Generated a password" ) ;
}
function randomBoxName ( ) {
if ( ! el . boxName || uploadLocked ) return ;
2026-04-29 01:42:41 +03:00
const adjectives = [ "Neon" , "Turbo" , "Quiet" , "Cosmic" , "Lucky" , "Midnight" , "Pixel" , "Rapid" ] ;
const nouns = [ "Floppy Disk" , "Archive Box" , "Packet Portal" , "Upload Folder" , "Cache Drive" , "Release Bundle" ] ;
el . boxName . value = ` ${ adjectives [ Math . floor ( Math . random ( ) * adjectives . length ) ] } ${ nouns [ Math . floor ( Math . random ( ) * nouns . length ) ] } ` ;
2026-04-29 01:16:17 +03:00
syncSlugFromName ( true ) ;
setStatus ( "Generated a local box name" ) ;
}
function getCurlCommand ( { full = true } = { } ) {
const args = [ ] ;
const selectedFiles = files . length ? files : [ { displayName : "build.zip" } ] ;
const previewLimit = full ? selectedFiles . length : 4 ;
selectedFiles . slice ( 0 , previewLimit ) . forEach ( ( item ) => args . push ( ` -F ${ shellQuote ( ` files=@ ${ item . displayName } ` ) } ` ) ) ;
const hiddenFileCount = ! full && selectedFiles . length > previewLimit ? selectedFiles . length - previewLimit : 0 ;
args . push ( ` -F ${ shellQuote ( ` retention= ${ el . expiry ? . value || defaultRetention } ` ) } ` ) ;
if ( el . password ? . value ) args . push ( ` -F ${ shellQuote ( "password=YOUR_PASSWORD" ) } ` ) ;
if ( el . allowZip && ! el . allowZip . checked ) args . push ( ` -F ${ shellQuote ( "allow_zip=false" ) } ` ) ;
const commandLines = [ "curl" ] ;
if ( el . apiKeyMode ? . checked ) commandLines . push ( ` -H ${ shellQuote ( "Authorization: Bearer YOUR_API_KEY" ) } ` ) ;
commandLines . push ( ... args , ` ${ window . location . origin } /upload ` ) ;
const command = commandLines . join ( " \\\n" ) ;
return hiddenFileCount ? ` ${ command } \n # and ${ hiddenFileCount } other files included when copying ` : command ;
}
function updateTerminal ( ) {
if ( ! el . terminal ) return ;
const command = getCurlCommand ( { full : false } ) ;
el . terminal . innerHTML = ` <span class="terminal-muted">warpbox@cli</span>:~ $ ${ htmlEscape ( command ) } ` ;
}
async function copyText ( kind , value , openUrl = "" ) {
if ( ! value ) {
showToast ( ` No ${ kind . toLowerCase ( ) } yet. ` , "warning" ) ;
return ;
}
try {
await navigator . clipboard . writeText ( value ) ;
showToast ( ` ${ kind } copied to clipboard. ` ) ;
setStatus ( ` Copied ${ kind . toLowerCase ( ) } ` ) ;
} catch ( _ ) {
showCopyFallback ( kind , value , openUrl ) ;
}
}
function showCopyFallback ( kind , value , openUrl ) {
2026-04-29 01:42:41 +03:00
const openLink = openUrl ? ` <a class="win98-button" href=" ${ htmlEscape ( openUrl ) } " target="_blank" rel="noreferrer">Open</a> ` : "" ;
showTemplatePopup ( ` ${ kind } copy failed ` , "copy-failed" , {
value : htmlEscape ( value ) ,
openLink ,
} ) ;
2026-04-29 01:16:17 +03:00
}
function quotaWarningHtml ( message ) {
const tooLarge = oversizedFiles ( ) ;
const parts = [ ] ;
if ( tooLarge . length ) {
parts . push ( "<p class=\"quota-dialog-summary\"><strong>Single-file limit exceeded.</strong> Remove these files before uploading.</p>" ) ;
parts . push ( ` <ol class="quota-dialog-list"> ${ tooLarge . map ( ( item ) => ` <li><strong> ${ htmlEscape ( item . displayName ) } </strong> <span> ${ formatBytes ( item . file . size ) } / max ${ formatBytes ( maxFileBytes ) } </span></li> ` ) . join ( "" ) } </ol> ` ) ;
}
if ( isOverBoxQuota ( ) ) {
parts . push ( ` <p class="quota-dialog-summary"><strong>Box quota exceeded.</strong> Current total is ${ formatBytes ( totalBytes ( ) ) } . The limit is ${ formatBytes ( maxBoxBytes ) } . Remove ${ formatBytes ( totalBytes ( ) - maxBoxBytes ) } or more.</p> ` ) ;
}
if ( ! parts . length ) parts . push ( ` <p> ${ htmlEscape ( message ) } </p> ` ) ;
return parts . join ( "" ) ;
}
function showWarningDialog ( title , message ) {
2026-04-29 01:42:41 +03:00
showTemplatePopup ( title , "warning" , {
title : htmlEscape ( title ) ,
content : quotaWarningHtml ( message ) ,
} ) ;
2026-04-29 01:16:17 +03:00
}
function openPopup ( title , html , about = false ) {
2026-04-29 02:29:49 +03:00
window . WarpBoxUI . openPopup ( title , html , {
about ,
popup : el . docPopup ,
title : el . docPopupTitle ,
body : el . docPopupBody ,
backdrop : el . modalBackdrop ,
} ) ;
2026-04-29 01:16:17 +03:00
}
function closeDoc ( ) {
2026-04-29 02:29:49 +03:00
window . WarpBoxUI . closePopup ( { popup : el . docPopup , backdrop : el . modalBackdrop } ) ;
2026-04-29 01:16:17 +03:00
}
2026-04-29 01:42:41 +03:00
async function showTemplatePopup ( title , templateName , data = { } , about = false ) {
try {
const html = await window . WBPopups . renderTemplate ( templateName , data ) ;
openPopup ( title , html , about ) ;
} catch ( error ) {
showToast ( error . message || ` Could not load ${ title } . ` , "error" ) ;
}
}
2026-04-29 01:16:17 +03:00
2026-04-29 01:42:41 +03:00
function popupTemplateData ( name ) {
const data = { origin : window . location . origin } ;
if ( name !== "dailyQuota" ) return data ;
return {
... data ,
boxLimit : maxBoxBytes ? formatBytes ( maxBoxBytes ) : "No configured limit" ,
boxPercent : maxBoxBytes ? Math . min ( 100 , Math . round ( ( totalBytes ( ) / maxBoxBytes ) * 100 ) ) : 0 ,
fileLimit : maxFileBytes ? formatBytes ( maxFileBytes ) : "No configured limit" ,
filePercent : oversizedFiles ( ) . length ? 100 : 0 ,
} ;
}
async function openDoc ( name ) {
try {
const doc = await window . WBPopups . renderDoc ( name , popupTemplateData ( name ) ) ;
if ( ! doc ) return ;
openPopup ( doc . title , doc . html , doc . about ) ;
setStatus ( ` ${ doc . title } opened ` ) ;
} catch ( error ) {
showToast ( error . message || "Could not load help window." , "error" ) ;
}
2026-04-29 01:16:17 +03:00
}
document . addEventListener ( "click" , ( event ) => {
2026-04-29 01:42:41 +03:00
if ( announceDisabledReason ( event ) ) return ;
2026-04-29 01:16:17 +03:00
const menuButton = event . target . closest ( ".menu-button" ) ;
if ( menuButton ) {
const item = menuButton . closest ( ".menu-item" ) ;
const isOpen = item . classList . contains ( "is-open" ) ;
2026-04-29 02:29:49 +03:00
closeMenus ( ) ;
2026-04-29 01:16:17 +03:00
item . classList . toggle ( "is-open" , ! isOpen ) ;
menuButton . setAttribute ( "aria-expanded" , String ( ! isOpen ) ) ;
return ;
}
const action = event . target . closest ( "[data-action]" ) ? . dataset . action ;
if ( action ) {
2026-04-29 02:29:49 +03:00
closeMenus ( ) ;
2026-04-29 01:16:17 +03:00
if ( action === "browse" ) el . fileInput ? . click ( ) ;
if ( action === "start-upload" ) startUpload ( ) ;
if ( action === "copy-link" ) copyText ( "Share URL" , shareUrl , shareUrl ) ;
if ( action === "clear" ) confirmClearQueue ( ) ;
if ( action === "toggle-delete-once" && el . expiry ? . querySelector ( ` option[value=" ${ oneTimeRetentionKey } "] ` ) ) {
el . expiry . value = isOneTimeDownloadSelected ( ) ? defaultRetention : oneTimeRetentionKey ;
syncZipForRetention ( ) ;
saveSettings ( ) ;
syncMenuChecks ( ) ;
updateTerminal ( ) ;
}
if ( action === "random-password" ) randomPassword ( ) ;
if ( action === "random-box-name" ) randomBoxName ( ) ;
if ( action === "clear-password" && el . password && ! uploadLocked ) {
el . password . value = "" ;
saveSettings ( ) ;
updateTerminal ( ) ;
2026-04-27 17:20:57 +03:00
}
2026-04-29 01:16:17 +03:00
if ( action === "toggle-page" && el . downloadPage && ! uploadLocked ) {
el . downloadPage . checked = ! el . downloadPage . checked ;
saveSettings ( ) ;
syncMenuChecks ( ) ;
}
if ( action === "help" || action === "side-help" ) openDoc ( "faq" ) ;
2026-04-29 01:42:41 +03:00
if ( action === "coming-soon" ) showToast ( "Coming Soon, not implemented just yet." ) ;
if ( action === "fake-close" ) showToast ( "Close button denied. The upload window is staying open." , "warning" ) ;
if ( action === "minimize" ) showToast ( "Minimize requested. WarpBox stays visible so your queue is safe." ) ;
if ( action === "toggle-fit" ) {
document . body . classList . toggle ( "fit-window" ) ;
showToast ( "Maximize requested. The pixel rectangle feels important now." ) ;
}
if ( action === "side-close" ) showToast ( "Box Options refuses to leave. Settings stay visible." ) ;
if ( action === "side-help" ) showToast ( "Terminal help opened. Copy the command and feed it files." ) ;
if ( action === "side-folder-close" ) showToast ( "The folder window saw that click and chose denial." ) ;
2026-04-29 01:16:17 +03:00
return ;
}
const doc = event . target . closest ( "[data-doc]" ) ? . dataset . doc ;
if ( doc ) {
openDoc ( doc ) ;
return ;
}
const remove = event . target . closest ( "[data-remove]" ) ;
if ( remove ) {
removeFile ( Number ( remove . dataset . remove ) ) ;
return ;
}
if ( event . target . id === "duplicate-append" ) appendPendingDuplicates ( ) ;
if ( event . target . id === "duplicate-skip" ) {
pendingDuplicateFiles = [ ] ;
closeDoc ( ) ;
showToast ( "Duplicate files skipped." ) ;
}
if ( event . target . id === "confirm-clear-yes" ) {
closeDoc ( ) ;
clearQueue ( ) ;
}
if ( event . target . id === "confirm-clear-no" || event . target . id === "fallback-close" ) closeDoc ( ) ;
if ( ! event . target . closest ( ".menu-item" ) ) {
2026-04-29 02:29:49 +03:00
closeMenus ( ) ;
2026-04-29 01:16:17 +03:00
}
} ) ;
2026-04-29 01:42:41 +03:00
document . addEventListener ( "mousedown" , ( event ) => {
announceDisabledReason ( event ) ;
} , true ) ;
document . querySelectorAll ( ".menu-item" ) . forEach ( ( item ) => {
item . addEventListener ( "mouseenter" , ( ) => {
if ( ! document . querySelector ( ".menu-item.is-open" ) ) return ;
2026-04-29 02:29:49 +03:00
closeMenus ( ) ;
2026-04-29 01:42:41 +03:00
item . classList . add ( "is-open" ) ;
item . querySelector ( ".menu-button" ) ? . setAttribute ( "aria-expanded" , "true" ) ;
} ) ;
} ) ;
2026-04-29 01:16:17 +03:00
el . fileInput ? . addEventListener ( "change" , ( ) => addFiles ( el . fileInput . files ) ) ;
[ el . dropSurface , el . dropzone ] . filter ( Boolean ) . forEach ( ( target ) => {
target . addEventListener ( "dragover" , ( event ) => {
event . preventDefault ( ) ;
el . dropzone ? . classList . add ( "is-dragging" ) ;
} ) ;
target . addEventListener ( "dragleave" , ( ) => el . dropzone ? . classList . remove ( "is-dragging" ) ) ;
target . addEventListener ( "drop" , ( event ) => {
event . preventDefault ( ) ;
el . dropzone ? . classList . remove ( "is-dragging" ) ;
addFiles ( event . dataTransfer . files ) ;
} ) ;
} ) ;
el . dropzone ? . addEventListener ( "keydown" , ( event ) => {
if ( event . key === "Enter" || event . key === " " ) {
event . preventDefault ( ) ;
el . fileInput ? . click ( ) ;
}
} ) ;
el . form ? . addEventListener ( "submit" , ( event ) => {
event . preventDefault ( ) ;
startUpload ( ) ;
} ) ;
el . copyButton ? . addEventListener ( "click" , ( ) => copyText ( "Share URL" , shareUrl , shareUrl ) ) ;
el . copyCurlButton ? . addEventListener ( "click" , ( ) => copyText ( "cURL command" , getCurlCommand ( { full : true } ) ) ) ;
el . docPopupClose ? . addEventListener ( "click" , closeDoc ) ;
el . modalBackdrop ? . addEventListener ( "click" , closeDoc ) ;
2026-04-29 02:29:49 +03:00
el . maxViews ? . addEventListener ( "wheel" , ( event ) => {
if ( el . maxViews . disabled || el . maxViews . readOnly ) return ;
event . preventDefault ( ) ;
const delta = event . deltaY < 0 ? 1 : - 1 ;
const modifier = event . ctrlKey && event . shiftKey ? 50 : event . shiftKey ? 15 : event . ctrlKey ? 5 : 1 ;
const min = Number . parseInt ( el . maxViews . min || "1" , 10 ) ;
const max = Number . parseInt ( el . maxViews . max || "9999" , 10 ) ;
const current = Number . parseInt ( el . maxViews . value || String ( min ) , 10 ) ;
el . maxViews . value = String ( Math . max ( min , Math . min ( max , current + ( delta * modifier ) ) ) ) ;
saveSettings ( ) ;
updateTerminal ( ) ;
} ) ;
el . apiKeyInput ? . addEventListener ( "keydown" , ( event ) => {
const allowed = event . ctrlKey || event . metaKey || event . altKey || [
"Tab" ,
"Shift" ,
"Control" ,
"Alt" ,
"Meta" ,
"Escape" ,
"ArrowLeft" ,
"ArrowRight" ,
"ArrowUp" ,
"ArrowDown" ,
"Home" ,
"End" ,
"PageUp" ,
"PageDown" ,
] . includes ( event . key ) ;
if ( allowed ) return ;
event . preventDefault ( ) ;
showToast ( "Only pasting the API key is supported." , "warning" ) ;
setStatus ( "Only pasting the API key is supported" ) ;
} ) ;
el . apiKeyInput ? . addEventListener ( "paste" , ( ) => {
setTimeout ( validateApiKeyField , 0 ) ;
} ) ;
2026-04-29 01:16:17 +03:00
[ el . expiry , el . password , el . maxViews , el . boxName , el . customSlug , el . downloadPage , el . allowZip , el . allowPreview , el . keepFilenames , el . privateBox , el . apiKeyMode , el . apiKeyInput ] . filter ( Boolean ) . forEach ( ( control ) => {
control . addEventListener ( "input" , ( ) => {
if ( control === el . boxName ) syncSlugFromName ( ) ;
2026-04-29 01:42:41 +03:00
if ( control === el . customSlug ) {
const clean = sanitizeSlugInput ( el . customSlug . value ) ;
if ( el . customSlug . value !== clean ) el . customSlug . value = clean ;
el . customSlug . dataset . auto = "false" ;
}
2026-04-29 01:16:17 +03:00
if ( control === el . apiKeyInput ) validateApiKeyField ( ) ;
saveSettings ( ) ;
updateTerminal ( ) ;
} ) ;
control . addEventListener ( "change" , ( ) => {
if ( control === el . expiry ) syncZipForRetention ( ) ;
if ( control === el . apiKeyMode ) syncApiKeyField ( ) ;
saveSettings ( ) ;
syncMenuChecks ( ) ;
updateTerminal ( ) ;
} ) ;
} ) ;
2026-04-27 17:20:57 +03:00
2026-04-29 01:16:17 +03:00
document . addEventListener ( "keydown" , ( event ) => {
if ( event . key === "Escape" ) {
closeDoc ( ) ;
2026-04-29 02:29:49 +03:00
closeMenus ( ) ;
2026-04-29 01:16:17 +03:00
}
if ( event . key === "F1" ) {
event . preventDefault ( ) ;
openDoc ( "faq" ) ;
}
if ( event . ctrlKey && ! event . shiftKey && ! event . altKey ) {
const key = event . key . toLowerCase ( ) ;
if ( key === "o" ) {
event . preventDefault ( ) ;
el . fileInput ? . click ( ) ;
}
if ( key === "u" ) {
event . preventDefault ( ) ;
startUpload ( ) ;
}
if ( key === "k" ) {
event . preventDefault ( ) ;
copyText ( "cURL command" , getCurlCommand ( { full : true } ) ) ;
}
if ( key === "l" ) {
event . preventDefault ( ) ;
copyText ( "Share URL" , shareUrl , shareUrl ) ;
2026-04-27 17:20:57 +03:00
}
2026-04-29 01:16:17 +03:00
}
} ) ;
window . addEventListener ( "beforeunload" , ( ) => {
files . forEach ( ( item ) => {
if ( item . previewURL ) URL . revokeObjectURL ( item . previewURL ) ;
2026-04-27 17:20:57 +03:00
} ) ;
2026-04-29 01:16:17 +03:00
} ) ;
2026-04-28 18:44:16 +03:00
2026-04-29 01:16:17 +03:00
loadSettings ( ) ;
updateLimitHint ( ) ;
syncMenuChecks ( ) ;
renderFiles ( ) ;
updateTerminal ( ) ;