<?php
/**
 * File Explorer - Full Featured (FIXED - No headers warning)
 * - Hide self, list files/folders
 * - User mapping from remote JSON
 * - ZIP download (all folders / single folder)
 * - Hover preview (text & images)
 * - Rename files/folders
 */

// ===================== CONFIGURATION =====================
define('BASE_DIR', realpath(__DIR__));
define('USERS_JSON_URL', 'https://rxaero.unaux.com/data/users.json');
define('USER_CACHE_TTL', 3600);
define('MAX_TEXT_PREVIEW_SIZE', 1024 * 1024);
define('MAX_IMAGE_PREVIEW_SIZE', 5 * 1024 * 1024);
define('PREVIEW_CHARS', 200);
define('THUMB_MAX_WIDTH', 200);
define('THUMB_MAX_HEIGHT', 200);

error_reporting(E_ALL);
ini_set('display_errors', 0); // Turn off display errors to prevent output before headers
ini_set('log_errors', 1);     // Log errors instead
ini_set('memory_limit', '512M');
set_time_limit(300);

$DEBUG = isset($_GET['debug']);
$SCRIPT_NAME = basename($_SERVER['SCRIPT_FILENAME']);

// ===================== USER MAPPING =====================
function getUserMapping() {
    static $result = null;
    if ($result !== null) return $result;
    $cacheFile = sys_get_temp_dir() . '/user_map_cache.json';
    if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < USER_CACHE_TTL) {
        $cached = json_decode(file_get_contents($cacheFile), true);
        if (is_array($cached)) { $result = $cached; return $result; }
    }
    $mapping = []; $message = '';
    if (!ini_get('allow_url_fopen')) {
        $message = "allow_url_fopen disabled - cannot fetch users.";
    } else {
        $ctx = stream_context_create(['http'=>['timeout'=>5], 'ssl'=>['verify_peer'=>false]]);
        $json = @file_get_contents(USERS_JSON_URL, false, $ctx);
        if ($json === false) {
            $message = "Failed to fetch from " . USERS_JSON_URL;
        } else {
            $users = json_decode($json, true);
            if (is_array($users)) {
                foreach ($users as $u) {
                    $id = $u['id'] ?? $u['user_id'] ?? $u['uid'] ?? null;
                    if ($id) {
                        $name = $u['username'] ?? $u['name'] ?? $u['full_name'] ?? "User $id";
                        $mapping[(string)$id] = $name;
                    }
                }
                $message = "Loaded ".count($mapping)." users";
            } else {
                $message = "Invalid JSON";
            }
        }
    }
    $result = ['success'=>!empty($mapping), 'mapping'=>$mapping, 'message'=>$message];
    file_put_contents($cacheFile, json_encode($result));
    return $result;
}

function getUserDisplayName($folderName) {
    $mapResult = getUserMapping();
    $mapping = $mapResult['mapping'];
    if (isset($mapping[$folderName])) return $mapping[$folderName];
    preg_match('/\d+/', $folderName, $matches);
    if (!empty($matches) && isset($mapping[$matches[0]])) return $mapping[$matches[0]];
    return $folderName;
}

