πŸ“‚ File Browser

tst
πŸŒ™ Dark Mode
🎯 Quick Launch:

πŸ“ Directories

πŸ“ files/ πŸ”“ Open

πŸ“„ Files

🐘 index.php
β–Ά Open πŸ“„ View Source

πŸ“„ Source: index.php

<?php
// ============================================================
//  Atestat Demo – upload / view / delete
//  Single folder: "files" next to this script.
// ============================================================

$BASE_DIR   = __DIR__;
$FILES_DIR  = $BASE_DIR . DIRECTORY_SEPARATOR . 'files';

// LibreOffice binary – in PATH on this server
define('SOFFICE_BIN', 'soffice');
define('SOFFICE_TIMEOUT', 60);

$ALLOWED = [
    'php','html','htm','txt','md','json','xml','css','js','svg',
    'pdf','jpg','jpeg','png','gif','webp',
    'docx','doc','pptx','ppt','xlsx','xls','odt','odp','ods','rtf'
];

// Plain text extensions – can be shown as source
$TEXT_EXT = ['php','html','htm','txt','md','json','xml','css','js','svg'];

// Extensions the browser can render inline without conversion
$INLINE_PREVIEW = ['pdf','jpg','jpeg','png','gif','svg','webp'];

// Extensions LibreOffice can convert to PDF for preview
$CONVERTIBLE = ['docx','doc','pptx','ppt','xlsx','xls','odt','odp','ods','rtf'];

// ---------- Ensure the files folder exists ----------
if (!is_dir($FILES_DIR)) {
    mkdir($FILES_DIR, 0777, true);
}

// ---------- Helpers ----------
function safeName($name) {
    $name = basename($name);
    $name = preg_replace('/[^a-zA-Z0-9._\- ]/', '_', $name);
    return trim($name);
}

function resolveFile($baseDir, $name) {
    $name = safeName($name);
    if ($name === '') return false;
    $path = $baseDir . DIRECTORY_SEPARATOR . $name;
    // Make sure it stays inside baseDir
    $real = realpath(dirname($path));
    if ($real === false) return false;
    if (strpos($real, realpath($baseDir)) !== 0) return false;
    return $path;
}

function cleanupDir($dir) {
    if (!is_dir($dir)) return;
    foreach (scandir($dir) as $e) {
        if ($e === '.' || $e === '..') continue;
        $p = $dir . DIRECTORY_SEPARATOR . $e;
        if (is_dir($p)) cleanupDir($p);
        else @unlink($p);
    }
    @rmdir($dir);
}

/**
 * Convert a file to a cached PDF next to it.
 * Cache name: "<original>.pdf" e.g. "report.docx.pdf".
 * Returns the PDF path on success, or false.
 */
function convertToPdf($sourcePath) {
    if (!is_file($sourcePath)) return false;

    $pdfPath = $sourcePath . '.pdf';

    // Fresh cache?
    if (is_file($pdfPath) && filemtime($pdfPath) >= filemtime($sourcePath)) {
        return $pdfPath;
    }

    $tmpDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR
            . 'atestat_conv_' . bin2hex(random_bytes(4));
    if (!mkdir($tmpDir, 0777, true)) return false;

    $srcArg  = escapeshellarg($sourcePath);
    $outArg  = escapeshellarg($tmpDir);
    $bin     = escapeshellcmd(SOFFICE_BIN);
    $profile = escapeshellarg('file://' . $tmpDir . '/profile');

    $cmd = "$bin --headless --norestore --invisible "
         . "-env:UserInstallation=$profile "
         . "--convert-to pdf --outdir $outArg $srcArg";

    $descriptors = [
        0 => ['pipe', 'r'],
        1 => ['pipe', 'w'],
        2 => ['pipe', 'w'],
    ];
    $proc = proc_open($cmd, $descriptors, $pipes);
    if (!is_resource($proc)) {
        cleanupDir($tmpDir);
        return false;
    }

    stream_set_blocking($pipes[1], false);
    stream_set_blocking($pipes[2], false);

    $start = time();
    while (true) {
        $status = proc_get_status($proc);
        stream_get_contents($pipes[1]);
        stream_get_contents($pipes[2]);
        if (!$status['running']) break;
        if (time() - $start > SOFFICE_TIMEOUT) {
            proc_terminate($proc, 9);
            break;
        }
        usleep(100000);
    }
    fclose($pipes[1]);
    fclose($pipes[2]);
    proc_close($proc);

    // LibreOffice writes "<name-without-ext>.pdf"
    $expected = $tmpDir . DIRECTORY_SEPARATOR
              . pathinfo(basename($sourcePath), PATHINFO_FILENAME) . '.pdf';
    if (!is_file($expected)) {
        $found = glob($tmpDir . DIRECTORY_SEPARATOR . '*.pdf');
        if (empty($found)) {
            cleanupDir($tmpDir);
            return false;
        }
        $expected = $found[0];
    }

    if (!@rename($expected, $pdfPath)) {
        @copy($expected, $pdfPath);
    }
    cleanupDir($tmpDir);

    return is_file($pdfPath) ? $pdfPath : false;
}

