πŸ“‚ File Browser

/atestat
β˜€οΈ Light Mode
🎯 Quick Launch:

πŸ“ Directories

πŸ“ eng/ πŸ”“ Open
πŸ“ inf/ πŸ”“ Open

πŸ“„ Files

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

πŸ“„ Source: index.php

<?php
// ============================================================
//  Atestat Dashboard – index.php
//  Two-panel dashboard with LibreOffice preview for Office files
// ============================================================

$BASE_DIR = __DIR__;
$FOLDERS = [
    'english' => 'eng',
    'cs'      => 'inf'
];

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

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

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

function resolvePath($base, $subfolder, $name = '') {
    $dir = realpath($base . DIRECTORY_SEPARATOR . $subfolder);
    if ($dir === false || strpos($dir, realpath($base)) !== 0) {
        return false;
    }
    if ($name === '') return $dir;
    return $dir . DIRECTORY_SEPARATOR . safeName($name);
}

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".
 * Uses a persistent profile dir so LibreOffice reuses its font cache
 * across conversions β€” cuts cold start roughly in half on repeat runs.
 */
function convertToPdf($sourcePath) {
    if (!is_file($sourcePath)) return false;

    $pdfPath = $sourcePath . '.pdf';

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

    // Persistent profile so the font cache survives between conversions
    $profileDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'atestat_lo_profile';
    if (!is_dir($profileDir)) {
        @mkdir($profileDir, 0777, true);
    }

    // Per-run scratch dir for the output PDF
    $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://' . $profileDir);

    // HOME is set too, since some LibreOffice code paths still check it
    $cmd = "HOME=" . escapeshellarg($profileDir) . " "
         . "$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();
    $stdout = ''; $stderr = '';
    while (true) {
        $status = proc_get_status($proc);
        $stdout .= stream_get_contents($pipes[1]);
        $stderr .= 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;
}

// ---------- AJAX actions ----------
if (isset($_POST['action'])) {
    header('Content-Type: application/json');
    $action  = $_POST['action'];
    $folder  = $_POST['folder']  ?? '';
    $name    = $_POST['name']    ?? '';
    $content = $_POST['content'] ?? '';

    if (!isset($FOLDERS[$folder])) {
        echo json_encode(['ok' => false, 'error' => 'Invalid folder']);
        exit;
    }

    $subdir = $FOLDERS[$folder];

    if (!is_dir($BASE_DIR . DIRECTORY_SEPARATOR . $subdir)) {
        mkdir($BASE_DIR . DIRECTORY_SEPARATOR . $subdir, 0777, true);
    }

    $targetDir = resolvePath($BASE_DIR, $subdir);
    if ($targetDir === false) {
        echo json_encode(['ok' => false, 'error' => 'Invalid directory']);
        exit;
    }

    // ----- CREATE FILE -----
    if ($action === 'create') {
        $safe = safeName($name);
        if ($safe === '') {
            echo json_encode(['ok' => false, 'error' => 'Invalid filename']);
            exit;
        }
        $ext = strtolower(pathinfo($safe, PATHINFO_EXTENSION));
        if (!in_array($ext, $ALLOWED)) {
            echo json_encode(['ok' => false, 'error' => 'File type not allowed']);
            exit;
        }
        $path = $targetDir . DIRECTORY_SEPARATOR . $safe;
        if (file_exists($path)) {
            echo json_encode(['ok' => false, 'error' => 'File already exists']);
            exit;
        }
        file_put_contents($path, $content ?: '');
        echo json_encode(['ok' => true, 'name' => $safe]);
        exit;
    }

    // ----- DELETE ONE -----
    if ($action === 'delete') {
        $path = resolvePath($BASE_DIR, $subdir, $name);
        if ($path === false || !file_exists($path)) {
            echo json_encode(['ok' => false, 'error' => 'Not found']);
            exit;
        }
        if (is_dir($path)) {
            $contents = array_diff(scandir($path), ['.', '..']);
            if (!empty($contents)) {
                echo json_encode(['ok' => false, 'error' => 'Folder is not empty']);
                exit;
            }
            rmdir($path);
        } else {
            @unlink($path);
            @unlink($path . '.pdf');   // also remove cached preview
        }
        echo json_encode(['ok' => true]);
        exit;
    }

    // ----- DELETE MANY -----
    if ($action === 'deleteMany') {
        $names = $_POST['names'] ?? '';
        $list = array_filter(array_map('trim', explode("\n", $names)));
        if (empty($list)) {
            echo json_encode(['ok' => false, 'error' => 'Nothing to delete']);
            exit;
        }
        $deleted = [];
        $errors  = [];
        foreach ($list as $n) {
            $path = resolvePath($BASE_DIR, $subdir, $n);
            if ($path === false || !file_exists($path)) {
                $errors[] = "$n: not found";
                continue;
            }
            if (is_dir($path)) {
                $contents = array_diff(scandir($path), ['.', '..']);
                if (!empty($contents)) {
                    $errors[] = "$n: folder not empty";
                    continue;
                }
                rmdir($path);
            } else {
                @unlink($path);
                @unlink($path . '.pdf');
            }
            $deleted[] = $n;
        }
        echo json_encode(['ok' => true, 'deleted' => $deleted, 'errors' => $errors]);
        exit;
    }

    // ----- CREATE FOLDER -----
    if ($action === 'createFolder') {
        $safe = safeName($name);
        if ($safe === '') {
            echo json_encode(['ok' => false, 'error' => 'Invalid folder name']);
            exit;
        }
        $path = $targetDir . DIRECTORY_SEPARATOR . $safe;
        if (file_exists($path)) {
            echo json_encode(['ok' => false, 'error' => 'Already exists']);
            exit;
        }
        mkdir($path, 0777, true);
        echo json_encode(['ok' => true, 'name' => $safe]);
        exit;
    }

    echo json_encode(['ok' => false, 'error' => 'Unknown action']);
    exit;
}

// ---------- UPLOAD ----------
if (isset($_FILES['file']) && isset($_POST['folder'])) {
    header('Content-Type: application/json');
    $folder = $_POST['folder'];
    if (!isset($FOLDERS[$folder])) {
        echo json_encode(['ok' => false, 'error' => 'Invalid folder']);
        exit;
    }
    $subdir = $FOLDERS[$folder];
    if (!is_dir($BASE_DIR . DIRECTORY_SEPARATOR . $subdir)) {
        mkdir($BASE_DIR . DIRECTORY_SEPARATOR . $subdir, 0777, true);
    }
    $targetDir = resolvePath($BASE_DIR, $subdir);

    $file = $_FILES['file'];
    $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 = $targetDir . 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)) {
        // Warm the PDF cache for Office documents
        if (in_array($ext, $CONVERTIBLE)) {
            @convertToPdf($dest);
        }
        echo json_encode(['ok' => true, 'name' => $safe]);
    } else {
        echo json_encode(['ok' => false, 'error' => 'Upload failed']);
    }
    exit;
}