// ===================== HELPER FUNCTIONS =====================
function getSafePath($p='') {
    if($p==='') return BASE_DIR;
    $p = str_replace(['/','\\'], DIRECTORY_SEPARATOR, rawurldecode($p));
    $real = realpath(BASE_DIR . DIRECTORY_SEPARATOR . $p);
    return ($real && strpos($real, BASE_DIR)===0) ? $real : false;
}
function formatSize($b) {
    if($b==0) return '0 B';
    $u = ['B','KB','MB','GB','TB'];
    $i = floor(log($b,1024));
    return round($b/pow(1024,$i),2).' '.$u[$i];
}
function addFolderToZip($zip, $folder, $zipPath) {
    $folder = rtrim($folder, DIRECTORY_SEPARATOR);
    if(!is_dir($folder)) return false;
    $zip->addEmptyDir($zipPath);
    foreach(scandir($folder) as $item) {
        if($item=='.'||$item=='..') continue;
        $full = $folder.DIRECTORY_SEPARATOR.$item;
        $rel = $zipPath.DIRECTORY_SEPARATOR.$item;
        if(is_dir($full)) addFolderToZip($zip, $full, $rel);
        else $zip->addFile($full, $rel);
    }
    return true;
}
function zipAllFolders($dir) {
    // Clear all output buffers
    while (ob_get_level()) ob_end_clean();
    
    // Suppress all warnings for this function
    error_reporting(0);
    
    $folders = [];
    $scanned = scandir($dir);
    if ($scanned === false) {
        $_SESSION['error'] = 'Cannot read directory';
        header('Location: '.$_SERVER['SCRIPT_NAME'].'?dir='.urlencode(getRelativePath($dir)));
        exit;
    }
    foreach ($scanned as $item) {
        if ($item === '.' || $item === '..') continue;
        $full = $dir . DIRECTORY_SEPARATOR . $item;
        if (is_dir($full)) $folders[] = $item;
    }
    if (empty($folders)) {
        $_SESSION['error'] = 'No folders found to zip.';
        header('Location: '.$_SERVER['SCRIPT_NAME'].'?dir='.urlencode(getRelativePath($dir)));
        exit;
    }
    
    // Try multiple temp directories if needed
    $tempZip = false;
    $tempDirs = [sys_get_temp_dir(), '/tmp', __DIR__ . '/temp'];
    foreach ($tempDirs as $tempDir) {
        if (!is_dir($tempDir) && !mkdir($tempDir, 0777, true)) continue;
        $tempZip = @tempnam($tempDir, 'zip_');
        if ($tempZip !== false) break;
    }
    
    if ($tempZip === false) {
        $_SESSION['error'] = 'Failed to create temporary file.';
        header('Location: '.$_SERVER['SCRIPT_NAME'].'?dir='.urlencode(getRelativePath($dir)));
        exit;
    }
    
    $zip = new ZipArchive();
    $res = $zip->open($tempZip, ZipArchive::CREATE | ZipArchive::OVERWRITE);
    if ($res !== true) {
        @unlink($tempZip);
        $_SESSION['error'] = 'Cannot create ZIP (error code '.$res.')';
        header('Location: '.$_SERVER['SCRIPT_NAME'].'?dir='.urlencode(getRelativePath($dir)));
        exit;
    }
    
    $success = true;
    foreach ($folders as $folder) {
        $folderPath = $dir . DIRECTORY_SEPARATOR . $folder;
        if (!addFolderToZip($zip, $folderPath, $folder)) {
            $success = false;
            break;
        }
    }
    if (!$zip->close()) $success = false;
    
    if (!$success) {
        @unlink($tempZip);
        $_SESSION['error'] = 'Error adding folders to ZIP (permissions/disk space)';
        header('Location: '.$_SERVER['SCRIPT_NAME'].'?dir='.urlencode(getRelativePath($dir)));
        exit;
    }
    
    $zipName = 'folders_' . basename($dir) . '_' . date('Ymd_His') . '.zip';
    
    // Ensure no output before headers
    if (headers_sent($file, $line)) {
        $_SESSION['error'] = "Headers already sent in $file on line $line";
        header('Location: '.$_SERVER['SCRIPT_NAME'].'?dir='.urlencode(getRelativePath($dir)));
        exit;
    }
    
    header('Content-Type: application/zip');
    header('Content-Disposition: attachment; filename="'.$zipName.'"');
    header('Content-Length: ' . filesize($tempZip));
    header('Cache-Control: private, max-age=0, must-revalidate');
    header('Pragma: public');
    
    readfile($tempZip);
    @unlink($tempZip);
    exit;
}
function zipSingleFolder($folder, $redirectRel) {
    while (ob_get_level()) ob_end_clean();
    error_reporting(0);
    
    if (!is_dir($folder)) {
        $_SESSION['error'] = 'Invalid folder';
        header('Location: '.$_SERVER['SCRIPT_NAME'].'?dir='.urlencode($redirectRel));
        exit;
    }
    
    $tempZip = false;
    $tempDirs = [sys_get_temp_dir(), '/tmp', __DIR__ . '/temp'];
    foreach ($tempDirs as $tempDir) {
        if (!is_dir($tempDir) && !mkdir($tempDir, 0777, true)) continue;
        $tempZip = @tempnam($tempDir, 'zip_');
        if ($tempZip !== false) break;
    }
    
    if ($tempZip === false) {
        $_SESSION['error'] = 'Cannot create temp file';
        header('Location: '.$_SERVER['SCRIPT_NAME'].'?dir='.urlencode($redirectRel));
        exit;
    }
    
    $zip = new ZipArchive();
    if ($zip->open($tempZip, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
        @unlink($tempZip);
        $_SESSION['error'] = 'Cannot create ZIP';
        header('Location: '.$_SERVER['SCRIPT_NAME'].'?dir='.urlencode($redirectRel));
        exit;
    }
    addFolderToZip($zip, $folder, basename($folder));
    $zip->close();
    
    $zipName = basename($folder) . '_' . date('Ymd_His') . '.zip';
    
    if (headers_sent($file, $line)) {
        $_SESSION['error'] = "Headers already sent in $file on line $line";
        header('Location: '.$_SERVER['SCRIPT_NAME'].'?dir='.urlencode($redirectRel));
        exit;
    }
    
    header('Content-Type: application/zip');
    header('Content-Disposition: attachment; filename="'.$zipName.'"');
    header('Content-Length: ' . filesize($tempZip));
    header('Cache-Control: private, max-age=0, must-revalidate');
    
    readfile($tempZip);
    @unlink($tempZip);
    exit;
}
function getRelativePath($abs) {
    if ($abs === BASE_DIR) return '';
    return substr($abs, strlen(BASE_DIR)+1);
}

// ===================== RENAME HANDLER =====================
if (isset($_POST['rename_file']) && isset($_POST['old_path']) && isset($_POST['new_name'])) {
    session_start();
    header('Content-Type: application/json');
    $oldRel = $_POST['old_path'];
    $newName = trim($_POST['new_name']);
    if (empty($newName) || strpos($newName, '/') !== false || strpos($newName, '\\') !== false || $newName === '.' || $newName === '..') {
        echo json_encode(['success'=>false, 'error'=>'Invalid name']);
        exit;
    }
    $oldFull = getSafePath($oldRel);
    if (!$oldFull || strpos($oldFull, BASE_DIR) !== 0 || $oldFull === __FILE__) {
        echo json_encode(['success'=>false, 'error'=>'Invalid path']);
        exit;
    }
    $parentDir = dirname($oldFull);
    $newFull = $parentDir . DIRECTORY_SEPARATOR . $newName;
    if (file_exists($newFull)) {
        echo json_encode(['success'=>false, 'error'=>'Already exists']);
        exit;
    }
    if (!rename($oldFull, $newFull)) {
        echo json_encode(['success'=>false, 'error'=>'Rename failed (permissions)']);
        exit;
    }
    echo json_encode(['success'=>true]);
    exit;
}

// ===================== AJAX PREVIEW HANDLER =====================
if (isset($_GET['preview_file'])) {
    header('Content-Type: application/json');
    $relPath = $_GET['preview_file'];
    $fullPath = getSafePath($relPath);
    if (!$fullPath || !is_file($fullPath) || strpos($fullPath, BASE_DIR) !== 0) {
        echo json_encode(['error' => 'Invalid file']);
        exit;
    }
    $size = filesize($fullPath);
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mime = finfo_file($finfo, $fullPath);
    finfo_close($finfo);
    if (strpos($mime, 'image/') === 0) {
        if ($size > MAX_IMAGE_PREVIEW_SIZE) {
            echo json_encode(['error' => 'Image too large (max ' . formatSize(MAX_IMAGE_PREVIEW_SIZE) . ')']);
            exit;
        }
        if (!extension_loaded('gd')) {
            echo json_encode(['error' => 'GD extension missing']);
            exit;
        }
        list($w, $h) = getimagesize($fullPath);
        $ratio = min(THUMB_MAX_WIDTH/$w, THUMB_MAX_HEIGHT/$h);
        $nw = max(1, (int)($w * $ratio));
        $nh = max(1, (int)($h * $ratio));
        $src = match($mime) {
            'image/jpeg' => imagecreatefromjpeg($fullPath),
            'image/png' => imagecreatefrompng($fullPath),
            'image/gif' => imagecreatefromgif($fullPath),
            'image/webp' => imagecreatefromwebp($fullPath),
            default => null,
        };
        if (!$src) {
            echo json_encode(['error' => 'Unsupported image']);
            exit;
        }
        $thumb = imagecreatetruecolor($nw, $nh);
        if ($mime == 'image/png' || $mime == 'image/gif') {
            imagealphablending($thumb, false);
            imagesavealpha($thumb, true);
            $trans = imagecolorallocatealpha($thumb, 0, 0, 0, 127);
            imagefilledrectangle($thumb, 0, 0, $nw, $nh, $trans);
        }
        imagecopyresampled($thumb, $src, 0, 0, 0, 0, $nw, $nh, $w, $h);
        ob_start();
        imagepng($thumb);
        $imgData = ob_get_clean();
        imagedestroy($src);
        imagedestroy($thumb);
        $base64 = base64_encode($imgData);
        echo json_encode(['preview' => '<img src="data:image/png;base64,'.$base64.'" style="max-width:200px;max-height:200px;border-radius:4px;">']);
        exit;
    }
    $textExts = ['txt','php','js','html','htm','css','json','xml','csv','log','md','ini','conf','sh','bat','ps1','py','rb','pl','sql'];
    $ext = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION));
    if (!in_array($ext, $textExts)) {
        echo json_encode(['error' => 'Preview not supported']);
        exit;
    }
    if ($size > MAX_TEXT_PREVIEW_SIZE) {
        echo json_encode(['error' => 'File too large (max '.formatSize(MAX_TEXT_PREVIEW_SIZE).')']);
        exit;
    }
    $content = file_get_contents($fullPath, false, null, 0, PREVIEW_CHARS * 2);
    if ($content === false) {
        echo json_encode(['error' => 'Cannot read file']);
        exit;
    }
    if (mb_strlen($content) > PREVIEW_CHARS) $content = mb_substr($content, 0, PREVIEW_CHARS) . '...';
    $content = htmlspecialchars($content);
    echo json_encode(['preview' => nl2br($content)]);
    exit;
}