// ============================================================
//  Endpoints
// ============================================================

// ---------- Upload ----------
if (isset($_FILES['file'])) {
    header('Content-Type: application/json');
    $file = $_FILES['file'];
    if ($file['error'] !== UPLOAD_ERR_OK) {
        echo json_encode(['ok' => false, 'error' => 'Upload error code ' . $file['error']]);
        exit;
    }
    $safe = safeName($file['name']);
    $ext  = strtolower(pathinfo($safe, PATHINFO_EXTENSION));
    if (!in_array($ext, $ALLOWED)) {
        echo json_encode(['ok' => false, 'error' => 'File type not allowed']);
        exit;
    }
    $dest = $FILES_DIR . DIRECTORY_SEPARATOR . $safe;
    if (file_exists($dest)) {
        echo json_encode(['ok' => false, 'error' => 'File already exists']);
        exit;
    }
    if (!move_uploaded_file($file['tmp_name'], $dest)) {
        echo json_encode(['ok' => false, 'error' => 'Could not save file']);
        exit;
    }
    // Warm the PDF cache for Office documents
    if (in_array($ext, $CONVERTIBLE)) {
        @convertToPdf($dest);
    }
    echo json_encode(['ok' => true, 'name' => $safe]);
    exit;
}

// ---------- Delete ----------
if (isset($_POST['delete'])) {
    header('Content-Type: application/json');
    $path = resolveFile($FILES_DIR, $_POST['delete']);
    if ($path === false || !is_file($path)) {
        echo json_encode(['ok' => false, 'error' => 'Not found']);
        exit;
    }
    @unlink($path);
    @unlink($path . '.pdf');   // remove cached preview too
    echo json_encode(['ok' => true]);
    exit;
}

// ---------- Info (for the modal) ----------
if (isset($_GET['info'])) {
    header('Content-Type: application/json');
    $path = resolveFile($FILES_DIR, $_GET['info']);
    if ($path === false || !is_file($path)) {
        echo json_encode(['ok' => false, 'error' => 'Not found']);
        exit;
    }
    $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
    $cachedPdf = $path . '.pdf';
    $convertible = in_array($ext, $CONVERTIBLE);
    $hasFreshPdf = $convertible
        && is_file($cachedPdf)
        && filemtime($cachedPdf) >= filemtime($path);

    echo json_encode([
        'ok'               => true,
        'name'             => basename($path),
        'ext'              => $ext,
        'size'             => filesize($path),
        'isText'           => in_array($ext, $TEXT_EXT),
        'isInlinePreview'  => in_array($ext, $INLINE_PREVIEW),
        'isConvertible'    => $convertible,
        'hasFreshPdf'      => $hasFreshPdf,
    ]);
    exit;
}

// ---------- Convert (on demand) ----------
if (isset($_GET['convert'])) {
    header('Content-Type: application/json');
    $path = resolveFile($FILES_DIR, $_GET['convert']);
    if ($path === false || !is_file($path)) {
        echo json_encode(['ok' => false, 'error' => 'Not found']);
        exit;
    }
    $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
    if (!in_array($ext, $CONVERTIBLE)) {
        echo json_encode(['ok' => false, 'error' => 'Not a convertible file']);
        exit;
    }
    $pdf = convertToPdf($path);
    if ($pdf === false) {
        echo json_encode(['ok' => false, 'error' => 'Conversion failed']);
        exit;
    }
    echo json_encode(['ok' => true]);
    exit;
}

