422 lines
21 KiB
PHP
422 lines
21 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;
|
|
|
|
$lang = library_get_language();
|
|
$pageCopy = [
|
|
'en' => [
|
|
'not_found_title' => 'Document not found',
|
|
'not_found_description' => 'The requested library document could not be found.',
|
|
'not_found_copy' => 'This item is unavailable or private.',
|
|
'go_back' => 'Go back',
|
|
'back_to_admin' => 'Back to Admin Studio',
|
|
'back_to_catalog' => 'Back to catalog',
|
|
'detail_fallback' => 'Document detail',
|
|
'meta_description' => 'Read the document online, review its metadata, and see the selected-language summary and description.',
|
|
'author' => 'Author',
|
|
'views' => 'Views',
|
|
'file' => 'File',
|
|
'unknown_author' => 'Unknown author',
|
|
'unavailable' => 'Unavailable',
|
|
'reader_kicker' => 'Online reader',
|
|
'reader_title' => 'Read in the browser',
|
|
'open_fullscreen' => 'Open fullscreen',
|
|
'open_file' => 'Open file',
|
|
'private_item' => 'Private item',
|
|
'private_copy' => 'This title is marked as login-required by the admin, so it stays hidden from the public reading experience.',
|
|
'loading_book' => 'Loading book...',
|
|
'page' => 'Page',
|
|
'previous_page' => 'Previous page',
|
|
'next_page' => 'Next page',
|
|
'document_stored' => 'Document stored',
|
|
'document_stored_copy' => 'This file type is stored successfully, but inline reading is optimized for PDF in this first slice.',
|
|
'download_or_open' => 'Download / open file',
|
|
'no_file_title' => 'No file attached',
|
|
'no_file_copy' => 'Upload a file from the Admin Studio to enable reading.',
|
|
'summary_kicker' => 'AI summary',
|
|
'summary_title' => 'Quick summary',
|
|
'regenerate' => 'Regenerate',
|
|
'generate' => 'Generate',
|
|
'no_summary' => 'No summary yet for the selected language. Click Generate to create one from the document content.',
|
|
'metadata_kicker' => 'Metadata',
|
|
'metadata_title' => 'Catalog notes',
|
|
'published' => 'Published',
|
|
'tags' => 'Tags',
|
|
'size' => 'Size',
|
|
'description_kicker' => 'Description',
|
|
'description_title' => 'Source text used by AI',
|
|
'no_excerpt' => 'No excerpt yet for the selected language.',
|
|
],
|
|
'ar' => [
|
|
'not_found_title' => 'المستند غير موجود',
|
|
'not_found_description' => 'تعذر العثور على مستند المكتبة المطلوب.',
|
|
'not_found_copy' => 'هذا العنصر غير متاح أو خاص.',
|
|
'go_back' => 'العودة',
|
|
'back_to_admin' => 'العودة إلى استوديو الإدارة',
|
|
'back_to_catalog' => 'العودة إلى الفهرس',
|
|
'detail_fallback' => 'تفاصيل المستند',
|
|
'meta_description' => 'اقرأ المستند عبر الإنترنت، وراجع بياناته الوصفية، وشاهد الملخص والوصف وفق اللغة المختارة.',
|
|
'author' => 'المؤلف',
|
|
'views' => 'المشاهدات',
|
|
'file' => 'الملف',
|
|
'unknown_author' => 'مؤلف غير معروف',
|
|
'unavailable' => 'غير متاح',
|
|
'reader_kicker' => 'القارئ الإلكتروني',
|
|
'reader_title' => 'اقرأ داخل المتصفح',
|
|
'open_fullscreen' => 'فتح بملء الشاشة',
|
|
'open_file' => 'فتح الملف',
|
|
'private_item' => 'عنصر خاص',
|
|
'private_copy' => 'هذا العنوان محدد من قبل المشرف كمحتوى يتطلب تسجيل الدخول، لذلك يظل مخفياً عن تجربة القراءة العامة.',
|
|
'loading_book' => 'جارٍ تحميل الكتاب...',
|
|
'page' => 'الصفحة',
|
|
'previous_page' => 'الصفحة السابقة',
|
|
'next_page' => 'الصفحة التالية',
|
|
'document_stored' => 'تم حفظ المستند',
|
|
'document_stored_copy' => 'تم حفظ هذا النوع من الملفات بنجاح، لكن القراءة المضمنة مهيأة لملفات PDF في هذه النسخة الأولى.',
|
|
'download_or_open' => 'تنزيل / فتح الملف',
|
|
'no_file_title' => 'لا يوجد ملف مرفق',
|
|
'no_file_copy' => 'ارفع ملفاً من استوديو الإدارة لتفعيل القراءة.',
|
|
'summary_kicker' => 'ملخص الذكاء الاصطناعي',
|
|
'summary_title' => 'ملخص سريع',
|
|
'regenerate' => 'إعادة التوليد',
|
|
'generate' => 'توليد',
|
|
'no_summary' => 'لا يوجد ملخص بعد للغة المختارة. اضغط على توليد لإنشاء ملخص من محتوى المستند.',
|
|
'metadata_kicker' => 'البيانات الوصفية',
|
|
'metadata_title' => 'ملاحظات الفهرس',
|
|
'published' => 'تاريخ النشر',
|
|
'tags' => 'الوسوم',
|
|
'size' => 'الحجم',
|
|
'description_kicker' => 'الوصف',
|
|
'description_title' => 'النص المصدر المستخدم من الذكاء الاصطناعي',
|
|
'no_excerpt' => 'لا يوجد مقتطف بعد للغة المختارة.',
|
|
],
|
|
][$lang];
|
|
|
|
if (!$document) {
|
|
http_response_code(404);
|
|
library_render_header($pageCopy['not_found_title'], $pageCopy['not_found_description'], $context === 'admin' ? 'admin' : 'catalog');
|
|
?>
|
|
<section class="panel empty-panel text-center py-5">
|
|
<div class="empty-icon mb-3">?</div>
|
|
<h1 class="h4"><?= h($pageCopy['not_found_title']) ?></h1>
|
|
<p class="text-secondary mb-4"><?= h($pageCopy['not_found_copy']) ?></p>
|
|
<a class="btn btn-dark" href="<?= $context === 'admin' ? '/admin.php' : '/index.php' ?>"><?= h($pageCopy['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;
|
|
}
|
|
|
|
$selectedTitle = library_localized_document_title($document, $lang, $pageCopy['detail_fallback']);
|
|
$selectedTitleLang = library_text_lang($selectedTitle, $lang);
|
|
$selectedTitleDir = library_text_dir($selectedTitle, $selectedTitleLang);
|
|
$selectedSummary = library_localized_document_summary($document, $lang);
|
|
$selectedDescription = library_localized_document_description($document, $lang, $pageCopy['no_excerpt']);
|
|
|
|
library_render_header(
|
|
$selectedTitle,
|
|
$pageCopy['meta_description'],
|
|
$context === 'admin' ? 'admin' : 'catalog'
|
|
);
|
|
?>
|
|
<section class="mb-4">
|
|
<a class="back-link" href="<?= $context === 'admin' ? '/admin.php' : '/index.php' ?>">← <?= h($context === 'admin' ? $pageCopy['back_to_admin'] : $pageCopy['back_to_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>
|
|
<h1 class="display-6 mb-1" lang="<?= h($selectedTitleLang) ?>" dir="<?= h($selectedTitleDir) ?>"><?= h($selectedTitle) ?></h1>
|
|
</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"><?= h($pageCopy['author']) ?></strong><?= h((string) ($document['author'] ?: $pageCopy['unknown_author'])) ?></div>
|
|
<div class="col-md-4"><strong class="text-dark d-block mb-1"><?= h($pageCopy['views']) ?></strong><?= h((string) $document['view_count']) ?></div>
|
|
<div class="col-md-4"><strong class="text-dark d-block mb-1"><?= h($pageCopy['file']) ?></strong><?= h((string) ($document['file_name'] ?: $pageCopy['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"><?= h($pageCopy['reader_kicker']) ?></div>
|
|
<h2 class="h4 mb-0"><?= h($pageCopy['reader_title']) ?></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"><?= h($pageCopy['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"><?= h($pageCopy['open_file']) ?></a>
|
|
<?php endif; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<?php if ($document['visibility'] === 'private' && $context !== 'admin'): ?>
|
|
<div class="reader-lock">
|
|
<h3 class="h5 mb-2"><?= h($pageCopy['private_item']) ?></h3>
|
|
<p class="text-secondary mb-0"><?= h($pageCopy['private_copy']) ?></p>
|
|
</div>
|
|
<?php elseif (library_can_preview($document)): ?>
|
|
<!-- Flipbook Container -->
|
|
<div id="flipbook-wrapper" data-book-direction="<?= library_get_language() === 'ar' ? 'rtl' : 'ltr' ?>" dir="ltr" style="position: relative; background: #2d3035; border-radius: 8px; overflow: hidden; height: 700px; display: flex; align-items: center; justify-content: center; direction: ltr;">
|
|
<div id="flipbook-loader" class="text-center text-white">
|
|
<div class="spinner-border mb-2" role="status"></div>
|
|
<div><?= h($pageCopy['loading_book']) ?></div>
|
|
</div>
|
|
<!-- The actual book container for PageFlip -->
|
|
<div id="flipbook" class="shadow-lg" dir="ltr" style="display:none; direction: ltr;"></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="<?= h($pageCopy['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;"><?= h($pageCopy['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="<?= h($pageCopy['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"><?= h($pageCopy['document_stored']) ?></h3>
|
|
<p class="text-secondary mb-3"><?= h($pageCopy['document_stored_copy']) ?></p>
|
|
<a class="btn btn-dark" href="<?= h(library_file_url((string) $document['file_path'])) ?>" target="_blank" rel="noopener"><?= h($pageCopy['download_or_open']) ?></a>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="reader-lock">
|
|
<h3 class="h5 mb-2"><?= h($pageCopy['no_file_title']) ?></h3>
|
|
<p class="text-secondary mb-0"><?= h($pageCopy['no_file_copy']) ?></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"><?= h($pageCopy['summary_kicker']) ?></div>
|
|
<h2 class="h4 mb-0"><?= h($pageCopy['summary_title']) ?></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"><?= $selectedSummary !== '' ? h($pageCopy['regenerate']) : h($pageCopy['generate']) ?></button>
|
|
</form>
|
|
</div>
|
|
|
|
<?php if ($selectedSummary !== ''): ?>
|
|
<div class="summary-box" lang="<?= h($lang) ?>" dir="<?= $lang === 'ar' ? 'rtl' : 'ltr' ?>"><?= nl2br(h($selectedSummary)) ?></div>
|
|
<?php else: ?>
|
|
<div class="summary-box summary-box-muted"><?= h($pageCopy['no_summary']) ?></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="panel mb-4">
|
|
<div class="section-kicker"><?= h($pageCopy['metadata_kicker']) ?></div>
|
|
<h2 class="h5 mb-3"><?= h($pageCopy['metadata_title']) ?></h2>
|
|
<dl class="row small gy-2 mb-0">
|
|
<dt class="col-4"><?= h($pageCopy['published']) ?></dt>
|
|
<dd class="col-8 mb-0"><?= h(date('M d, Y', strtotime((string) $document['created_at']))) ?></dd>
|
|
<dt class="col-4"><?= h($pageCopy['tags']) ?></dt>
|
|
<dd class="col-8 mb-0"><?= h((string) ($document['tags'] ?: '—')) ?></dd>
|
|
<dt class="col-4"><?= h($pageCopy['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"><?= h($pageCopy['description_kicker']) ?></div>
|
|
<h2 class="h5 mb-3"><?= h($pageCopy['description_title']) ?></h2>
|
|
<div class="description-stack">
|
|
<div>
|
|
<p class="mb-0 text-secondary" lang="<?= h($lang) ?>" dir="<?= $lang === 'ar' ? 'rtl' : 'ltr' ?>"><?= h($selectedDescription) ?></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 bookDirection = wrapper?.dataset.bookDirection === 'rtl' ? 'rtl' : 'ltr';
|
|
|
|
// Keep the PDF canvas/layout isolated from the surrounding Bootstrap RTL shell.
|
|
wrapper.style.direction = 'ltr';
|
|
container.style.direction = 'ltr';
|
|
|
|
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 display and render sizes separately so text stays crisp,
|
|
// especially for Arabic glyphs which become unreadable when rasterized too small.
|
|
const firstPage = await pdf.getPage(1);
|
|
const baseViewport = firstPage.getViewport({ scale: 1 });
|
|
const deviceScale = Math.max(1, window.devicePixelRatio || 1);
|
|
const desiredHeight = Math.min(860, Math.max(640, window.innerHeight - 260));
|
|
const displayScale = desiredHeight / baseViewport.height;
|
|
const scaledViewport = firstPage.getViewport({ scale: displayScale });
|
|
const renderScale = Math.max(displayScale * deviceScale * 1.6, 2);
|
|
|
|
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 renderViewport = page.getViewport({ scale: renderScale });
|
|
const displayViewport = page.getViewport({ scale: displayScale });
|
|
canvas.height = Math.ceil(renderViewport.height);
|
|
canvas.width = Math.ceil(renderViewport.width);
|
|
canvas.style.width = `${displayViewport.width}px`;
|
|
canvas.style.height = `${displayViewport.height}px`;
|
|
|
|
const renderContext = {
|
|
canvasContext: canvas.getContext('2d', { alpha: false }),
|
|
viewport: renderViewport
|
|
};
|
|
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
|
|
direction: bookDirection
|
|
});
|
|
|
|
pageFlip.loadFromHTML(container.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();
|