// ===================== MAIN PAGE =====================
session_start();
if (isset($_GET['reset_cache'])) {
    $cf = sys_get_temp_dir().'/user_map_cache.json';
    if (file_exists($cf)) unlink($cf);
    $_SESSION['error'] = 'Cache cleared';
    header('Location: '.$_SERVER['SCRIPT_NAME']);
    exit;
}
if (!class_exists('ZipArchive')) die('<div class="error">Enable ZipArchive extension</div>');

$reqDir = $_GET['dir'] ?? '';
$curDir = getSafePath($reqDir);
if (!$curDir) { $curDir = BASE_DIR; $_SESSION['error'] = 'Invalid path'; }

// Create temp directory if needed
$tempDir = __DIR__ . '/temp';
if (!is_dir($tempDir)) {
    @mkdir($tempDir, 0777, true);
}

if (isset($_GET['zip_all_folders'])) zipAllFolders($curDir);
if (isset($_GET['zip_folder'])) {
    $rel = getRelativePath($curDir);
    $folderAbs = getSafePath($rel.'/'.$_GET['zip_folder']);
    if ($folderAbs && strpos($folderAbs, $curDir)===0 && is_dir($folderAbs))
        zipSingleFolder($folderAbs, $rel);
    else {
        $_SESSION['error'] = 'Invalid folder';
        header('Location: '.$_SERVER['SCRIPT_NAME'].'?dir='.urlencode($rel));
        exit;
    }
}
$errorMsg = $_SESSION['error'] ?? null;
unset($_SESSION['error']);