// ---------- Raw source (text files) ----------
if (isset($_GET['source'])) {
    $path = resolveFile($FILES_DIR, $_GET['source']);
    if ($path === false || !is_file($path)) { http_response_code(404); exit; }
    $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
    if (!in_array($ext, $TEXT_EXT)) { http_response_code(415); exit; }
    header('Content-Type: text/plain; charset=utf-8');
    echo file_get_contents($path);
    exit;
}

// ---------- Inline preview ----------
if (isset($_GET['inline'])) {
    $path = resolveFile($FILES_DIR, $_GET['inline']);
    if ($path === false || !is_file($path)) { http_response_code(404); exit; }

    // Cached PDF for Office documents?
    if (isset($_GET['cached'])) {
        $pdf = $path . '.pdf';
        if (!is_file($pdf)) { http_response_code(404); exit; }
        header('Content-Type: application/pdf');
        header('Content-Disposition: inline; filename="' . basename($pdf) . '"');
        header('Content-Length: ' . filesize($pdf));
        readfile($pdf);
        exit;
    }

    $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
    $mimes = [
        'pdf'  => 'application/pdf',
        'jpg'  => 'image/jpeg', 'jpeg' => 'image/jpeg',
        'png'  => 'image/png',  'gif'  => 'image/gif',
        'svg'  => 'image/svg+xml', 'webp' => 'image/webp',
    ];
    $mime = $mimes[$ext] ?? 'application/octet-stream';
    header('Content-Type: ' . $mime);
    header('Content-Disposition: inline; filename="' . basename($path) . '"');
    header('Content-Length: ' . filesize($path));
    readfile($path);
    exit;
}

// ---------- Download ----------
if (isset($_GET['download'])) {
    $path = resolveFile($FILES_DIR, $_GET['download']);
    if ($path === false || !is_file($path)) { http_response_code(404); exit; }
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="' . basename($path) . '"');
    header('Content-Length: ' . filesize($path));
    readfile($path);
    exit;
}

// ============================================================
//  Build file listing for the page
// ============================================================
$items = [];
foreach (scandir($FILES_DIR) as $entry) {
    if ($entry === '.' || $entry === '..') continue;
    $path = $FILES_DIR . DIRECTORY_SEPARATOR . $entry;
    if (!is_file($path)) continue;
    // Hide cached PDFs from the listing
    if (preg_match('/\.(docx|doc|pptx|ppt|xlsx|xls|odt|odp|ods|rtf)\.pdf$/i', $entry)) continue;
    $ext = strtolower(pathinfo($entry, PATHINFO_EXTENSION));
    if (!in_array($ext, $ALLOWED)) continue;
    $items[] = [
        'name'     => $entry,
        'ext'      => $ext,
        'size'     => filesize($path),
        'modified' => filemtime($path),
    ];
}
usort($items, fn($a, $b) => strcasecmp($a['name'], $b['name']));

