π 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()">×</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()">×</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()">×</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