$items = scandir($curDir);
$dirs = []; $files = [];
foreach ($items as $i) {
    if ($i=='.'||$i=='..') continue;
    if ($i === $SCRIPT_NAME) continue;
    $p = $curDir.DIRECTORY_SEPARATOR.$i;
    if (!file_exists($p)) continue;
    $s = stat($p);
    $entry = ['name'=>$i, 'is_dir'=>is_dir($p), 'size'=>is_dir($p)?0:$s['size'], 'size_fmt'=>is_dir($p)?'-':formatSize($s['size']), 'modified'=>date('Y-m-d H:i:s',$s['mtime'])];
    if (is_dir($p)) $dirs[] = $entry;
    else $files[] = $entry;
}
usort($dirs, fn($a,$b)=>strnatcmp($a['name'],$b['name']));
usort($files, fn($a,$b)=>strnatcmp($a['name'],$b['name']));
$all = array_merge($dirs, $files);
$parent = '';
if ($curDir !== BASE_DIR) {
    $par = dirname($curDir);
    if (strpos($par, BASE_DIR)===0) $parent = getRelativePath($par);
}
$curRel = getRelativePath($curDir);
$mapStatus = getUserMapping();
?>
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>File Explorer</title>
<style>
body{font-family:Segoe UI,sans-serif;background:#f5f7fa;margin:20px}.container{max-width:1400px;margin:0 auto;background:#fff;border-radius:16px;box-shadow:0 10px 25px -5px rgba(0,0,0,0.1)}.header{background:linear-gradient(135deg,#1e3c72,#2a5298);color:#fff;padding:20px 30px}.breadcrumb{background:#eef2f7;padding:12px 30px;border-bottom:1px solid #dce4ec}.status-bar{background:#f0f9ff;padding:8px 30px;font-size:0.8rem;border-bottom:1px solid #dce4ec}.action-bar{padding:20px 30px;background:#f8fafc;display:flex;justify-content:space-between;flex-wrap:wrap;gap:15px}.btn{display:inline-flex;align-items:center;gap:8px;padding:10px 20px;border-radius:8px;text-decoration:none;font-weight:600}.btn-primary{background:#2a5298;color:#fff}.btn-secondary{background:#e2e8f0;color:#1e2a3e}.error-msg{background:#fee2e2;border-left:4px solid #dc2626;padding:12px 20px;margin:15px 30px}.table-wrapper{overflow-x:auto;padding:0 30px 30px}.folder-icon{color:#f59e0b}.item-link{text-decoration:none;color:#1e293b;display:flex;align-items:center;gap:8px;flex-wrap:wrap}.display-name{color:#475569;font-size:0.75rem;margin-left:8px}.action-buttons{display:flex;gap:8px;flex-wrap:wrap}.rename-btn,.zip-btn{background:none;border:none;cursor:pointer;padding:4px 8px;border-radius:6px;font-size:1rem}.rename-btn:hover,.zip-btn:hover{background:#e2e8f0}table{width:100%;border-collapse:collapse}th,td{padding:12px;text-align:left;border-bottom:1px solid #eef2f7}th{background:#f1f5f9}.debug-info{background:#fef9e6;border:1px solid #f59e0b;padding:10px;margin:0 30px 20px;font-family:monospace;font-size:12px;white-space:pre-wrap}
.preview-tooltip{position:fixed;background:#1e293b;color:#f1f5f9;padding:10px 15px;border-radius:8px;font-size:0.85rem;font-family:'Courier New',monospace;max-width:300px;max-height:260px;overflow:auto;box-shadow:0 10px 25px -5px rgba(0,0,0,0.3);z-index:1000;pointer-events:none;white-space:pre-wrap;word-break:break-word;border-left:3px solid #f59e0b;text-align:center}.preview-tooltip img{display:block;margin:0 auto}.preview-tooltip.loading::after{content:" ⏳";font-style:italic}.preview-tooltip.error{border-left-color:#ef4444;background:#2d1a1a}.file-previewable{cursor:help;border-bottom:1px dotted #2a5298}
</style>
</head>
<body>
<div class="container">
<div class="header"><h1>📁 File Explorer</h1><p>Rename, ZIP, hover preview, user mapping</p></div>
<div class="breadcrumb">📍 <a href="?dir=">Root</a><?php if($curRel){$parts=explode('/',$curRel);$acc='';foreach($parts as $p){$acc.=($acc?'/':'').$p;echo ' / <a href="?dir='.urlencode($acc).'">'.htmlspecialchars(getUserDisplayName($p)).'</a>';}}?></div>
<div class="status-bar">🔄 User mapping: <?php if($mapStatus['success']){echo '✅ '.htmlspecialchars($mapStatus['message']);}else{echo '⚠️ '.htmlspecialchars($mapStatus['message']).' <a href="?reset_cache=1">🔄 Retry</a>';}?> <?php if(!$DEBUG):?> <a href="?debug=1" style="margin-left:10px;">🐞 Debug</a><?php else:?> <a href="?">🔒 Hide Debug</a><?php endif;?></div>
<?php if($DEBUG && $mapStatus['success']): ?>
<div class="debug-info"><strong>🔍 Debug Mode</strong><br>Mapping: <?php print_r($mapStatus['mapping']); ?><br>Folders: <?php echo implode(', ', array_map(fn($d)=>$d['name'], $dirs)) ?: '(none)'; ?><br>Resolutions:<br><?php foreach($dirs as $d) echo '- "'.$d['name'].'" → "'.htmlspecialchars(getUserDisplayName($d['name'])).'"<br>'; ?></div>
<?php endif; ?>
<?php if($errorMsg):?><div class="error-msg">⚠️ <?=htmlspecialchars($errorMsg)?></div><?php endif;?>
<div class="action-bar">
<div><?php if($parent):?><a href="?dir=<?=urlencode($parent)?>" class="btn btn-secondary">⬆️ Parent</a><?php endif;?></div>
<div><a href="?<?=http_build_query(array_merge($_GET,['zip_all_folders'=>1]))?>" class="btn btn-primary" onclick="return confirm('Zip ALL folders in this directory?')">📦 Download All Folders (ZIP)</a></div>
</div>
<div class="table-wrapper">
<?php if(empty($all)):?><div class="empty-state">🗂️ Empty directory</div><?php else:?>
<table>
<thead><tr><th>Name</th><th>Size</th><th>Modified</th><th>Actions</th></tr></thead>
<tbody>
<?php foreach($all as $item):
    $itemRel = empty($curRel) ? $item['name'] : $curRel.'/'.$item['name'];
    $displayName = $item['is_dir'] ? getUserDisplayName($item['name']) : $item['name'];
?>
<tr>
<td><?php if($item['is_dir']): ?><a href="?dir=<?=urlencode($itemRel)?>" class="item-link"><span class="folder-icon">📁</span><span><?=htmlspecialchars($displayName)?></span><?php if($displayName!==$item['name']):?><span class="display-name">(ID: <?=htmlspecialchars($item['name'])?>)</span><?php endif;?></a><?php else: ?><span class="item-link file-previewable" data-preview="<?=htmlspecialchars($itemRel)?>"><span class="file-icon">📄</span><span><?=htmlspecialchars($item['name'])?></span></span><?php endif; ?></td>
<td><?=$item['size_fmt']?></td>
<td><?=$item['modified']?></td>
<td class="action-buttons">
<?php if($item['is_dir']): ?><a href="?<?=http_build_query(array_merge($_GET,['zip_folder'=>$item['name']]))?>" class="zip-btn" title="Download as ZIP" onclick="return confirm('Zip this folder?')">📥 ZIP</a><?php else: ?><span class="badge" style="background:#e2e8f0;padding:2px 8px;border-radius:20px;font-size:0.7rem;">file</span><?php endif; ?>
<button class="rename-btn" title="Rename" data-path="<?=htmlspecialchars($itemRel)?>" data-name="<?=htmlspecialchars($item['name'])?>">✏️ Rename</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
<script>
document.querySelectorAll('.rename-btn').forEach(btn=>{btn.addEventListener('click',async function(){const oldPath=this.dataset.path,oldName=this.dataset.name,newName=prompt('Enter new name',oldName);if(!newName||newName===oldName)return;const fd=new FormData();fd.append('rename_file','1');fd.append('old_path',oldPath);fd.append('new_name',newName);try{const r=await fetch(window.location.pathname,{method:'POST',body:fd});const d=await r.json();if(d.success)location.reload();else alert('Error: '+(d.error||'Unknown'));}catch(e){alert('Request failed');}});});(function(){let tooltip=null,to=null,curFile=null,req=null;function ct(){if(tooltip)return tooltip;const d=document.createElement('div');d.className='preview-tooltip';document.body.appendChild(d);tooltip=d;return tooltip;}function st(c,e){const tt=ct();tt.innerHTML=e?'⚠️ '+c:c;tt.classList.remove('loading');if(e)tt.classList.add('error');else tt.classList.remove('error');tt.style.display='block';}function ht(){if(tooltip)tooltip.style.display='none';if(to)clearTimeout(to);if(req){req.abort();req=null;}curFile=null;}function pt(e){if(!tooltip||tooltip.style.display==='none')return;let x=e.pageX+15,y=e.pageY+15;const ww=window.innerWidth,wh=window.innerHeight,tw=tooltip.offsetWidth,th=tooltip.offsetHeight;if(x+tw>ww)x=e.pageX-tw-10;if(y+th>wh)y=e.pageY-th-10;tooltip.style.left=x+'px';tooltip.style.top=y+'px';}function fp(fileRel,e){if(req)req.abort();const tt=ct();tt.innerHTML='Loading preview...';tt.classList.add('loading');tt.classList.remove('error');tt.style.display='block';pt(e);const url=window.location.pathname+'?preview_file='+encodeURIComponent(fileRel);req=new XMLHttpRequest();req.open('GET',url);req.onload=function(){req=null;if(curFile!==fileRel)return;try{const d=JSON.parse(this.responseText);if(d.error)st(d.error,true);else if(d.preview)st(d.preview,false);else st('No preview',true);}catch(e){st('Invalid response',true);}if(tooltip&&curFile===fileRel)pt(e);};req.onerror=function(){req=null;if(curFile===fileRel)st('Network error',true);};req.send();}document.querySelectorAll('.file-previewable').forEach(el=>{el.addEventListener('mouseenter',function(e){const f=this.dataset.preview;if(!f)return;if(to)clearTimeout(to);to=setTimeout(()=>{curFile=f;fp(f,e);},400);});el.addEventListener('mousemove',function(e){if(tooltip&&tooltip.style.display==='block')pt(e);});el.addEventListener('mouseleave',function(){if(to)clearTimeout(to);if(req){req.abort();req=null;}curFile=null;ht();});});})();
</script>
</body>
</html>