diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..f757485
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,4 @@
+php_value upload_max_filesize 20M
+php_value post_max_size 20M
+php_value max_execution_time 300
+php_value max_input_time 300
\ No newline at end of file
diff --git a/admin_documents.php b/admin_documents.php
index d8adae1..a508795 100644
--- a/admin_documents.php
+++ b/admin_documents.php
@@ -1,6 +1,12 @@
0) {
+ $errors[] = 'The uploaded file exceeds the server limit (post_max_size). Please try a smaller file or contact admin.';
+ } else {
+ $action = $_POST['action'] ?? '';
+ $id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
- try {
- if ($action === 'create_document') {
- library_create_document($_POST, $_FILES['document_file'] ?? [], $_FILES['cover_file'] ?? []);
- library_set_flash('success', 'Document created successfully.');
- header('Location: /admin_documents.php');
- exit;
- } elseif ($action === 'update_document') {
- if (!$id) {
- throw new RuntimeException('Invalid Document ID.');
+ try {
+ if ($action === 'create_document') {
+ library_create_document($_POST, $_FILES['document_file'] ?? [], $_FILES['cover_file'] ?? []);
+ library_set_flash('success', 'Document created successfully.');
+ header('Location: /admin_documents.php');
+ exit;
+ } elseif ($action === 'update_document') {
+ if (!$id) {
+ throw new RuntimeException('Invalid Document ID.');
+ }
+ library_update_document($id, $_POST, $_FILES['document_file'] ?? [], $_FILES['cover_file'] ?? []);
+ library_set_flash('success', 'Document updated successfully.');
+ header('Location: /admin_documents.php');
+ exit;
+ } elseif ($action === 'delete_document') {
+ if (!$id) {
+ throw new RuntimeException('Invalid Document ID.');
+ }
+ library_delete_document($id);
+ library_set_flash('success', 'Document deleted successfully.');
+ header('Location: /admin_documents.php');
+ exit;
}
- library_update_document($id, $_POST, $_FILES['document_file'] ?? [], $_FILES['cover_file'] ?? []);
- library_set_flash('success', 'Document updated successfully.');
- header('Location: /admin_documents.php');
- exit;
- } elseif ($action === 'delete_document') {
- if (!$id) {
- throw new RuntimeException('Invalid Document ID.');
- }
- library_delete_document($id);
- library_set_flash('success', 'Document deleted successfully.');
- header('Location: /admin_documents.php');
- exit;
+ } catch (Throwable $exception) {
+ $errors[] = $exception->getMessage();
+ error_log('AdminDocuments Error: ' . $exception->getMessage());
}
- } catch (Throwable $exception) {
- $errors[] = $exception->getMessage();
}
}
// Search Logic
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
-// We can implement search in fetch_documents if needed, currently it supports filters
-// For now, fetch all and filter in PHP or improve SQL later if specific search needed.
-// library_fetch_documents doesn't have search param yet, let's just fetch all.
$documents = library_fetch_documents(false);
-// Basic search filter in PHP for now
if ($search !== '') {
$documents = array_filter($documents, function($doc) use ($search) {
return stripos($doc['title_en'] ?? '', $search) !== false
@@ -63,7 +71,14 @@ admin_render_header('Material Entry', 'documents');
?>
-
= h(implode(' ', $errors)) ?>
+
@@ -162,25 +177,27 @@ admin_render_header('Material Entry', 'documents');
-
@@ -327,6 +343,13 @@ admin_render_header('Material Entry', 'documents');
document.addEventListener('DOMContentLoaded', function() {
documentModal = new bootstrap.Modal(document.getElementById('documentModal'));
+
+ // Form submission loading state
+ document.getElementById('documentForm').addEventListener('submit', function() {
+ const btn = document.getElementById('saveButton');
+ btn.disabled = true;
+ btn.querySelector('.spinner-border').classList.remove('d-none');
+ });
});
function updateSubcategories(selectedSubId = null) {
@@ -354,6 +377,11 @@ admin_render_header('Material Entry', 'documents');
document.getElementById('doc_id').value = '';
document.getElementById('documentForm').reset();
+ // UI Helpers for Create
+ document.getElementById('file_required_indicator').classList.remove('d-none');
+ document.getElementById('document_file_input').required = true;
+ document.getElementById('file_help_text').innerText = 'Required for new documents.';
+
// Clear previews
document.getElementById('current_cover_preview').classList.add('d-none');
document.getElementById('current_file_info').classList.add('d-none');
@@ -369,6 +397,11 @@ admin_render_header('Material Entry', 'documents');
document.getElementById('doc_action').value = 'update_document';
document.getElementById('doc_id').value = doc.id;
+ // UI Helpers for Edit
+ document.getElementById('file_required_indicator').classList.add('d-none');
+ document.getElementById('document_file_input').required = false;
+ document.getElementById('file_help_text').innerText = 'Leave empty to keep existing file.';
+
// Fill fields
document.getElementById('doc_title_en').value = doc.title_en || '';
document.getElementById('doc_title_ar').value = doc.title_ar || '';
@@ -420,4 +453,4 @@ admin_render_header('Material Entry', 'documents');
}
-
+
\ No newline at end of file
diff --git a/assets/css/custom.css b/assets/css/custom.css
index 2e1905e..8d65ea0 100644
--- a/assets/css/custom.css
+++ b/assets/css/custom.css
@@ -347,3 +347,28 @@ footer a {
min-height: 60vh;
}
}
+
+/* Flipbook Custom Styles */
+#flipbook-wrapper {
+ background: #333;
+ box-shadow: inset 0 0 30px rgba(0,0,0,0.6);
+}
+
+#flipbook .page {
+ background-color: #fff;
+ /* Soft border for realism */
+ border-right: 1px solid #ddd;
+}
+
+#flipbook-toolbar button {
+ transition: all 0.2s ease;
+}
+
+#flipbook-toolbar button:hover {
+ transform: scale(1.1);
+ color: var(--accent);
+}
+
+#flipbook-toolbar button:active {
+ transform: scale(0.95);
+}
\ No newline at end of file
diff --git a/db/migrations/006_add_author_column.sql b/db/migrations/006_add_author_column.sql
new file mode 100644
index 0000000..64c15b1
--- /dev/null
+++ b/db/migrations/006_add_author_column.sql
@@ -0,0 +1,2 @@
+ALTER TABLE library_documents
+ADD COLUMN author VARCHAR(255) DEFAULT NULL;
diff --git a/document.php b/document.php
index 331479e..eb1017e 100644
--- a/document.php
+++ b/document.php
@@ -68,7 +68,7 @@ library_render_header(
-
Author= h((string) ($document['author_name'] ?: 'Unknown author')) ?>
+
Author= h((string) ($document['author'] ?: 'Unknown author')) ?>
Views= h((string) $document['view_count']) ?>
File= h((string) ($document['file_name'] ?: 'Unavailable')) ?>
@@ -81,7 +81,11 @@ library_render_header(
Read in the browser
- Open file
+
+ Open fullscreen
+
+ Open file
+
@@ -91,9 +95,36 @@ library_render_header(
This title is marked as login-required by the admin, so it stays hidden from the public reading experience.
-
-
+
+
+
+
+
+
+
+
+
Page
+
+ 1
+ /
+ --
+
+
+
+
+
+
Document stored
@@ -158,5 +189,142 @@ library_render_header(
+
+
+
+
+
+
+
+
query("SHOW COLUMNS FROM library_documents LIKE 'author'")->fetch();
+ if (!$exists) {
+ $sql = file_get_contents($migration6Path);
+ db()->exec($sql);
+ }
+ }
+
$uploadDir = __DIR__ . '/../uploads/library';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
@@ -162,6 +173,94 @@ function library_allowed_extensions(): array
];
}
+function library_file_url(string $path): string
+{
+ if ($path === '') return '';
+ if ($path[0] !== '/') {
+ return '/' . $path;
+ }
+ return $path;
+}
+
+function library_can_preview(array $doc): bool
+{
+ // Only PDFs are supported for inline preview in this version
+ return strtolower((string) ($doc['document_type'] ?? '')) === 'pdf';
+}
+
+function library_increment_views(int $id): void
+{
+ library_bootstrap();
+ $stmt = db()->prepare('UPDATE library_documents SET view_count = view_count + 1 WHERE id = ?');
+ $stmt->execute([$id]);
+}
+
+function library_generate_summary(int $id): array
+{
+ library_bootstrap();
+ $doc = library_fetch_document($id);
+ if (!$doc) {
+ return ['success' => false, 'message' => 'Document not found.'];
+ }
+
+ $descEn = trim((string) ($doc['description_en'] ?? ''));
+ $descAr = trim((string) ($doc['description_ar'] ?? ''));
+
+ if ($descEn === '' && $descAr === '') {
+ // Fallback to title if description is missing
+ $descEn = trim((string) ($doc['title_en'] ?? ''));
+ $descAr = trim((string) ($doc['title_ar'] ?? ''));
+ }
+
+ $prompt = "Please summarize the following document content into a concise paragraph in English and a concise paragraph in Arabic.\n\n";
+ if ($descEn) $prompt .= "English content: $descEn\n";
+ if ($descAr) $prompt .= "Arabic content: $descAr\n";
+
+ $prompt .= "\nReturn the result as valid JSON with keys 'summary_en' and 'summary_ar'.";
+
+ // Call AI
+ $resp = LocalAIApi::createResponse([
+ 'model' => 'gpt-5-mini',
+ 'messages' => [
+ ['role' => 'system', 'content' => 'You are a helpful bilingual assistant.'],
+ ['role' => 'user', 'content' => $prompt],
+ ]
+ ]);
+
+ if (empty($resp['success'])) {
+ return ['success' => false, 'message' => 'AI request failed: ' . ($resp['error'] ?? 'Unknown error')];
+ }
+
+ $text = LocalAIApi::extractText($resp);
+
+ $sumEn = '';
+ $sumAr = '';
+
+ // Try to parse JSON
+ $jsonStart = strpos($text, '{');
+ $jsonEnd = strrpos($text, '}');
+ if ($jsonStart !== false && $jsonEnd !== false) {
+ $jsonStr = substr($text, $jsonStart, $jsonEnd - $jsonStart + 1);
+ $data = json_decode($jsonStr, true);
+ $sumEn = $data['summary_en'] ?? '';
+ $sumAr = $data['summary_ar'] ?? '';
+ }
+
+ // If JSON parsing failed or returned empty strings, try to infer or use raw text
+ if (!$sumEn && !$sumAr) {
+ // If the AI just returned text, use it for English (or both)
+ $sumEn = $text;
+ }
+
+ // Update DB
+ $stmt = db()->prepare('UPDATE library_documents SET summary_text = ?, summary_en = ?, summary_ar = ? WHERE id = ?');
+ // Note: summary_text is legacy/combined, we can store JSON or just English
+ $combined = "English: $sumEn\n\nArabic: $sumAr";
+ $stmt->execute([$combined, $sumEn, $sumAr, $id]);
+
+ return ['success' => true, 'message' => 'Summary generated successfully.'];
+}
+
// --- Category Functions ---
function library_get_categories(string $search = ''): array
@@ -521,6 +620,8 @@ function library_create_document(array $payload, array $file, array $coverFile =
$pageCount = !empty($payload['page_count']) ? (int)$payload['page_count'] : null;
$summaryEn = trim((string) ($payload['summary_en'] ?? ''));
$summaryAr = trim((string) ($payload['summary_ar'] ?? ''));
+ $descriptionEn = trim((string) ($payload['description_en'] ?? ''));
+ $descriptionAr = trim((string) ($payload['description_ar'] ?? ''));
$fileData = library_handle_uploaded_file($file);
$coverPath = library_handle_cover_image($coverFile);
@@ -531,14 +632,16 @@ function library_create_document(array $payload, array $file, array $coverFile =
category_id, subcategory_id,
visibility, document_type,
file_name, file_path, file_size_kb, allow_download, allow_print, allow_copy,
- cover_image_path, publisher, publish_year, author, country, type_id, page_count, summary_en, summary_ar
+ cover_image_path, publisher, publish_year, author, country, type_id, page_count, summary_en, summary_ar,
+ description_en, description_ar
) VALUES (
:title_en, :title_ar,
:category, :category_ar, :sub_category, :sub_category_ar,
:category_id, :subcategory_id,
:visibility, :document_type,
:file_name, :file_path, :file_size_kb, :allow_download, :allow_print, :allow_copy,
- :cover_image_path, :publisher, :publish_year, :author, :country, :type_id, :page_count, :summary_en, :summary_ar
+ :cover_image_path, :publisher, :publish_year, :author, :country, :type_id, :page_count, :summary_en, :summary_ar,
+ :description_en, :description_ar
)');
$stmt->execute([
@@ -567,6 +670,8 @@ function library_create_document(array $payload, array $file, array $coverFile =
':page_count' => $pageCount,
':summary_en' => $summaryEn ?: null,
':summary_ar' => $summaryAr ?: null,
+ ':description_en' => $descriptionEn ?: null,
+ ':description_ar' => $descriptionAr ?: null,
]);
return (int) db()->lastInsertId();
@@ -622,6 +727,8 @@ function library_update_document(int $id, array $payload, array $file = [], arra
$pageCount = !empty($payload['page_count']) ? (int)$payload['page_count'] : null;
$summaryEn = trim((string) ($payload['summary_en'] ?? ''));
$summaryAr = trim((string) ($payload['summary_ar'] ?? ''));
+ $descriptionEn = trim((string) ($payload['description_en'] ?? ''));
+ $descriptionAr = trim((string) ($payload['description_ar'] ?? ''));
// Handle File Update
$fileData = null;
@@ -642,7 +749,8 @@ function library_update_document(int $id, array $payload, array $file = [], arra
visibility = :visibility,
allow_download = :allow_download, allow_print = :allow_print, allow_copy = :allow_copy,
publisher = :publisher, publish_year = :publish_year, author = :author, country = :country,
- type_id = :type_id, page_count = :page_count, summary_en = :summary_en, summary_ar = :summary_ar';
+ type_id = :type_id, page_count = :page_count, summary_en = :summary_en, summary_ar = :summary_ar,
+ description_en = :description_en, description_ar = :description_ar';
$params = [
':title_en' => $titleEn ?: null,
@@ -665,6 +773,8 @@ function library_update_document(int $id, array $payload, array $file = [], arra
':page_count' => $pageCount,
':summary_en' => $summaryEn ?: null,
':summary_ar' => $summaryAr ?: null,
+ ':description_en' => $descriptionEn ?: null,
+ ':description_ar' => $descriptionAr ?: null,
':id' => $id,
];
@@ -694,4 +804,4 @@ function library_delete_document(int $id): void
// For now, just delete DB record.
$stmt = db()->prepare('DELETE FROM library_documents WHERE id = ?');
$stmt->execute([$id]);
-}
\ No newline at end of file
+}
diff --git a/index.php b/index.php
index 0103368..c508262 100644
--- a/index.php
+++ b/index.php
@@ -137,7 +137,7 @@ library_render_header(
- Author
- - = h((string) ($document['author_name'] ?: 'Not set')) ?>
+ - = h((string) ($document['author'] ?: 'Not set')) ?>
- Views
- = h((string) $document['view_count']) ?>
- Tags
@@ -182,7 +182,7 @@ library_render_header(
= h(library_language_label((string) $document['document_language'])) ?>
= h((string) ($document['title_en'] ?: $document['title_ar'] ?: 'Untitled')) ?>
- = h((string) ($document['author_name'] ?: 'Unknown author')) ?>
+ = h((string) ($document['author'] ?: 'Unknown author')) ?>
diff --git a/uploads/library/20260325102925---8a635cbb.pdf b/uploads/library/20260325102925---8a635cbb.pdf
new file mode 100644
index 0000000..9b53d72
Binary files /dev/null and b/uploads/library/20260325102925---8a635cbb.pdf differ
diff --git a/uploads/library/20260325103107---099c7452.pdf b/uploads/library/20260325103107---099c7452.pdf
new file mode 100644
index 0000000..9b53d72
Binary files /dev/null and b/uploads/library/20260325103107---099c7452.pdf differ
diff --git a/uploads/library/20260325103121---3affd94e.pdf b/uploads/library/20260325103121---3affd94e.pdf
new file mode 100644
index 0000000..9b53d72
Binary files /dev/null and b/uploads/library/20260325103121---3affd94e.pdf differ
diff --git a/uploads/library/20260325103208---ed905fee.pdf b/uploads/library/20260325103208---ed905fee.pdf
new file mode 100644
index 0000000..9b53d72
Binary files /dev/null and b/uploads/library/20260325103208---ed905fee.pdf differ
diff --git a/viewer.php b/viewer.php
new file mode 100644
index 0000000..94175d6
--- /dev/null
+++ b/viewer.php
@@ -0,0 +1,539 @@
+ 0 ? library_fetch_document($documentId, $publicOnly) : null;
+
+if (!$document || empty($document['file_path'])) {
+ http_response_code(404);
+ die('Document not found or no file attached.');
+}
+
+$fileUrl = library_file_url((string) $document['file_path']);
+?>
+
+
+
+
+
+ = h($document['title_en'] ?: 'Document Viewer') ?> - Reader
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Loading Document...
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file