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