function iconFor($ext) {
    if ($ext === 'php')                                   return 'fa-file-code';
    if ($ext === 'pdf')                                   return 'fa-file-pdf';
    if (in_array($ext, ['jpg','jpeg','png','gif','svg','webp'])) return 'fa-file-image';
    if (in_array($ext, ['html','htm']))                   return 'fa-file-code';
    if (in_array($ext, ['txt','md']))                     return 'fa-file-alt';
    if (in_array($ext, ['css','js','json','xml']))        return 'fa-file-code';
    if (in_array($ext, ['docx','doc']))                   return 'fa-file-word';
    if (in_array($ext, ['pptx','ppt']))                   return 'fa-file-powerpoint';
    if (in_array($ext, ['xlsx','xls']))                   return 'fa-file-excel';
    return 'fa-file';
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Atestat Demo</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<style>
    * { box-sizing: border-box; margin: 0; padding: 0; }

    body {
        font-family: 'Segoe UI', Roboto, -apple-system, Arial, sans-serif;
        font-size: 14px;
        line-height: 1.5;
        background: #eef1f5;
        color: #1f2933;
        padding: 20px;
        min-height: 100vh;
    }

    .container {
        max-width: 1000px;
        margin: 0 auto;
    }

    .header {
        background: #fff;
        border: 1px solid #d8dee6;
        border-radius: 6px;
        padding: 20px 24px;
        margin-bottom: 16px;
        box-shadow: 0 1px 2px rgba(15,23,42,0.06);
    }

    .header h1 {
        font-size: 20px;
        font-weight: 600;
        color: #2563eb;
        display: flex;
        align-items: center;
        gap: 10px;
    }

    .header p {
        font-size: 13px;
        color: #5b6b7c;
        margin-top: 4px;
    }

    .panel {
        background: #fff;
        border: 1px solid #d8dee6;
        border-radius: 6px;
        box-shadow: 0 1px 2px rgba(15,23,42,0.06);
        overflow: hidden;
    }

    .panel-header {
        padding: 12px 16px;
        background: #f7f9fc;
        border-bottom: 1px solid #d8dee6;
        display: flex;
        justify-content: space-between;
        align-items: center;
    }

    .panel-header h2 {
        font-size: 15px;
        font-weight: 600;
    }

    .panel-header .path {
        font-family: 'Consolas', 'Monaco', monospace;
        font-size: 11px;
        color: #5b6b7c;
        background: #fff;
        padding: 2px 8px;
        border: 1px solid #d8dee6;
        border-radius: 3px;
    }

    .toolbar {
        padding: 10px 14px;
        border-bottom: 1px solid #d8dee6;
        display: flex;
        align-items: center;
        gap: 8px;
        flex-wrap: wrap;
    }

    .btn {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        padding: 7px 14px;
        background: #2563eb;
        color: #fff;
        border: 1px solid #2563eb;
        border-radius: 4px;
        font-size: 13px;
        font-family: inherit;
        cursor: pointer;
        transition: background 0.12s, border-color 0.12s;
    }

    .btn:hover { background: #1d4ed8; border-color: #1d4ed8; }

    .btn-outline {
        background: transparent;
        color: #2563eb;
        border-color: #c2cad6;
    }

    .btn-outline:hover {
        background: #e0e9ff;
        border-color: #2563eb;
    }

    .file-list {
        max-height: 560px;
        overflow-y: auto;
    }

    .file-item {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 10px 16px;
        border-bottom: 1px solid #d8dee6;
        gap: 10px;
        transition: background 0.12s;
    }

    .file-item:last-child { border-bottom: none; }
    .file-item:hover { background: #f0f4fb; }

    .file-name {
        font-family: 'Consolas', 'Monaco', monospace;
        font-size: 13px;
        color: #1f2933;
        display: flex;
        align-items: center;
        gap: 10px;
        flex: 1;
        min-width: 0;
        word-break: break-all;
    }

    .file-name i {
        color: #2563eb;
        font-size: 14px;
        width: 16px;
        text-align: center;
        flex-shrink: 0;
    }

    .file-meta {
        font-size: 11px;
        color: #5b6b7c;
        font-family: 'Consolas', 'Monaco', monospace;
        margin-left: 8px;
    }

    .file-actions { display: flex; gap: 4px; flex-shrink: 0; }

    .btn-icon {
        width: 30px;
        height: 30px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        background: transparent;
        border: 1px solid transparent;
        border-radius: 4px;
        color: #5b6b7c;
        cursor: pointer;
        font-size: 12px;
        transition: background 0.12s, color 0.12s, border-color 0.12s, width 0.12s, padding 0.12s;
    }

    .btn-icon:hover {
        background: #eef3fb;
        color: #2563eb;
        border-color: #d8dee6;
    }

    .btn-icon.confirming {
        background: #dc2626;
        border-color: #dc2626;
        color: #fff;
        width: auto;
        padding: 0 10px;
        gap: 5px;
        animation: pulse 0.9s ease-in-out infinite alternate;
    }

    .btn-icon.confirming:hover { background: #b91c1c; border-color: #b91c1c; }

    @keyframes pulse {
        from { box-shadow: 0 0 0 0 rgba(220,38,38,0.5); }
        to   { box-shadow: 0 0 0 5px rgba(220,38,38,0); }
    }

    .empty {
        padding: 40px 16px;
        text-align: center;
        color: #5b6b7c;
        font-style: italic;
    }

    /* Modal */
    .modal {
        display: none;
        position: fixed;
        inset: 0;
        background: rgba(0,0,0,0.7);
        z-index: 1000;
        justify-content: center;
        align-items: center;
        padding: 20px;
    }

    .modal.active { display: flex; }

    .modal-content {
        background: #fff;
        border: 1px solid #d8dee6;
        border-radius: 6px;
        max-width: 900px;
        width: 100%;
        max-height: 88vh;
        display: flex;
        flex-direction: column;
        overflow: hidden;
        box-shadow: 0 10px 40px rgba(0,0,0,0.3);
    }

    .modal-header {
        padding: 12px 16px;
        background: #f7f9fc;
        border-bottom: 1px solid #d8dee6;
        display: flex;
        justify-content: space-between;
        align-items: center;
    }

    .modal-header h3 {
        font-family: 'Consolas', 'Monaco', monospace;
        font-size: 14px;
        word-break: break-all;
    }

    .close-modal {
        background: none;
        border: none;
        font-size: 24px;
        line-height: 1;
        color: #5b6b7c;
        cursor: pointer;
        padding: 0 6px;
    }

    .close-modal:hover { color: #dc2626; }

    .modal-body {
        flex: 1;
        overflow: auto;
        background: #f7f9fc;
        display: flex;
        flex-direction: column;
    }

    .modal-code {
        padding: 16px;
        margin: 0;
        font-family: 'Consolas', 'Monaco', monospace;
        font-size: 12px;
        line-height: 1.55;
        white-space: pre-wrap;
        word-break: break-all;
    }

    .modal-embed {
        width: 100%;
        height: 75vh;
        border: none;
        background: #fff;
    }

    .modal-image {
        max-width: 100%;
        max-height: 75vh;
        display: block;
        margin: 16px auto;
    }

    .info-box {
        padding: 40px 24px;
        text-align: center;
        color: #1f2933;
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 8px;
    }

    .info-box i { font-size: 48px; color: #2563eb; margin-bottom: 6px; }
    .info-box p { margin: 2px 0; font-size: 13px; }
    .info-box .muted { color: #5b6b7c; font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; }

    .modal-footer {
        padding: 10px 16px;
        background: #f7f9fc;
        border-top: 1px solid #d8dee6;
        display: flex;
        gap: 8px;
        justify-content: flex-end;
    }

    /* Toast */
    .toast {
        position: fixed;
        bottom: 20px;
        right: 20px;
        background: #16a34a;
        color: #fff;
        padding: 10px 18px;
        border-radius: 4px;
        font-size: 13px;
        box-shadow: 0 8px 24px rgba(0,0,0,0.2);
        animation: slideIn 0.25s ease;
        z-index: 2000;
        max-width: 320px;
    }

    .toast.error { background: #dc2626; }

    @keyframes slideIn {
        from { transform: translateX(100%); opacity: 0; }
        to   { transform: translateX(0); opacity: 1; }
    }

    @keyframes spin {
        from { transform: rotate(0deg); }
        to   { transform: rotate(360deg); }
    }

    .fa-spinner { animation: spin 1s linear infinite; }

    @media (max-width: 640px) {
        body { padding: 10px; }
        .file-meta { display: none; }
    }
</style>
</head>
<body>

<div class="container">
    <div class="header">
        <h1><i class="fas fa-graduation-cap"></i> Atestat Demo</h1>
        <p>Upload Β· view Β· delete β€” served from <code>files/</code></p>
    </div>

    <div class="panel">
        <div class="panel-header">
            <h2><i class="fas fa-folder-open"></i> Files</h2>
            <span class="path">./files</span>
        </div>
        <div class="toolbar">
            <input type="file" id="uploadInput" style="display:none;" onchange="doUpload(this)">
            <button class="btn" onclick="document.getElementById('uploadInput').click()">
                <i class="fas fa-upload"></i> Upload file
            </button>
            <button class="btn btn-outline" onclick="location.reload()">
                <i class="fas fa-sync-alt"></i> Refresh
            </button>
        </div>
        <div class="file-list">
            <?php if (empty($items)): ?>
                <div class="empty">No files yet β€” upload one to get started.</div>
            <?php else: foreach ($items as $it): ?>
                <div class="file-item">
                    <span class="file-name">
                        <i class="fas <?= iconFor($it['ext']) ?>"></i>
                        <?= htmlspecialchars($it['name']) ?>
                        <span class="file-meta"><?= number_format($it['size'] / 1024, 1) ?> KB</span>
                    </span>
                    <div class="file-actions">
                        <button class="btn-icon" title="View"
                                onclick="viewFile('<?= htmlspecialchars(addslashes($it['name'])) ?>')">
                            <i class="fas fa-eye"></i>
                        </button>
                        <button class="btn-icon" title="Download"
                                onclick="downloadFile('<?= htmlspecialchars(addslashes($it['name'])) ?>')">
                            <i class="fas fa-download"></i>
                        </button>
                        <button class="btn-icon" title="Delete"
                                onclick="confirmDelete(this, '<?= htmlspecialchars(addslashes($it['name'])) ?>')">
                            <i class="fas fa-trash"></i>
                        </button>
                    </div>
                </div>
            <?php endforeach; endif; ?>
        </div>
    </div>
</div>

<!-- ========== View modal ========== -->
<div id="modal" class="modal">
    <div class="modal-content">
        <div class="modal-header">
            <h3 id="modalTitle">View</h3>
            <button class="close-modal" onclick="closeModal()">&times;</button>
        </div>
        <div id="modalBody" class="modal-body"></div>
        <div id="modalFooter" class="modal-footer">
            <button class="btn" onclick="closeModal()">Close</button>
        </div>
    </div>
</div>

<script>
// ==================== Helpers ====================
function escapeHtml(t) {
    const d = document.createElement('div');
    d.textContent = t;
    return d.innerHTML;
}

function showToast(msg, isError) {
    const t = document.createElement('div');
    t.className = 'toast' + (isError ? ' error' : '');
    t.textContent = msg;
    document.body.appendChild(t);
    setTimeout(() => t.remove(), 3000);
}

// ==================== Upload ====================
async function doUpload(input) {
    const file = input.files[0];
    if (!file) return;

    const fd = new FormData();
    fd.append('file', file);

    // Show progress toast for larger files / conversions
    const converting = /\.(docx?|pptx?|xlsx?|odt|odp|ods|rtf)$/i.test(file.name);
    showToast(converting ? 'πŸ“€ Uploading and converting…' : 'πŸ“€ Uploading…');

    try {
        const r = await fetch('', { method: 'POST', body: fd });
        const j = await r.json();
        if (j.ok) {
            showToast('βœ… Uploaded');
            setTimeout(() => location.reload(), 400);
        } else {
            showToast('❌ ' + (j.error || 'Upload failed'), true);
        }
    } catch (e) {
        showToast('❌ Upload failed', true);
    }
    input.value = '';
}

// ==================== Delete (two-click) ====================
function confirmDelete(btn, name) {
    if (btn.dataset.confirming === '1') {
        doDelete(name);
        return;
    }
    const original = btn.innerHTML;
    btn.dataset.confirming = '1';
    btn.dataset.originalHtml = original;
    btn.classList.add('confirming');
    btn.innerHTML = '<i class="fas fa-check"></i> Confirm';
    btn.title = 'Click again to delete';

    btn._t = setTimeout(() => {
        btn.dataset.confirming = '0';
        btn.classList.remove('confirming');
        btn.innerHTML = btn.dataset.originalHtml;
        btn.title = 'Delete';
    }, 3000);
}

async function doDelete(name) {
    const fd = new FormData();
    fd.append('delete', name);
    const r = await fetch('', { method: 'POST', body: fd });
    const j = await r.json();
    if (j.ok) {
        showToast('πŸ—‘οΈ Deleted');
        setTimeout(() => location.reload(), 300);
    } else {
        showToast('❌ ' + (j.error || 'Delete failed'), true);
    }
}

// ==================== Download ====================
function downloadFile(name) {
    window.location.href = '?download=' + encodeURIComponent(name);
    showToast('⬇️ Download started');
}

// ==================== View ====================
async function viewFile(name) {
    const r = await fetch('?info=' + encodeURIComponent(name));
    const info = await r.json();

    if (!info.ok) {
        showToast('❌ ' + (info.error || 'Cannot open'), true);
        return;
    }

    document.getElementById('modalTitle').textContent = info.name;
    const body   = document.getElementById('modalBody');
    const footer = document.getElementById('modalFooter');

    body.innerHTML = '';
    footer.innerHTML = '<button class="btn" onclick="closeModal()">Close</button>';

    if (info.isText) {
        const src = await fetch('?source=' + encodeURIComponent(name));
        const text = await src.text();
        const pre = document.createElement('pre');
        pre.className = 'modal-code';
        pre.textContent = text || '(empty)';
        body.appendChild(pre);

    } else if (info.isInlinePreview) {
        const url = '?inline=' + encodeURIComponent(name);
        if (info.ext === 'pdf') {
            const em = document.createElement('embed');
            em.src = url + '#toolbar=1';
            em.type = 'application/pdf';
            em.className = 'modal-embed';
            body.appendChild(em);
        } else {
            const im = document.createElement('img');
            im.src = url;
            im.alt = name;
            im.className = 'modal-image';
            body.appendChild(im);
        }
        addDownloadBtn(footer, name, 'btn-outline');

    } else if (info.isConvertible && info.hasFreshPdf) {
        const em = document.createElement('embed');
        em.src = '?inline=' + encodeURIComponent(name) + '&cached=1#toolbar=1';
        em.type = 'application/pdf';
        em.className = 'modal-embed';
        body.appendChild(em);
        addDownloadBtn(footer, name, 'btn-outline');

    } else if (info.isConvertible) {
        const box = document.createElement('div');
        box.className = 'info-box';
        box.innerHTML = `
            <i class="fas fa-file-export"></i>
            <p><strong>${escapeHtml(info.name)}</strong></p>
            <p class="muted">${(info.size/1024).toFixed(1)} KB Β· .${info.ext}</p>
            <p>This file needs to be converted for preview.</p>
            <p class="muted">Takes a few seconds. Cached afterwards.</p>
        `;
        body.appendChild(box);

        const conv = document.createElement('button');
        conv.className = 'btn';
        conv.style.marginRight = '8px';
        conv.innerHTML = '<i class="fas fa-cog"></i> Convert for preview';
        conv.onclick = async () => {
            conv.disabled = true;
            conv.innerHTML = '<i class="fas fa-spinner"></i> Converting…';
            const rr = await fetch('?convert=' + encodeURIComponent(name));
            const jj = await rr.json();
            if (jj.ok) {
                showToast('βœ… Converted');
                closeModal();
                setTimeout(() => viewFile(name), 150);
            } else {
                conv.disabled = false;
                conv.innerHTML = '<i class="fas fa-cog"></i> Convert for preview';
                showToast('❌ ' + (jj.error || 'Conversion failed'), true);
            }
        };
        footer.insertBefore(conv, footer.firstChild);
        addDownloadBtn(footer, name, 'btn-outline');

    } else {
        const box = document.createElement('div');
        box.className = 'info-box';
        box.innerHTML = `
            <i class="fas fa-file-archive"></i>
            <p><strong>${escapeHtml(info.name)}</strong></p>
            <p class="muted">${(info.size/1024).toFixed(1)} KB Β· .${info.ext}</p>
            <p>This file type can't be previewed in the browser.</p>
        `;
        body.appendChild(box);
        addDownloadBtn(footer, name, '');
    }

    document.getElementById('modal').classList.add('active');
}

function addDownloadBtn(footer, name, cls) {
    const b = document.createElement('button');
    b.className = 'btn ' + (cls || '');
    b.style.marginRight = '8px';
    b.innerHTML = '<i class="fas fa-download"></i> Download';
    b.onclick = () => downloadFile(name);
    footer.insertBefore(b, footer.firstChild);
}

function closeModal() {
    document.getElementById('modal').classList.remove('active');
    document.getElementById('modalBody').innerHTML = '';
}

// Close modal when clicking outside
document.getElementById('modal').addEventListener('click', function(e) {
    if (e.target === this) closeModal();
});
</script>
</body>
</html>
← Back