39301-vm/document.php
2026-03-25 16:47:00 +00:00

331 lines
15 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
library_bootstrap();
$documentId = isset($_GET['id']) ? (int) $_GET['id'] : 0;
$context = ($_GET['context'] ?? '') === 'admin' ? 'admin' : 'public';
$publicOnly = $context !== 'admin';
$document = $documentId > 0 ? library_fetch_document($documentId, $publicOnly) : null;
if (!$document) {
http_response_code(404);
library_render_header('Document not found', 'The requested library document could not be found.', $context === 'admin' ? 'admin' : 'catalog');
?>
<section class="panel empty-panel text-center py-5">
<div class="empty-icon mb-3">?</div>
<h1 class="h4">Document not found</h1>
<p class="text-secondary mb-4">This item is unavailable or private.</p>
<a class="btn btn-dark" href="<?= $context === 'admin' ? '/admin.php' : '/index.php' ?>">Go back</a>
</section>
<?php
library_render_footer();
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'generate_summary') {
$result = library_generate_summary((int) $document['id']);
library_set_flash($result['success'] ? 'success' : 'warning', $result['message']);
header('Location: /document.php?id=' . (int) $document['id'] . ($context === 'admin' ? '&context=admin' : '') . '#summary-card');
exit;
}
if ($context !== 'admin') {
library_increment_views((int) $document['id']);
$document = library_fetch_document((int) $document['id'], true) ?: $document;
}
library_render_header(
(string) ($document['title_en'] ?: $document['title_ar'] ?: 'Document detail'),
'Read a library document online, review metadata, and generate a bilingual AI summary from saved excerpts.',
$context === 'admin' ? 'admin' : 'catalog'
);
?>
<section class="mb-4">
<a class="back-link" href="<?= $context === 'admin' ? '/admin.php' : '/index.php' ?>">← Back to <?= $context === 'admin' ? 'Admin Studio' : 'catalog' ?></a>
</section>
<section class="row g-4 align-items-start">
<div class="col-xl-8">
<div class="panel mb-4">
<div class="d-flex flex-wrap justify-content-between gap-3 mb-3">
<div>
<?php if (!empty($document['title_en'])): ?>
<h1 class="display-6 mb-1"><?= h((string) $document['title_en']) ?></h1>
<?php endif; ?>
<?php if (!empty($document['title_ar'])): ?>
<div class="lead text-secondary" dir="rtl"><?= h((string) $document['title_ar']) ?></div>
<?php endif; ?>
</div>
<div class="d-flex flex-wrap gap-2 align-content-start">
<span class="badge text-bg-light"><?= h(library_language_label((string) $document['document_language'])) ?></span>
<span class="badge text-bg-light"><?= h(library_visibility_label((string) $document['visibility'])) ?></span>
<span class="badge text-bg-light"><?= h(library_document_type_label((string) $document['document_type'])) ?></span>
</div>
</div>
<div class="row g-3 small text-secondary border-top pt-3">
<div class="col-md-4"><strong class="text-dark d-block mb-1">Author</strong><?= h((string) ($document['author'] ?: 'Unknown author')) ?></div>
<div class="col-md-4"><strong class="text-dark d-block mb-1">Views</strong><?= h((string) $document['view_count']) ?></div>
<div class="col-md-4"><strong class="text-dark d-block mb-1">File</strong><?= h((string) ($document['file_name'] ?: 'Unavailable')) ?></div>
</div>
</div>
<div class="panel reader-panel">
<div class="d-flex justify-content-between align-items-center gap-3 mb-3">
<div>
<div class="section-kicker">Online reader</div>
<h2 class="h4 mb-0">Read in the browser</h2>
</div>
<?php if (!empty($document['file_path'])): ?>
<?php if (library_can_preview($document)): ?>
<a class="btn btn-outline-secondary btn-sm" href="/viewer.php?id=<?= $document['id'] ?><?= $context === 'admin' ? '&context=admin' : '' ?>" target="_blank">Open fullscreen</a>
<?php else: ?>
<a class="btn btn-outline-secondary btn-sm" href="<?= h(library_file_url((string) $document['file_path'])) ?>" target="_blank" rel="noopener">Open file</a>
<?php endif; ?>
<?php endif; ?>
</div>
<?php if ($document['visibility'] === 'private' && $context !== 'admin'): ?>
<div class="reader-lock">
<h3 class="h5 mb-2">Private item</h3>
<p class="text-secondary mb-0">This title is marked as login-required by the admin, so it stays hidden from the public reading experience.</p>
</div>
<?php elseif (library_can_preview($document)): ?>
<!-- Flipbook Container -->
<div id="flipbook-wrapper" style="position: relative; background: #2d3035; border-radius: 8px; overflow: hidden; height: 700px; display: flex; align-items: center; justify-content: center;">
<div id="flipbook-loader" class="text-center text-white">
<div class="spinner-border mb-2" role="status"></div>
<div>Loading Book...</div>
</div>
<!-- The actual book container for PageFlip -->
<div id="flipbook" class="shadow-lg" style="display:none;"></div>
</div>
<!-- Custom Toolbar -->
<div id="flipbook-toolbar" class="d-flex justify-content-center align-items-center gap-3 mt-3 p-2 bg-white border rounded shadow-sm opacity-50 pe-none">
<button id="fb-prev" class="btn btn-outline-dark border-0 btn-lg" title="Previous Page">
<i class="bi bi-arrow-left-circle-fill"></i>
</button>
<div class="text-center" style="min-width: 120px;">
<span class="small text-uppercase text-secondary fw-bold" style="letter-spacing: 1px;">Page</span>
<div class="d-flex align-items-baseline justify-content-center gap-1">
<span id="fb-current" class="fs-5 fw-bold font-monospace">1</span>
<span class="text-muted">/</span>
<span id="fb-total" class="text-muted font-monospace">--</span>
</div>
</div>
<button id="fb-next" class="btn btn-outline-dark border-0 btn-lg" title="Next Page">
<i class="bi bi-arrow-right-circle-fill"></i>
</button>
</div>
<?php elseif (!empty($document['file_path'])): ?>
<div class="reader-lock">
<h3 class="h5 mb-2">Document stored</h3>
<p class="text-secondary mb-3">This file type is stored successfully, but inline reading is optimized for PDF in this first slice.</p>
<a class="btn btn-dark" href="<?= h(library_file_url((string) $document['file_path'])) ?>" target="_blank" rel="noopener">Download / open file</a>
</div>
<?php else: ?>
<div class="reader-lock">
<h3 class="h5 mb-2">No file attached</h3>
<p class="text-secondary mb-0">Upload a file from the Admin Studio to enable reading.</p>
</div>
<?php endif; ?>
</div>
</div>
<div class="col-xl-4">
<div class="panel mb-4" id="summary-card">
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
<div>
<div class="section-kicker">AI summary</div>
<h2 class="h4 mb-0">Bilingual quick summary</h2>
</div>
<form method="post" action="/document.php?id=<?= h((string) $document['id']) ?><?= $context === 'admin' ? '&context=admin' : '' ?>#summary-card">
<input type="hidden" name="action" value="generate_summary">
<button class="btn btn-dark btn-sm" type="submit"><?= !empty($document['summary_text']) ? 'Regenerate' : 'Generate' ?></button>
</form>
</div>
<?php if (!empty($document['summary_text'])): ?>
<div class="summary-box"><?= nl2br(h((string) $document['summary_text'])) ?></div>
<?php else: ?>
<div class="summary-box summary-box-muted">No AI summary yet. Use the button above after adding a strong Arabic or English excerpt in the admin upload form.</div>
<?php endif; ?>
</div>
<div class="panel mb-4">
<div class="section-kicker">Metadata</div>
<h2 class="h5 mb-3">Catalog notes</h2>
<dl class="row small gy-2 mb-0">
<dt class="col-4">Published</dt>
<dd class="col-8 mb-0"><?= h(date('M d, Y', strtotime((string) $document['created_at']))) ?></dd>
<dt class="col-4">Tags</dt>
<dd class="col-8 mb-0"><?= h((string) ($document['tags'] ?: '—')) ?></dd>
<dt class="col-4">Size</dt>
<dd class="col-8 mb-0"><?= h((string) ($document['file_size_kb'] ?: 0)) ?> KB</dd>
</dl>
</div>
<div class="panel">
<div class="section-kicker">Descriptions</div>
<h2 class="h5 mb-3">Source text used by AI</h2>
<div class="description-stack">
<div>
<div class="small text-uppercase text-secondary mb-2">English</div>
<p class="mb-0 text-secondary"><?= h((string) ($document['description_en'] ?: 'No English excerpt yet.')) ?></p>
</div>
<div>
<div class="small text-uppercase text-secondary mb-2">العربية</div>
<p class="mb-0 text-secondary" dir="rtl"><?= h((string) ($document['description_ar'] ?: 'لا يوجد مقتطف عربي حتى الآن.')) ?></p>
</div>
</div>
</div>
</div>
</section>
<?php if (library_can_preview($document)): ?>
<!-- Flipbook Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script src="https://unpkg.com/page-flip/dist/js/page-flip.browser.js"></script>
<script>
document.addEventListener('DOMContentLoaded', async () => {
const pdfUrl = '<?= h(library_file_url((string) $document['file_path'])) ?>';
const container = document.getElementById('flipbook');
const wrapper = document.getElementById('flipbook-wrapper');
const loader = document.getElementById('flipbook-loader');
const toolbar = document.getElementById('flipbook-toolbar');
const prevBtn = document.getElementById('fb-prev');
const nextBtn = document.getElementById('fb-next');
const currentSpan = document.getElementById('fb-current');
const totalSpan = document.getElementById('fb-total');
// PDF.js worker
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
try {
const loadingTask = pdfjsLib.getDocument(pdfUrl);
const pdf = await loadingTask.promise;
const totalPages = pdf.numPages;
totalSpan.textContent = totalPages;
// Render pages to canvases
// Note: For very large PDFs, this should be lazy-loaded.
// For this demo, we render all to allow smooth flipping.
const canvasPromises = [];
// Calculate dimensions based on the first page
const firstPage = await pdf.getPage(1);
const viewport = firstPage.getViewport({ scale: 1.5 }); // Scale up for better quality
const width = viewport.width;
const height = viewport.height;
// Define desired height for the book (e.g. 600px)
const desiredHeight = 600;
const scale = desiredHeight / height;
// Re-calculate viewport with correct scale
const scaledViewport = firstPage.getViewport({ scale: scale });
for (let i = 1; i <= totalPages; i++) {
const pageDiv = document.createElement('div');
pageDiv.className = 'page'; // Class for PageFlip
// pageDiv.style.padding = '20px';
pageDiv.style.backgroundColor = '#fdfdfd';
// pageDiv.style.border = '1px solid #c2c5cc';
const canvas = document.createElement('canvas');
canvas.className = 'shadow-sm'; // Add some shadow to page
pageDiv.appendChild(canvas);
container.appendChild(pageDiv);
canvasPromises.push(async () => {
const page = await pdf.getPage(i);
const vp = page.getViewport({ scale: scale });
canvas.height = vp.height;
canvas.width = vp.width;
const renderContext = {
canvasContext: canvas.getContext('2d'),
viewport: vp
};
await page.render(renderContext).promise;
});
}
// Wait for all pages to render (or at least start)
// In a real app, we might want to render just the first few before showing.
await Promise.all(canvasPromises.map(fn => fn()));
// Show container and hide loader
loader.style.display = 'none';
container.style.display = 'block';
// Ensure PageFlip is available
if (typeof St === 'undefined' || typeof St.PageFlip === 'undefined') {
throw new Error('PageFlip library failed to load.');
}
// Initialize PageFlip
const pageFlip = new St.PageFlip(container, {
width: scaledViewport.width, // Width of one page
height: scaledViewport.height, // Height of one page
size: 'fixed', // Fixed size
// minWidth: 300,
// maxWidth: 1000,
// minHeight: 400,
// maxHeight: 1200,
showCover: true,
maxShadowOpacity: 0.5,
mobileScrollSupport: false // Disable mobile scroll to prevent conflicts
});
pageFlip.loadFromHTML(document.querySelectorAll('.page'));
// Enable toolbar
toolbar.classList.remove('opacity-50', 'pe-none');
// Events
pageFlip.on('flip', (e) => {
// e.data is the page index (0-based or page number?)
// update page number
// PageFlip creates "leafs" (2 pages per view usually).
// getCurrentPageIndex() returns the index of the current page.
updatePagination();
});
function updatePagination() {
// Get current viewing pages
// In landscape (spread) mode, it might be 1 (cover) or 2-3.
// Let's rely on simple index for now.
// index is 0-based
// For single page view vs spread?
// PageFlip usually handles spread automatically.
// Simplest way:
let current = pageFlip.getCurrentPageIndex() + 1;
currentSpan.textContent = current;
}
prevBtn.addEventListener('click', () => pageFlip.flipPrev());
nextBtn.addEventListener('click', () => pageFlip.flipNext());
} catch (error) {
console.error('Error loading PDF:', error);
loader.innerHTML = '<div class="text-danger">Error loading document.<br>' + error.message + '</div>';
}
});
</script>
<?php endif; ?>
<?php
library_render_footer();