// ---------- DOWNLOAD ----------
if (isset($_GET['raw'])) {
    $folder = $_GET['folder'] ?? '';
    $name   = $_GET['name']   ?? '';
    if (!isset($FOLDERS[$folder])) { http_response_code(400); exit; }
    $path = resolvePath($BASE_DIR, $FOLDERS[$folder], $name);
    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;
}

// ---------- INLINE PREVIEW (including cached PDFs for Office files) ----------
if (isset($_GET['inline'])) {
    $folder = $_GET['folder'] ?? '';
    $name   = $_GET['name']   ?? '';
    if (!isset($FOLDERS[$folder])) { http_response_code(400); exit; }
    $path = resolvePath($BASE_DIR, $FOLDERS[$folder], $name);
    if ($path === false || !is_file($path)) { http_response_code(404); exit; }

    // Serve the 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;
}

// ---------- SOURCE ----------
if (isset($_GET['source'])) {
    $folder = $_GET['folder'] ?? '';
    $name   = $_GET['name']   ?? '';
    if (!isset($FOLDERS[$folder])) { http_response_code(400); exit; }
    $path = resolvePath($BASE_DIR, $FOLDERS[$folder], $name);
    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);
        echo "Binary file – cannot be displayed as text.";
        exit;
    }
    header('Content-Type: text/plain; charset=utf-8');
    echo file_get_contents($path);
    exit;
}

// ---------- INFO ----------
if (isset($_GET['info'])) {
    header('Content-Type: application/json');
    $folder = $_GET['folder'] ?? '';
    $name   = $_GET['name']   ?? '';
    if (!isset($FOLDERS[$folder])) {
        echo json_encode(['ok' => false, 'error' => 'Invalid folder']);
        exit;
    }
    $path = resolvePath($BASE_DIR, $FOLDERS[$folder], $name);
    if ($path === false || !is_file($path)) {
        echo json_encode(['ok' => false, 'error' => 'Not found']);
        exit;
    }
    $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
    $convertible = in_array($ext, $CONVERTIBLE);
    $cachedPdf = $path . '.pdf';
    $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),
        'isBinary'         => !in_array($ext, $TEXT_EXT),
        'isConvertible'    => $convertible,
        'hasFreshPdf'      => $hasFreshPdf,
    ]);
    exit;
}

// ---------- CONVERT (on-demand) ----------
if (isset($_GET['convert'])) {
    header('Content-Type: application/json');
    $folder = $_GET['folder'] ?? '';
    $name   = $_GET['name']   ?? '';
    if (!isset($FOLDERS[$folder])) {
        echo json_encode(['ok' => false, 'error' => 'Invalid folder']);
        exit;
    }
    $path = resolvePath($BASE_DIR, $FOLDERS[$folder], $name);
    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. Is LibreOffice installed?']);
        exit;
    }
    echo json_encode(['ok' => true]);
    exit;
}

// ---------- LIST ----------
function listFolder($baseDir, $subdir, $allowed) {
    $dir = $baseDir . DIRECTORY_SEPARATOR . $subdir;
    if (!is_dir($dir)) return [];
    $items = [];
    foreach (scandir($dir) as $entry) {
        if ($entry === '.' || $entry === '..') continue;
        // Hide cached conversion PDFs from the listing
        if (preg_match('/\.(docx?|pptx?|xlsx?|odt|odp|ods|rtf)\.pdf$/i', $entry)) continue;
        $path = $dir . DIRECTORY_SEPARATOR . $entry;
        if (is_dir($path)) {
            $items[] = ['name' => $entry, 'isFolder' => true, 'type' => 'folder'];
        } else {
            $ext = strtolower(pathinfo($entry, PATHINFO_EXTENSION));
            if (!in_array($ext, $allowed)) continue;
            $items[] = [
                'name' => $entry,
                'isFolder' => false,
                'type' => $ext,
                'size' => filesize($path),
                'modified' => filemtime($path)
            ];
        }
    }
    usort($items, function($a, $b) {
        if ($a['isFolder'] !== $b['isFolder']) return $a['isFolder'] ? -1 : 1;
        return strcasecmp($a['name'], $b['name']);
    });
    return $items;
}

$engFiles = listFolder($BASE_DIR, $FOLDERS['english'], $ALLOWED);
$infFiles = listFolder($BASE_DIR, $FOLDERS['cs'], $ALLOWED);

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 Β· Dashboard</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
    <link rel="stylesheet" href="style.css">
</head>
<body class="theme-light">
    <div class="container">
        <header class="header">
            <div class="header-inner">
                <h1><i class="fas fa-graduation-cap"></i> Atestat</h1>
                <p>With a <strong>flexible</strong> layout Β· combined dashboard</p>
            </div>
            <nav class="navbar">
                <a href="#" onclick="refreshAll();return false;"><i class="fas fa-home"></i> Dashboard</a>
                <a href="#" onclick="showToast('Use panels to manage files');return false;"><i class="fas fa-info-circle"></i> Info</a>
                <div class="theme-switcher">
                    <label for="themeSelect" class="theme-label"><i class="fas fa-palette"></i></label>
                    <select id="themeSelect" onchange="applyTheme(this.value)">
                        <option value="light">β˜€οΈ Light</option>
                        <option value="dark">πŸŒ™ Dark</option>
                        <option value="oled">⬛ OLED</option>
                        <option value="galaxy">🌌 Galaxy</option>
                    </select>
                </div>
            </nav>
        </header>

        <div class="row">
            <!-- ENGLISH PANEL -->
            <section class="panel side" id="englishPanel" data-panel="english">
                <div class="panel-header">
                    <h2><i class="fas fa-book"></i> English</h2>
                    <span class="path">/eng</span>
                </div>
                <div class="panel-toolbar" data-mode="normal">
                    <button class="btn btn-sm tool-normal" onclick="refreshSection('english')" title="Refresh"><i class="fas fa-sync-alt"></i></button>
                    <input type="text" id="newEnglishFile" placeholder="filename.ext" class="file-input tool-normal">
                    <button class="btn btn-sm tool-normal" onclick="createFile('english')" title="Create"><i class="fas fa-plus"></i></button>
                    <input type="file" id="uploadEnglish" style="display:none;" onchange="uploadFile('english', this)">
                    <button class="btn btn-sm btn-outline tool-normal" onclick="document.getElementById('uploadEnglish').click()" title="Upload"><i class="fas fa-upload"></i></button>
                    <button class="btn btn-sm btn-outline tool-normal" onclick="openFolderModal('english')" title="New folder"><i class="fas fa-folder-plus"></i></button>
                    <button class="btn btn-sm btn-outline tool-normal select-toggle" onclick="toggleSelectMode('english')" title="Select multiple"><i class="fas fa-check-square"></i> Select</button>
                    <span class="select-info tool-select" style="display:none;">0 selected</span>
                    <button class="btn btn-sm tool-select" style="display:none;" onclick="deleteSelected('english')" id="deleteSelEng"><i class="fas fa-trash"></i> Delete selected</button>
                    <button class="btn btn-sm btn-outline tool-select" style="display:none;" onclick="toggleSelectMode('english')"><i class="fas fa-times"></i> Cancel</button>
                </div>
                <div class="file-list" id="englishFileList">
                    <?php if (empty($engFiles)): ?>
                        <div class="empty-state">No files. Create or upload one.</div>
                    <?php else: foreach ($engFiles as $item): ?>
                        <?php if ($item['isFolder']): ?>
                            <div class="file-item folder-item" data-name="<?= htmlspecialchars($item['name']) ?>">
                                <label class="select-checkbox" style="display:none;">
                                    <input type="checkbox" onchange="updateSelection('english')" data-name="<?= htmlspecialchars($item['name']) ?>">
                                </label>
                                <span class="file-name"><i class="fas fa-folder"></i> <?= htmlspecialchars($item['name']) ?></span>
                                <div class="file-actions">
                                    <button class="btn-icon btn-danger-icon" onclick="deleteSingle(this, 'english', '<?= htmlspecialchars(addslashes($item['name'])) ?>')" title="Delete"><i class="fas fa-trash"></i></button>
                                </div>
                            </div>
                        <?php else: ?>
                            <div class="file-item" data-name="<?= htmlspecialchars($item['name']) ?>">
                                <label class="select-checkbox" style="display:none;">
                                    <input type="checkbox" onchange="updateSelection('english')" data-name="<?= htmlspecialchars($item['name']) ?>">
                                </label>
                                <span class="file-name"><i class="fas <?= iconFor($item['type']) ?>"></i> <?= htmlspecialchars($item['name']) ?></span>
                                <div class="file-actions">
                                    <button class="btn-icon" onclick="viewSource('english', '<?= htmlspecialchars(addslashes($item['name'])) ?>')" title="View"><i class="fas fa-eye"></i></button>
                                    <button class="btn-icon" onclick="downloadFile('english', '<?= htmlspecialchars(addslashes($item['name'])) ?>')" title="Download"><i class="fas fa-download"></i></button>
                                    <button class="btn-icon btn-danger-icon" onclick="deleteSingle(this, 'english', '<?= htmlspecialchars(addslashes($item['name'])) ?>')" title="Delete"><i class="fas fa-trash"></i></button>
                                </div>
                            </div>
                        <?php endif; ?>
                    <?php endforeach; endif; ?>
                </div>
            </section>

            <!-- COMPUTER SCIENCE PANEL -->
            <section class="panel main" id="csPanel" data-panel="cs">
                <div class="panel-header">
                    <h2><i class="fas fa-laptop-code"></i> Computer Science</h2>
                    <span class="path">/inf</span>
                </div>
                <div class="panel-toolbar" data-mode="normal">
                    <button class="btn btn-sm tool-normal" onclick="refreshSection('cs')" title="Refresh"><i class="fas fa-sync-alt"></i></button>
                    <input type="text" id="newCsFile" placeholder="filename.ext" class="file-input tool-normal">
                    <button class="btn btn-sm tool-normal" onclick="createFile('cs')" title="Create"><i class="fas fa-plus"></i></button>
                    <input type="file" id="uploadCs" style="display:none;" onchange="uploadFile('cs', this)">
                    <button class="btn btn-sm btn-outline tool-normal" onclick="document.getElementById('uploadCs').click()" title="Upload"><i class="fas fa-upload"></i></button>
                    <button class="btn btn-sm btn-outline tool-normal" onclick="openFolderModal('cs')" title="New folder"><i class="fas fa-folder-plus"></i></button>
                    <button class="btn btn-sm btn-outline tool-normal select-toggle" onclick="toggleSelectMode('cs')" title="Select multiple"><i class="fas fa-check-square"></i> Select</button>
                    <span class="select-info tool-select" style="display:none;">0 selected</span>
                    <button class="btn btn-sm tool-select" style="display:none;" onclick="deleteSelected('cs')" id="deleteSelCs"><i class="fas fa-trash"></i> Delete selected</button>
                    <button class="btn btn-sm btn-outline tool-select" style="display:none;" onclick="toggleSelectMode('cs')"><i class="fas fa-times"></i> Cancel</button>
                </div>
                <div class="file-list" id="csFileList">
                    <?php if (empty($infFiles)): ?>
                        <div class="empty-state">No files. Create or upload one.</div>
                    <?php else: foreach ($infFiles as $item): ?>
                        <?php if ($item['isFolder']): ?>
                            <div class="file-item folder-item" data-name="<?= htmlspecialchars($item['name']) ?>">
                                <label class="select-checkbox" style="display:none;">
                                    <input type="checkbox" onchange="updateSelection('cs')" data-name="<?= htmlspecialchars($item['name']) ?>">
                                </label>
                                <span class="file-name"><i class="fas fa-folder"></i> <?= htmlspecialchars($item['name']) ?></span>
                                <div class="file-actions">
                                    <button class="btn-icon btn-danger-icon" onclick="deleteSingle(this, 'cs', '<?= htmlspecialchars(addslashes($item['name'])) ?>')" title="Delete"><i class="fas fa-trash"></i></button>
                                </div>
                            </div>
                        <?php else: ?>
                            <div class="file-item" data-name="<?= htmlspecialchars($item['name']) ?>">
                                <label class="select-checkbox" style="display:none;">
                                    <input type="checkbox" onchange="updateSelection('cs')" data-name="<?= htmlspecialchars($item['name']) ?>">
                                </label>
                                <span class="file-name"><i class="fas <?= iconFor($item['type']) ?>"></i> <?= htmlspecialchars($item['name']) ?></span>
                                <div class="file-actions">
                                    <button class="btn-icon" onclick="viewSource('cs', '<?= htmlspecialchars(addslashes($item['name'])) ?>')" title="View"><i class="fas fa-eye"></i></button>
                                    <button class="btn-icon" onclick="downloadFile('cs', '<?= htmlspecialchars(addslashes($item['name'])) ?>')" title="Download"><i class="fas fa-download"></i></button>
                                    <button class="btn-icon btn-danger-icon" onclick="deleteSingle(this, 'cs', '<?= htmlspecialchars(addslashes($item['name'])) ?>')" title="Delete"><i class="fas fa-trash"></i></button>
                                </div>
                            </div>
                        <?php endif; ?>
                    <?php endforeach; endif; ?>
                </div>
            </section>
        </div>

        <footer class="footer">
            <p>Atestat dashboard Β· <a href="ex.php">eexxaammppllee</a></p>
        </footer>
    </div>

    <!-- VIEW MODAL -->
    <div id="sourceModal" 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">
                <pre id="modalCode" class="modal-code"></pre>
            </div>
            <div class="modal-footer" id="modalFooter">
                <button class="btn" onclick="closeModal()">Close</button>
            </div>
        </div>
    </div>

    <!-- FOLDER MODAL -->
    <div id="folderModal" class="modal">
        <div class="modal-content modal-sm">
            <div class="modal-header">
                <h3>Create Folder</h3>
                <button class="close-modal" onclick="closeFolderModal()">&times;</button>
            </div>
            <input type="text" id="folderName" placeholder="folder name" class="folder-input">
            <div class="modal-footer">
                <button class="btn" onclick="confirmFolder()">Create</button>
                <button class="btn btn-outline" onclick="closeFolderModal()">Cancel</button>
            </div>
        </div>
    </div>

    <!-- BATCH DELETE CONFIRMATION MODAL -->
    <div id="batchDeleteModal" class="modal">
        <div class="modal-content modal-sm">
            <div class="modal-header">
                <h3><i class="fas fa-exclamation-triangle" style="color:var(--danger);"></i> Confirm deletion</h3>
                <button class="close-modal" onclick="closeBatchModal()">&times;</button>
            </div>
            <div class="batch-body">
                <p class="batch-lead">You are about to delete <strong id="batchCount">0</strong> item(s):</p>
                <ul id="batchList" class="batch-list"></ul>
                <p class="batch-warn">This action cannot be undone.</p>
            </div>
            <div class="modal-footer">
                <button class="btn btn-outline" onclick="closeBatchModal()">Cancel</button>
                <button class="btn" style="background:var(--danger);border-color:var(--danger);color:#fff;" onclick="confirmBatchDelete()"><i class="fas fa-trash"></i> Delete</button>
            </div>
        </div>
    </div>

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

        function showToast(message, isError = false) {
            const toast = document.createElement('div');
            toast.className = 'toast';
            if (isError) toast.classList.add('error');
            toast.textContent = message;
            document.body.appendChild(toast);
            setTimeout(() => toast.remove(), 3000);
        }

        async function api(action, data) {
            const fd = new FormData();
            fd.append('action', action);
            for (const key in data) fd.append(key, data[key]);
            const res = await fetch('index.php', { method: 'POST', body: fd });
            return await res.json();
        }

        // ==================== CREATE / FOLDER ====================
        async function createFile(folderKey) {
            const inputId = folderKey === 'english' ? 'newEnglishFile' : 'newCsFile';
            const input = document.getElementById(inputId);
            const name = input.value.trim();
            if (!name) { showToast('Enter a filename', true); return; }

            const result = await api('create', { folder: folderKey, name: name, content: '' });
            if (result.ok) {
                input.value = '';
                showToast('βœ… File created');
                setTimeout(() => location.reload(), 300);
            } else {
                showToast('❌ ' + (result.error || 'Failed'), true);
            }
        }

        // ==================== SINGLE DELETE (two-click confirm) ====================
        function deleteSingle(btn, folderKey, name) {
            if (btn.dataset.confirming === '1') {
                performDelete(folderKey, 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._confirmTimeout = setTimeout(() => {
                btn.dataset.confirming = '0';
                btn.classList.remove('confirming');
                btn.innerHTML = btn.dataset.originalHtml;
                btn.title = 'Delete';
            }, 3000);
        }

        async function performDelete(folderKey, name) {
            const result = await api('delete', { folder: folderKey, name: name });
            if (result.ok) {
                showToast('πŸ—‘οΈ Deleted');
                setTimeout(() => location.reload(), 300);
            } else {
                showToast('❌ ' + (result.error || 'Failed'), true);
            }
        }

        // ==================== FOLDER ====================
        let currentFolderContext = null;

        function openFolderModal(folderKey) {
            currentFolderContext = folderKey;
            document.getElementById('folderName').value = '';
            document.getElementById('folderModal').classList.add('active');
            setTimeout(() => document.getElementById('folderName').focus(), 50);
        }

        function closeFolderModal() {
            document.getElementById('folderModal').classList.remove('active');
            currentFolderContext = null;
        }

        async function confirmFolder() {
            const name = document.getElementById('folderName').value.trim();
            if (!name) { showToast('Enter a folder name', true); return; }
            if (!currentFolderContext) return;
            const result = await api('createFolder', { folder: currentFolderContext, name: name });
            if (result.ok) {
                closeFolderModal();
                showToast('πŸ“ Folder created');
                setTimeout(() => location.reload(), 300);
            } else {
                showToast('❌ ' + (result.error || 'Failed'), true);
            }
        }

        // ==================== SELECT MODE ====================
        const selectState = {
            english: { active: false, selected: new Set() },
            cs:      { active: false, selected: new Set() }
        };

        function toggleSelectMode(folderKey) {
            const panel = document.getElementById(folderKey === 'english' ? 'englishPanel' : 'csPanel');
            const toolbar = panel.querySelector('.panel-toolbar');
            const state = selectState[folderKey];
            state.active = !state.active;

            if (!state.active) {
                state.selected.clear();
            }

            toolbar.querySelectorAll('.tool-normal').forEach(el => {
                el.style.display = state.active ? 'none' : '';
            });
            toolbar.querySelectorAll('.tool-select').forEach(el => {
                el.style.display = state.active ? '' : 'none';
            });

            panel.querySelectorAll('.select-checkbox').forEach(el => {
                el.style.display = state.active ? '' : 'none';
            });
            panel.querySelectorAll('.select-checkbox input').forEach(cb => {
                cb.checked = false;
            });

            updateSelectionDisplay(folderKey);
        }

        function updateSelection(folderKey) {
            const panel = document.getElementById(folderKey === 'english' ? 'englishPanel' : 'csPanel');
            const state = selectState[folderKey];
            state.selected.clear();
            panel.querySelectorAll('.select-checkbox input:checked').forEach(cb => {
                state.selected.add(cb.dataset.name);
            });
            updateSelectionDisplay(folderKey);
        }

        function updateSelectionDisplay(folderKey) {
            const panel = document.getElementById(folderKey === 'english' ? 'englishPanel' : 'csPanel');
            const state = selectState[folderKey];
            const info = panel.querySelector('.select-info');
            if (info) {
                info.textContent = state.selected.size + ' selected';
            }
        }

        // ==================== BATCH DELETE ====================
        let batchContext = null;

        function deleteSelected(folderKey) {
            const state = selectState[folderKey];
            if (state.selected.size === 0) {
                showToast('Select at least one file', true);
                return;
            }

            batchContext = { folderKey: folderKey, names: Array.from(state.selected) };

            const list = document.getElementById('batchList');
            list.innerHTML = '';
            batchContext.names.forEach(n => {
                const li = document.createElement('li');
                li.innerHTML = '<i class="fas fa-file"></i> ' + escapeHtml(n);
                list.appendChild(li);
            });
            document.getElementById('batchCount').textContent = batchContext.names.length;
            document.getElementById('batchDeleteModal').classList.add('active');
        }

        function closeBatchModal() {
            document.getElementById('batchDeleteModal').classList.remove('active');
            batchContext = null;
        }

        async function confirmBatchDelete() {
            if (!batchContext) return;
            const result = await api('deleteMany', {
                folder: batchContext.folderKey,
                names: batchContext.names.join('\n')
            });
            if (result.ok) {
                const n = result.deleted.length;
                const errs = result.errors.length;
                if (errs > 0) {
                    showToast('πŸ—‘οΈ ' + n + ' deleted, ' + errs + ' failed', true);
                } else {
                    showToast('πŸ—‘οΈ ' + n + ' item(s) deleted');
                }
                closeBatchModal();
                setTimeout(() => location.reload(), 500);
            } else {
                showToast('❌ ' + (result.error || 'Failed'), true);
            }
        }

        // ==================== VIEW / DOWNLOAD ====================
        async function viewSource(folderKey, name) {
            const infoRes = await fetch('index.php?info=1&folder=' + encodeURIComponent(folderKey) + '&name=' + encodeURIComponent(name));
            const info = await infoRes.json();

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

            document.getElementById('modalTitle').innerText = 'πŸ“„ ' + 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 res = await fetch('index.php?source=1&folder=' + encodeURIComponent(folderKey) + '&name=' + encodeURIComponent(name));
                const text = await res.text();
                const pre = document.createElement('pre');
                pre.className = 'modal-code';
                pre.textContent = text || '(empty)';
                body.appendChild(pre);

            } else if (info.isInlinePreview) {
                const url = 'index.php?inline=1&folder=' + encodeURIComponent(folderKey) + '&name=' + encodeURIComponent(name);
                if (info.ext === 'pdf') {
                    const embed = document.createElement('embed');
                    embed.src = url + '#toolbar=1';
                    embed.type = 'application/pdf';
                    embed.className = 'modal-embed';
                    body.appendChild(embed);
                } else {
                    const img = document.createElement('img');
                    img.src = url;
                    img.className = 'modal-image';
                    img.alt = name;
                    body.appendChild(img);
                }
                addDownloadButton(footer, folderKey, name, '');

            } else if (info.isConvertible && info.hasFreshPdf) {
                // Cached PDF ready – embed it
                const embed = document.createElement('embed');
                embed.src = 'index.php?inline=1&cached=1&folder=' + encodeURIComponent(folderKey) + '&name=' + encodeURIComponent(name) + '#toolbar=1';
                embed.type = 'application/pdf';
                embed.className = 'modal-embed';
                body.appendChild(embed);
                addDownloadButton(footer, folderKey, name, '');

            } else if (info.isConvertible) {
                // Convertible but no cache – offer conversion
                const box = document.createElement('div');
                box.className = 'binary-notice';
                const sizeKb = (info.size / 1024).toFixed(1);
                box.innerHTML = `
                    <i class="fas fa-file-export"></i>
                    <p><strong>${escapeHtml(name)}</strong></p>
                    <p class="muted">${sizeKb} KB Β· .${info.ext}</p>
                    <p>This file can be previewed after conversion.</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('index.php?convert=1&folder=' + encodeURIComponent(folderKey) + '&name=' + encodeURIComponent(name));
                    const jj = await rr.json();
                    if (jj.ok) {
                        showToast('βœ… Converted');
                        closeModal();
                        setTimeout(() => viewSource(folderKey, 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);
                addDownloadButton(footer, folderKey, name, 'btn-outline');

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

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

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

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

        function downloadFile(folderKey, name) {
            window.location.href = 'index.php?raw=1&folder=' + encodeURIComponent(folderKey) + '&name=' + encodeURIComponent(name);
            showToast('⬇️ Download started');
        }

        // ==================== UPLOAD ====================
        async function uploadFile(folderKey, input) {
            const file = input.files[0];
            if (!file) return;

            // Show a hint if this will take a moment (Office files convert during upload)
            const converting = /\.(docx?|pptx?|xlsx?|odt|odp|ods|rtf)$/i.test(file.name);
            showToast(converting ? 'πŸ“€ Uploading and converting…' : 'πŸ“€ Uploading…');

            const fd = new FormData();
            fd.append('folder', folderKey);
            fd.append('file', file);
            try {
                const res = await fetch('index.php', { method: 'POST', body: fd });
                const result = await res.json();
                input.value = '';
                if (result.ok) {
                    showToast('βœ… Uploaded');
                    setTimeout(() => location.reload(), 400);
                } else {
                    showToast('❌ ' + (result.error || 'Upload failed'), true);
                }
            } catch (e) {
                input.value = '';
                showToast('❌ Upload failed', true);
            }
        }

        // ==================== REFRESH ====================
        function refreshSection() { location.reload(); }
        function refreshAll() { location.reload(); }

        // ==================== THEME ====================
        const THEMES = ['light', 'dark', 'oled', 'galaxy'];

        function applyTheme(theme) {
            if (!THEMES.includes(theme)) theme = 'light';
            document.body.className = 'theme-' + theme;
            localStorage.setItem('atestat_theme_v3', theme);
            const sel = document.getElementById('themeSelect');
            if (sel) sel.value = theme;
        }

        // ==================== INIT ====================
        window.addEventListener('DOMContentLoaded', () => {
            const savedTheme = localStorage.getItem('atestat_theme_v3') || 'light';
            applyTheme(savedTheme);

            document.getElementById('sourceModal').addEventListener('click', function(e) {
                if (e.target === this) closeModal();
            });
            document.getElementById('folderModal').addEventListener('click', function(e) {
                if (e.target === this) closeFolderModal();
            });
            document.getElementById('batchDeleteModal').addEventListener('click', function(e) {
                if (e.target === this) closeBatchModal();
            });

            document.getElementById('newEnglishFile').addEventListener('keydown', e => {
                if (e.key === 'Enter') createFile('english');
            });
            document.getElementById('newCsFile').addEventListener('keydown', e => {
                if (e.key === 'Enter') createFile('cs');
            });
            document.getElementById('folderName').addEventListener('keydown', e => {
                if (e.key === 'Enter') confirmFolder();
            });
        });
    </script>
</body>
</html>
← Back