Autosave: 20260409-170145

This commit is contained in:
Flatlogic Bot 2026-04-09 17:01:46 +00:00
parent 3e7ac25120
commit 721fc0af97
13 changed files with 1129 additions and 320 deletions

20
GEMINI_RESPONSE.md Normal file
View File

@ -0,0 +1,20 @@
Plan:
1. **Bilingual Admin Panel:** Updated the entire Admin Panel (`admin.php`, `admin_documents.php`, `admin_categories.php`, etc.) to support both English and Arabic.
2. **Language Switcher:** Added a language toggle button to the admin sidebar in `includes/admin_layout.php`.
3. **RTL Support:** Configured `includes/admin_layout.php` to automatically switch to Right-to-Left (RTL) layout and load Bootstrap RTL when Arabic is selected.
4. **Translations:** Added comprehensive translation keys to `includes/library.php` covering dashboard metrics, tables, forms, buttons, and confirmation dialogs.
Changed (if any):
* `includes/library.php`: Added translation keys for admin panel terms.
* `includes/admin_layout.php`: Implemented language switcher and RTL logic.
* `admin.php`: Localized the Dashboard.
* `admin_documents.php`: Localized the Material Entry page.
* `admin_categories.php`: Localized the Categories management page.
* `admin_subcategories.php`: Localized the Subcategories management page.
* `admin_types.php`: Localized the Types management page.
Notes:
* **Try it out:** Go to the Admin Panel and click the "العربية" button in the sidebar to switch the entire interface to Arabic.
* **Reminder:** Click **Save** in the editor to apply these changes.
Next: Verify the Arabic layout and translations in the Admin Panel!

View File

@ -8,7 +8,7 @@ library_bootstrap();
$documents = library_fetch_documents(false, []);
$metrics = library_catalog_metrics();
admin_render_header('Dashboard', 'dashboard');
admin_render_header(library_trans('dashboard'), 'dashboard');
?>
<!-- Metrics -->
@ -16,7 +16,7 @@ admin_render_header('Dashboard', 'dashboard');
<div class="col-md-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="text-uppercase text-secondary small fw-bold">Total Documents</h6>
<h6 class="text-uppercase text-secondary small fw-bold"><?= library_trans('total_documents') ?></h6>
<div class="d-flex align-items-center justify-content-between mt-3">
<h2 class="display-6 fw-bold mb-0 text-primary"><?= number_format($metrics['total_documents']) ?></h2>
<i class="bi bi-file-earmark-text fs-1 text-primary opacity-25"></i>
@ -27,7 +27,7 @@ admin_render_header('Dashboard', 'dashboard');
<div class="col-md-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="text-uppercase text-secondary small fw-bold">Public Titles</h6>
<h6 class="text-uppercase text-secondary small fw-bold"><?= library_trans('public_titles') ?></h6>
<div class="d-flex align-items-center justify-content-between mt-3">
<h2 class="display-6 fw-bold mb-0 text-success"><?= number_format($metrics['public_documents']) ?></h2>
<i class="bi bi-globe fs-1 text-success opacity-25"></i>
@ -38,7 +38,7 @@ admin_render_header('Dashboard', 'dashboard');
<div class="col-md-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="text-uppercase text-secondary small fw-bold">Total Downloads</h6>
<h6 class="text-uppercase text-secondary small fw-bold"><?= library_trans('total_downloads') ?></h6>
<div class="d-flex align-items-center justify-content-between mt-3">
<h2 class="display-6 fw-bold mb-0 text-info"><?= number_format($metrics['total_downloads']) ?></h2>
<i class="bi bi-download fs-1 text-info opacity-25"></i>
@ -53,16 +53,16 @@ admin_render_header('Dashboard', 'dashboard');
<div class="col-lg-8">
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white py-3 border-bottom">
<h5 class="card-title mb-0">Recent Documents</h5>
<h5 class="card-title mb-0"><?= library_trans('recent_documents') ?></h5>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="ps-4">Title / Author</th>
<th>Type / Category</th>
<th>Visibility</th>
<th class="text-end pe-4">Actions</th>
<th class="ps-4"><?= library_trans('title_author') ?></th>
<th><?= library_trans('type_category') ?></th>
<th><?= library_trans('visibility') ?></th>
<th class="text-end pe-4"><?= library_trans('actions') ?></th>
</tr>
</thead>
<tbody>
@ -93,7 +93,7 @@ admin_render_header('Dashboard', 'dashboard');
</td>
<td class="text-end pe-4">
<a href="/document.php?id=<?= $doc['id'] ?>" class="btn btn-sm btn-outline-primary" target="_blank">
<i class="bi bi-eye"></i> View
<i class="bi bi-eye"></i> <?= library_trans('view') ?>
</a>
</td>
</tr>
@ -103,7 +103,7 @@ admin_render_header('Dashboard', 'dashboard');
</table>
</div>
<div class="card-footer bg-white text-center py-3">
<small class="text-muted">Showing last 10 uploads</small>
<small class="text-muted"><?= library_trans('showing_last_10') ?></small>
</div>
</div>
</div>
@ -112,18 +112,18 @@ admin_render_header('Dashboard', 'dashboard');
<div class="col-lg-4">
<div class="card shadow-sm border-0">
<div class="card-header bg-primary text-white py-3">
<h5 class="card-title mb-0"><i class="bi bi-folder2-open me-2"></i>Material Entry</h5>
<h5 class="card-title mb-0"><i class="bi bi-folder2-open me-2"></i><?= library_trans('material_entry') ?></h5>
</div>
<div class="card-body p-4 text-center">
<p class="text-secondary mb-4">
Manage your library documents, upload new materials, and edit metadata including bilingual titles, summaries, and more.
<?= library_trans('manage_documents_desc') ?>
</p>
<a href="/admin_documents.php" class="btn btn-primary w-100">
<i class="bi bi-plus-lg me-2"></i> Manage Documents
<i class="bi bi-plus-lg me-2"></i> <?= library_trans('manage_documents') ?>
</a>
</div>
</div>
</div>
</div>
<?php admin_render_footer(); ?>
<?php admin_render_footer(); ?>

View File

@ -47,11 +47,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
// Search Logic
// Search & Pagination Logic
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$limit = 10;
$offset = ($page - 1) * $limit;
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
$categories = library_get_categories($search);
admin_render_header('Categories', 'categories');
$result = library_get_categories_paginated($search, $limit, $offset);
$categories = $result['data'];
$totalCategories = $result['total'];
$totalPages = (int)ceil($totalCategories / $limit);
admin_render_header(library_trans('categories'), 'categories');
?>
<!-- Page Content -->
<?php if ($errors): ?>
@ -59,9 +66,9 @@ admin_render_header('Categories', 'categories');
<?php endif; ?>
<div class="d-flex justify-content-between align-items-center mb-4">
<p class="text-secondary mb-0">Manage document categories.</p>
<p class="text-secondary mb-0"><?= library_trans('manage_categories_desc') ?></p>
<button class="btn btn-primary" onclick="openCreateCategoryModal()">
<i class="bi bi-plus-lg me-1"></i> Add New Category
<i class="bi bi-plus-lg me-1"></i> <?= library_trans('add_new_category') ?>
</button>
</div>
@ -70,14 +77,14 @@ admin_render_header('Categories', 'categories');
<div class="card-body">
<form method="get" action="/admin_categories.php" class="row g-2 align-items-center">
<div class="col-auto flex-grow-1">
<input type="text" name="search" class="form-control" placeholder="Search categories..." value="<?= h($search) ?>">
<input type="text" name="search" class="form-control" placeholder="<?= library_trans('search_placeholder') ?>" value="<?= h($search) ?>">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-outline-primary">
<i class="bi bi-search"></i> Search
<i class="bi bi-search"></i> <?= library_trans('search') ?>
</button>
<?php if ($search): ?>
<a href="/admin_categories.php" class="btn btn-outline-secondary">Clear</a>
<a href="/admin_categories.php" class="btn btn-outline-secondary"><?= library_trans('clear') ?></a>
<?php endif; ?>
</div>
</form>
@ -90,13 +97,13 @@ admin_render_header('Categories', 'categories');
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="ps-4">Name</th>
<th class="text-end pe-4">Actions</th>
<th class="ps-4"><?= library_trans('name') ?></th>
<th class="text-end pe-4"><?= library_trans('actions') ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($categories)): ?>
<tr><td colspan="2" class="text-center py-5 text-muted">No categories found.</td></tr>
<tr><td colspan="2" class="text-center py-5 text-muted"><?= library_trans('no_categories_found') ?></td></tr>
<?php else: ?>
<?php foreach ($categories as $cat): ?>
<tr>
@ -110,10 +117,10 @@ admin_render_header('Categories', 'categories');
data-name-en="<?= h($cat['name_en']) ?>"
data-name-ar="<?= h($cat['name_ar']) ?>"
onclick="openEditCategoryModal(this)">
<i class="bi bi-pencil"></i> Edit
<i class="bi bi-pencil"></i> <?= library_trans('edit') ?>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteCategory(<?= $cat['id'] ?>)">
<i class="bi bi-trash"></i> Delete
<i class="bi bi-trash"></i> <?= library_trans('delete') ?>
</button>
</td>
</tr>
@ -122,6 +129,13 @@ admin_render_header('Categories', 'categories');
</tbody>
</table>
</div>
<!-- Pagination -->
<?php if ($totalPages > 1): ?>
<div class="p-3 border-top">
<?php library_render_pagination($page, $totalPages, '/admin_categories.php'); ?>
</div>
<?php endif; ?>
</div>
</div>
@ -133,12 +147,12 @@ admin_render_header('Categories', 'categories');
<input type="hidden" name="action" id="cat_action" value="create_category">
<input type="hidden" name="id" id="cat_id" value="">
<div class="modal-header">
<h5 class="modal-title" id="categoryModalTitle">Add New Category</h5>
<h5 class="modal-title" id="categoryModalTitle"><?= library_trans('add_new_category') ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Name (English)</label>
<label class="form-label"><?= library_trans('name_en') ?></label>
<div class="input-group">
<input type="text" class="form-control" name="name_en" id="cat_name_en" required>
<button class="btn btn-outline-secondary" type="button" onclick="translateText('cat_name_en', 'cat_name_ar', 'Arabic')" title="Translate to Arabic">
@ -147,7 +161,7 @@ admin_render_header('Categories', 'categories');
</div>
</div>
<div class="mb-3">
<label class="form-label">Name (Arabic)</label>
<label class="form-label"><?= library_trans('name_ar') ?></label>
<div class="input-group">
<input type="text" class="form-control" name="name_ar" id="cat_name_ar" dir="rtl" required>
<button class="btn btn-outline-secondary" type="button" onclick="translateText('cat_name_ar', 'cat_name_en', 'English')" title="Translate to English">
@ -157,8 +171,8 @@ admin_render_header('Categories', 'categories');
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?= library_trans('cancel') ?></button>
<button type="submit" class="btn btn-primary"><?= library_trans('save') ?></button>
</div>
</form>
</div>
@ -179,7 +193,7 @@ admin_render_header('Categories', 'categories');
});
function openCreateCategoryModal() {
document.getElementById('categoryModalTitle').innerText = 'Add New Category';
document.getElementById('categoryModalTitle').innerText = '<?= library_trans('add_new_category') ?>';
document.getElementById('cat_action').value = 'create_category';
document.getElementById('cat_id').value = '';
document.getElementById('cat_name_en').value = '';
@ -192,7 +206,8 @@ admin_render_header('Categories', 'categories');
const nameEn = btn.getAttribute('data-name-en');
const nameAr = btn.getAttribute('data-name-ar');
document.getElementById('categoryModalTitle').innerText = 'Edit Category';
document.getElementById('categoryModalTitle').innerText = '<?= library_trans('edit') ?>';
document.getElementById('cat_action').value = 'update_category';
document.getElementById('cat_id').value = id;
document.getElementById('cat_name_en').value = nameEn;
@ -201,7 +216,7 @@ admin_render_header('Categories', 'categories');
}
function deleteCategory(id) {
if (confirm('Are you sure you want to delete this category? All related subcategories will also be deleted.')) {
if (confirm('<?= library_trans('confirm_delete') ?>')) {
document.getElementById('deleteAction').value = 'delete_category';
document.getElementById('deleteId').value = id;
document.getElementById('deleteForm').submit();

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
// Debug Logging
@ -52,22 +53,22 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
// Search Logic
// Search & Pagination Logic
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$limit = 10;
$offset = ($page - 1) * $limit;
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
$documents = library_fetch_documents(false);
if ($search !== '') {
$documents = array_filter($documents, function($doc) use ($search) {
return stripos($doc['title_en'] ?? '', $search) !== false
|| stripos($doc['title_ar'] ?? '', $search) !== false
|| stripos($doc['author'] ?? '', $search) !== false;
});
}
$result = library_fetch_documents_paginated(false, ['q' => $search], $limit, $offset);
$documents = $result['data'];
$totalDocuments = $result['total'];
$totalPages = (int)ceil($totalDocuments / $limit);
$categories = library_get_categories();
$allSubcategories = library_get_subcategories(null);
$types = library_get_types();
admin_render_header('Material Entry', 'documents');
admin_render_header(library_trans('material_entry'), 'documents');
?>
<!-- Page Content -->
<?php if ($errors): ?>
@ -82,9 +83,9 @@ admin_render_header('Material Entry', 'documents');
<?php endif; ?>
<div class="d-flex justify-content-between align-items-center mb-4">
<p class="text-secondary mb-0">Manage library documents (Material Entry).</p>
<p class="text-secondary mb-0"><?= library_trans('manage_documents_desc') ?></p>
<button class="btn btn-primary" onclick="openCreateModal()">
<i class="bi bi-plus-lg me-1"></i> Add New Document
<i class="bi bi-plus-lg me-1"></i> <?= library_trans('add_new_document') ?>
</button>
</div>
@ -93,14 +94,14 @@ admin_render_header('Material Entry', 'documents');
<div class="card-body">
<form method="get" action="/admin_documents.php" class="row g-2 align-items-center">
<div class="col-auto flex-grow-1">
<input type="text" name="search" class="form-control" placeholder="Search by title or author..." value="<?= h($search) ?>">
<input type="text" name="search" class="form-control" placeholder="<?= library_trans('search_docs_placeholder') ?>" value="<?= h($search) ?>">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-outline-primary">
<i class="bi bi-search"></i> Search
<i class="bi bi-search"></i> <?= library_trans('search') ?>
</button>
<?php if ($search): ?>
<a href="/admin_documents.php" class="btn btn-outline-secondary">Clear</a>
<a href="/admin_documents.php" class="btn btn-outline-secondary"><?= library_trans('clear') ?></a>
<?php endif; ?>
</div>
</form>
@ -113,12 +114,12 @@ admin_render_header('Material Entry', 'documents');
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="ps-4">ID</th>
<th>Cover</th>
<th>Title / Author</th>
<th>Type / Category</th>
<th>Year</th>
<th class="text-end pe-4">Actions</th>
<th class="ps-4"><?= library_trans('id') ?></th>
<th><?= library_trans('cover') ?></th>
<th><?= library_trans('title_author') ?></th>
<th><?= library_trans('type_category') ?></th>
<th><?= library_trans('year') ?></th>
<th class="text-end pe-4"><?= library_trans('actions') ?></th>
</tr>
</thead>
<tbody>
@ -156,14 +157,14 @@ admin_render_header('Material Entry', 'documents');
</td>
<td><?= h((string)$doc['publish_year']) ?></td>
<td class="text-end pe-4">
<a href="/document.php?id=<?= $doc['id'] ?>" target="_blank" class="btn btn-sm btn-outline-secondary me-1" title="View">
<a href="/document.php?id=<?= $doc['id'] ?>" target="_blank" class="btn btn-sm btn-outline-secondary me-1" title="<?= library_trans('view') ?>">
<i class="bi bi-eye"></i>
</a>
<button class="btn btn-sm btn-outline-primary me-1"
onclick='openEditModal(<?= json_encode($doc) ?>)'>
<i class="bi bi-pencil"></i> Edit
onclick='openEditModal(<?= json_encode($doc) ?>)' title="<?= library_trans('edit') ?>">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteDocument(<?= $doc['id'] ?>)">
<button class="btn btn-sm btn-outline-danger" onclick="deleteDocument(<?= $doc['id'] ?>)" title="<?= library_trans('delete') ?>">
<i class="bi bi-trash"></i>
</button>
</td>
@ -173,6 +174,13 @@ admin_render_header('Material Entry', 'documents');
</tbody>
</table>
</div>
<!-- Pagination -->
<?php if ($totalPages > 1): ?>
<div class="p-3 border-top">
<?php library_render_pagination($page, $totalPages, '/admin_documents.php'); ?>
</div>
<?php endif; ?>
</div>
</div>
@ -181,12 +189,12 @@ admin_render_header('Material Entry', 'documents');
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<form class="modal-content" method="post" action="/admin_documents.php" id="documentForm" enctype="multipart/form-data">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="documentModalTitle">Add New Material</h5>
<h5 class="modal-title" id="documentModalTitle"><?= library_trans('add_new_document') ?></h5>
<div class="ms-auto">
<button type="button" class="btn btn-link text-white text-decoration-none me-2" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-link text-white text-decoration-none me-2" data-bs-dismiss="modal"><?= library_trans('cancel') ?></button>
<button type="submit" class="btn btn-light text-primary fw-bold" id="saveButton">
<span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
Save Changes
<?= library_trans('save_changes') ?>
</button>
</div>
</div>
@ -197,14 +205,14 @@ admin_render_header('Material Entry', 'documents');
<div class="row g-3">
<!-- Titles -->
<div class="col-md-6">
<label class="form-label small fw-bold text-uppercase text-muted">Title (English) <span class="text-danger">*</span></label>
<label class="form-label small fw-bold text-uppercase text-muted"><?= library_trans('name_en') ?> <span class="text-danger">*</span></label>
<div class="input-group">
<input type="text" class="form-control" name="title_en" id="doc_title_en" required>
<button class="btn btn-outline-secondary" type="button" onclick="translateText('doc_title_en', 'doc_title_ar', 'Arabic')"><i class="bi bi-translate"></i></button>
</div>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-uppercase text-muted">Title (Arabic)</label>
<label class="form-label small fw-bold text-uppercase text-muted"><?= library_trans('name_ar') ?></label>
<div class="input-group">
<input type="text" class="form-control" name="title_ar" id="doc_title_ar" dir="rtl">
<button class="btn btn-outline-secondary" type="button" onclick="translateText('doc_title_ar', 'doc_title_en', 'English')"><i class="bi bi-translate"></i></button>
@ -213,64 +221,64 @@ admin_render_header('Material Entry', 'documents');
<!-- Basic Info -->
<div class="col-md-6">
<label class="form-label small fw-bold text-uppercase text-muted">Author</label>
<label class="form-label small fw-bold text-uppercase text-muted"><?= library_trans('author') ?></label>
<input type="text" class="form-control" name="author" id="doc_author">
</div>
<div class="col-md-3">
<label class="form-label small fw-bold text-uppercase text-muted">Publisher</label>
<label class="form-label small fw-bold text-uppercase text-muted"><?= library_trans('publisher') ?></label>
<input type="text" class="form-control" name="publisher" id="doc_publisher">
</div>
<div class="col-md-3">
<label class="form-label small fw-bold text-uppercase text-muted">Year</label>
<label class="form-label small fw-bold text-uppercase text-muted"><?= library_trans('year') ?></label>
<input type="number" class="form-control" name="publish_year" id="doc_publish_year" min="1000" max="2100">
</div>
<!-- Classification -->
<div class="col-md-4">
<label class="form-label small fw-bold text-uppercase text-muted">Type</label>
<label class="form-label small fw-bold text-uppercase text-muted"><?= library_trans('types') ?></label>
<select class="form-select" name="type_id" id="doc_type_id">
<option value="">Select Type...</option>
<option value=""><?= library_trans('select_type') ?></option>
<?php foreach ($types as $t): ?>
<option value="<?= $t['id'] ?>"><?= h($t['name_en']) ?> / <?= h($t['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold text-uppercase text-muted">Category</label>
<label class="form-label small fw-bold text-uppercase text-muted"><?= library_trans('category') ?></label>
<select class="form-select" name="category_id" id="doc_category_id" onchange="updateSubcategories()">
<option value="">Select Category...</option>
<option value=""><?= library_trans('select_category') ?></option>
<?php foreach ($categories as $c): ?>
<option value="<?= $c['id'] ?>"><?= h($c['name_en']) ?> / <?= h($c['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold text-uppercase text-muted">Subcategory</label>
<label class="form-label small fw-bold text-uppercase text-muted"><?= library_trans('subcategories') ?></label>
<select class="form-select" name="subcategory_id" id="doc_subcategory_id">
<option value="">Select Category First...</option>
<option value=""><?= library_trans('select_subcategory') ?></option>
</select>
</div>
<!-- Details -->
<div class="col-md-6">
<label class="form-label small fw-bold text-uppercase text-muted">Country</label>
<label class="form-label small fw-bold text-uppercase text-muted"><?= library_trans('country') ?></label>
<input type="text" class="form-control" name="country" id="doc_country">
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-uppercase text-muted">Total Pages</label>
<label class="form-label small fw-bold text-uppercase text-muted"><?= library_trans('page_count') ?></label>
<input type="number" class="form-control" name="page_count" id="doc_page_count" min="0">
</div>
<!-- Summaries -->
<div class="col-md-6">
<label class="form-label small fw-bold text-uppercase text-muted">Summary (English)</label>
<label class="form-label small fw-bold text-uppercase text-muted"><?= library_trans('summary_en') ?></label>
<div class="input-group">
<textarea class="form-control" name="summary_en" id="doc_summary_en" rows="3"></textarea>
<button class="btn btn-outline-secondary" type="button" onclick="translateText('doc_summary_en', 'doc_summary_ar', 'Arabic')"><i class="bi bi-translate"></i></button>
</div>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-uppercase text-muted">Summary (Arabic)</label>
<label class="form-label small fw-bold text-uppercase text-muted"><?= library_trans('summary_ar') ?></label>
<div class="input-group">
<textarea class="form-control" name="summary_ar" id="doc_summary_ar" rows="3" dir="rtl"></textarea>
<button class="btn btn-outline-secondary" type="button" onclick="translateText('doc_summary_ar', 'doc_summary_en', 'English')"><i class="bi bi-translate"></i></button>
@ -279,7 +287,7 @@ admin_render_header('Material Entry', 'documents');
<!-- Files -->
<div class="col-md-6">
<label class="form-label small fw-bold text-uppercase text-muted">Front Cover Image</label>
<label class="form-label small fw-bold text-uppercase text-muted"><?= library_trans('cover_image') ?></label>
<input class="form-control" type="file" name="cover_file" accept="image/*">
<div id="current_cover_preview" class="mt-2 d-none">
<small class="text-muted d-block mb-1">Current Cover:</small>
@ -287,19 +295,19 @@ admin_render_header('Material Entry', 'documents');
</div>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-uppercase text-muted">Document File <span id="file_required_indicator" class="text-danger">*</span></label>
<label class="form-label small fw-bold text-uppercase text-muted"><?= library_trans('document_file') ?> <span id="file_required_indicator" class="text-danger">*</span></label>
<input class="form-control" type="file" name="document_file" id="document_file_input">
<div id="current_file_info" class="mt-2 d-none">
<small class="text-muted"><i class="bi bi-file-earmark"></i> <span id="current_filename"></span></small>
</div>
<small class="text-muted d-block mt-1" id="file_help_text">Required for new documents.</small>
<small class="text-muted d-block mt-1" id="file_help_text"><?= library_trans('required_new_docs') ?></small>
</div>
<!-- Settings -->
<div class="col-12">
<div class="card bg-light border-0">
<div class="card-body">
<h6 class="card-subtitle mb-2 text-muted text-uppercase small fw-bold">Visibility & Permissions</h6>
<h6 class="card-subtitle mb-2 text-muted text-uppercase small fw-bold"><?= library_trans('visibility_permissions') ?></h6>
<div class="row">
<div class="col-md-3">
<select class="form-select form-select-sm" name="visibility" id="doc_visibility">
@ -310,15 +318,15 @@ admin_render_header('Material Entry', 'documents');
<div class="col-md-9 d-flex align-items-center gap-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_download" id="doc_allow_download" value="1">
<label class="form-check-label small" for="doc_allow_download">Download</label>
<label class="form-check-label small" for="doc_allow_download"><?= library_trans('download') ?></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_print" id="doc_allow_print" value="1">
<label class="form-check-label small" for="doc_allow_print">Print</label>
<label class="form-check-label small" for="doc_allow_print"><?= library_trans('print') ?></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_copy" id="doc_allow_copy" value="1">
<label class="form-check-label small" for="doc_allow_copy">Copy</label>
<label class="form-check-label small" for="doc_allow_copy"><?= library_trans('copy') ?></label>
</div>
</div>
</div>
@ -357,7 +365,7 @@ admin_render_header('Material Entry', 'documents');
const subSelect = document.getElementById('doc_subcategory_id');
const catId = catSelect.value;
subSelect.innerHTML = '<option value="">Select Subcategory...</option>';
subSelect.innerHTML = '<option value=""><?= library_trans('select_subcategory') ?></option>';
if (catId) {
const subs = allSubcategories.filter(s => s.category_id == catId);
@ -372,7 +380,7 @@ admin_render_header('Material Entry', 'documents');
}
function openCreateModal() {
document.getElementById('documentModalTitle').innerText = 'Add New Material';
document.getElementById('documentModalTitle').innerText = '<?= library_trans('add_new_document') ?>';
document.getElementById('doc_action').value = 'create_document';
document.getElementById('doc_id').value = '';
document.getElementById('documentForm').reset();
@ -380,7 +388,7 @@ admin_render_header('Material Entry', 'documents');
// 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.';
document.getElementById('file_help_text').innerText = '<?= library_trans('required_new_docs') ?>';
// Clear previews
document.getElementById('current_cover_preview').classList.add('d-none');
@ -393,14 +401,14 @@ admin_render_header('Material Entry', 'documents');
}
function openEditModal(doc) {
document.getElementById('documentModalTitle').innerText = 'Edit Material: ' + doc.title_en;
document.getElementById('documentModalTitle').innerText = '<?= library_trans('edit') ?>'; // + doc.title_en;
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.';
document.getElementById('file_help_text').innerText = '<?= library_trans('leave_empty_keep') ?>';
// Fill fields
document.getElementById('doc_title_en').value = doc.title_en || '';
@ -445,7 +453,7 @@ admin_render_header('Material Entry', 'documents');
}
function deleteDocument(id) {
if (confirm('Are you sure you want to delete this document?')) {
if (confirm('<?= library_trans('confirm_delete') ?>')) {
document.getElementById('deleteAction').value = 'delete_document';
document.getElementById('deleteId').value = id;
document.getElementById('deleteForm').submit();

View File

@ -49,12 +49,22 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
// Search Logic
// Search & Pagination Logic
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$limit = 10;
$offset = ($page - 1) * $limit;
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
$categories = library_get_categories();
$allSubcategories = library_get_subcategories(null, $search);
admin_render_header('Subcategories', 'subcategories');
// Get categories for lookup/filter
$categories = library_get_categories();
// Paginated Fetch
$result = library_get_subcategories_paginated(null, $search, $limit, $offset);
$allSubcategories = $result['data'];
$totalSubcategories = $result['total'];
$totalPages = (int)ceil($totalSubcategories / $limit);
admin_render_header(library_trans('subcategories'), 'subcategories');
?>
<!-- Page Content -->
<?php if ($errors): ?>
@ -62,9 +72,9 @@ admin_render_header('Subcategories', 'subcategories');
<?php endif; ?>
<div class="d-flex justify-content-between align-items-center mb-4">
<p class="text-secondary mb-0">Manage document subcategories.</p>
<p class="text-secondary mb-0"><?= library_trans('manage_subcategories_desc') ?></p>
<button class="btn btn-primary" onclick="openCreateSubcategoryModal()">
<i class="bi bi-plus-lg me-1"></i> Add New Subcategory
<i class="bi bi-plus-lg me-1"></i> <?= library_trans('add_new_subcategory') ?>
</button>
</div>
@ -73,14 +83,14 @@ admin_render_header('Subcategories', 'subcategories');
<div class="card-body">
<form method="get" action="/admin_subcategories.php" class="row g-2 align-items-center">
<div class="col-auto flex-grow-1">
<input type="text" name="search" class="form-control" placeholder="Search subcategories..." value="<?= h($search) ?>">
<input type="text" name="search" class="form-control" placeholder="<?= library_trans('search_placeholder') ?>" value="<?= h($search) ?>">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-outline-primary">
<i class="bi bi-search"></i> Search
<i class="bi bi-search"></i> <?= library_trans('search') ?>
</button>
<?php if ($search): ?>
<a href="/admin_subcategories.php" class="btn btn-outline-secondary">Clear</a>
<a href="/admin_subcategories.php" class="btn btn-outline-secondary"><?= library_trans('clear') ?></a>
<?php endif; ?>
</div>
</form>
@ -93,20 +103,20 @@ admin_render_header('Subcategories', 'subcategories');
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="ps-4">Name</th>
<th>Parent Category</th>
<th class="text-end pe-4">Actions</th>
<th class="ps-4"><?= library_trans('name') ?></th>
<th><?= library_trans('category') ?></th>
<th class="text-end pe-4"><?= library_trans('actions') ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($allSubcategories)): ?>
<tr><td colspan="3" class="text-center py-5 text-muted">No subcategories found.</td></tr>
<tr><td colspan="3" class="text-center py-5 text-muted"><?= library_trans('no_subcategories_found') ?></td></tr>
<?php else: ?>
<?php foreach ($allSubcategories as $sub):
$parentName = 'Unknown';
foreach ($categories as $c) {
if ($c['id'] == $sub['category_id']) {
$parentName = $c['name_en'];
$parentName = $c['name_en'] . ' / ' . $c['name_ar']; // Show bilingual name for context
break;
}
}
@ -126,10 +136,10 @@ admin_render_header('Subcategories', 'subcategories');
data-name-en="<?= h($sub['name_en']) ?>"
data-name-ar="<?= h($sub['name_ar']) ?>"
onclick="openEditSubcategoryModal(this)">
<i class="bi bi-pencil"></i> Edit
<i class="bi bi-pencil"></i> <?= library_trans('edit') ?>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteSubcategory(<?= $sub['id'] ?>)">
<i class="bi bi-trash"></i> Delete
<i class="bi bi-trash"></i> <?= library_trans('delete') ?>
</button>
</td>
</tr>
@ -138,6 +148,13 @@ admin_render_header('Subcategories', 'subcategories');
</tbody>
</table>
</div>
<!-- Pagination -->
<?php if ($totalPages > 1): ?>
<div class="p-3 border-top">
<?php library_render_pagination($page, $totalPages, '/admin_subcategories.php'); ?>
</div>
<?php endif; ?>
</div>
</div>
@ -149,21 +166,21 @@ admin_render_header('Subcategories', 'subcategories');
<input type="hidden" name="action" id="sub_action" value="create_subcategory">
<input type="hidden" name="id" id="sub_id" value="">
<div class="modal-header">
<h5 class="modal-title" id="subcategoryModalTitle">Add New Subcategory</h5>
<h5 class="modal-title" id="subcategoryModalTitle"><?= library_trans('add_new_subcategory') ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Parent Category</label>
<label class="form-label"><?= library_trans('category') ?></label>
<select class="form-select" name="category_id" id="sub_category_id" required>
<option value="">Select...</option>
<option value=""><?= library_trans('select_category') ?></option>
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat['id'] ?>"><?= h($cat['name_en']) ?></option>
<option value="<?= $cat['id'] ?>"><?= h($cat['name_en']) ?> / <?= h($cat['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label">Name (English)</label>
<label class="form-label"><?= library_trans('name_en') ?></label>
<div class="input-group">
<input type="text" class="form-control" name="name_en" id="sub_name_en" required>
<button class="btn btn-outline-secondary" type="button" onclick="translateText('sub_name_en', 'sub_name_ar', 'Arabic')" title="Translate to Arabic">
@ -172,7 +189,7 @@ admin_render_header('Subcategories', 'subcategories');
</div>
</div>
<div class="mb-3">
<label class="form-label">Name (Arabic)</label>
<label class="form-label"><?= library_trans('name_ar') ?></label>
<div class="input-group">
<input type="text" class="form-control" name="name_ar" id="sub_name_ar" dir="rtl" required>
<button class="btn btn-outline-secondary" type="button" onclick="translateText('sub_name_ar', 'sub_name_en', 'English')" title="Translate to English">
@ -182,8 +199,8 @@ admin_render_header('Subcategories', 'subcategories');
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?= library_trans('cancel') ?></button>
<button type="submit" class="btn btn-primary"><?= library_trans('save') ?></button>
</div>
</form>
</div>
@ -204,7 +221,7 @@ admin_render_header('Subcategories', 'subcategories');
});
function openCreateSubcategoryModal() {
document.getElementById('subcategoryModalTitle').innerText = 'Add New Subcategory';
document.getElementById('subcategoryModalTitle').innerText = '<?= library_trans('add_new_subcategory') ?>';
document.getElementById('sub_action').value = 'create_subcategory';
document.getElementById('sub_id').value = '';
document.getElementById('sub_category_id').value = '';
@ -219,7 +236,7 @@ admin_render_header('Subcategories', 'subcategories');
const nameEn = btn.getAttribute('data-name-en');
const nameAr = btn.getAttribute('data-name-ar');
document.getElementById('subcategoryModalTitle').innerText = 'Edit Subcategory';
document.getElementById('subcategoryModalTitle').innerText = '<?= library_trans('edit') ?>';
document.getElementById('sub_action').value = 'update_subcategory';
document.getElementById('sub_id').value = id;
document.getElementById('sub_category_id').value = catId;
@ -229,7 +246,7 @@ admin_render_header('Subcategories', 'subcategories');
}
function deleteSubcategory(id) {
if (confirm('Are you sure you want to delete this subcategory?')) {
if (confirm('<?= library_trans('confirm_delete') ?>')) {
document.getElementById('deleteAction').value = 'delete_subcategory';
document.getElementById('deleteId').value = id;
document.getElementById('deleteForm').submit();

View File

@ -47,11 +47,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
// Search Logic
// Search & Pagination Logic
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$limit = 10;
$offset = ($page - 1) * $limit;
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
$types = library_get_types($search);
admin_render_header('Document Types', 'types');
$result = library_get_types_paginated($search, $limit, $offset);
$types = $result['data'];
$totalTypes = $result['total'];
$totalPages = (int)ceil($totalTypes / $limit);
admin_render_header(library_trans('types'), 'types');
?>
<!-- Page Content -->
<?php if ($errors): ?>
@ -59,9 +66,9 @@ admin_render_header('Document Types', 'types');
<?php endif; ?>
<div class="d-flex justify-content-between align-items-center mb-4">
<p class="text-secondary mb-0">Manage document types (e.g., Document, Film, etc.).</p>
<p class="text-secondary mb-0"><?= library_trans('manage_types_desc') ?></p>
<button class="btn btn-primary" onclick="openCreateTypeModal()">
<i class="bi bi-plus-lg me-1"></i> Add New Type
<i class="bi bi-plus-lg me-1"></i> <?= library_trans('add_new_type') ?>
</button>
</div>
@ -70,14 +77,14 @@ admin_render_header('Document Types', 'types');
<div class="card-body">
<form method="get" action="/admin_types.php" class="row g-2 align-items-center">
<div class="col-auto flex-grow-1">
<input type="text" name="search" class="form-control" placeholder="Search types..." value="<?= h($search) ?>">
<input type="text" name="search" class="form-control" placeholder="<?= library_trans('search_placeholder') ?>" value="<?= h($search) ?>">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-outline-primary">
<i class="bi bi-search"></i> Search
<i class="bi bi-search"></i> <?= library_trans('search') ?>
</button>
<?php if ($search): ?>
<a href="/admin_types.php" class="btn btn-outline-secondary">Clear</a>
<a href="/admin_types.php" class="btn btn-outline-secondary"><?= library_trans('clear') ?></a>
<?php endif; ?>
</div>
</form>
@ -90,13 +97,13 @@ admin_render_header('Document Types', 'types');
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="ps-4">Name</th>
<th class="text-end pe-4">Actions</th>
<th class="ps-4"><?= library_trans('name') ?></th>
<th class="text-end pe-4"><?= library_trans('actions') ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($types)): ?>
<tr><td colspan="2" class="text-center py-5 text-muted">No types found.</td></tr>
<tr><td colspan="2" class="text-center py-5 text-muted"><?= library_trans('no_types_found') ?></td></tr>
<?php else: ?>
<?php foreach ($types as $type): ?>
<tr>
@ -110,10 +117,10 @@ admin_render_header('Document Types', 'types');
data-name-en="<?= h($type['name_en']) ?>"
data-name-ar="<?= h($type['name_ar']) ?>"
onclick="openEditTypeModal(this)">
<i class="bi bi-pencil"></i> Edit
<i class="bi bi-pencil"></i> <?= library_trans('edit') ?>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteType(<?= $type['id'] ?>)">
<i class="bi bi-trash"></i> Delete
<i class="bi bi-trash"></i> <?= library_trans('delete') ?>
</button>
</td>
</tr>
@ -122,6 +129,13 @@ admin_render_header('Document Types', 'types');
</tbody>
</table>
</div>
<!-- Pagination -->
<?php if ($totalPages > 1): ?>
<div class="p-3 border-top">
<?php library_render_pagination($page, $totalPages, '/admin_types.php'); ?>
</div>
<?php endif; ?>
</div>
</div>
@ -133,12 +147,12 @@ admin_render_header('Document Types', 'types');
<input type="hidden" name="action" id="type_action" value="create_type">
<input type="hidden" name="id" id="type_id" value="">
<div class="modal-header">
<h5 class="modal-title" id="typeModalTitle">Add New Type</h5>
<h5 class="modal-title" id="typeModalTitle"><?= library_trans('add_new_type') ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Name (English)</label>
<label class="form-label"><?= library_trans('name_en') ?></label>
<div class="input-group">
<input type="text" class="form-control" name="name_en" id="type_name_en" required>
<button class="btn btn-outline-secondary" type="button" onclick="translateText('type_name_en', 'type_name_ar', 'Arabic')" title="Translate to Arabic">
@ -147,7 +161,7 @@ admin_render_header('Document Types', 'types');
</div>
</div>
<div class="mb-3">
<label class="form-label">Name (Arabic)</label>
<label class="form-label"><?= library_trans('name_ar') ?></label>
<div class="input-group">
<input type="text" class="form-control" name="name_ar" id="type_name_ar" dir="rtl" required>
<button class="btn btn-outline-secondary" type="button" onclick="translateText('type_name_ar', 'type_name_en', 'English')" title="Translate to English">
@ -157,8 +171,8 @@ admin_render_header('Document Types', 'types');
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?= library_trans('cancel') ?></button>
<button type="submit" class="btn btn-primary"><?= library_trans('save') ?></button>
</div>
</form>
</div>
@ -179,7 +193,7 @@ admin_render_header('Document Types', 'types');
});
function openCreateTypeModal() {
document.getElementById('typeModalTitle').innerText = 'Add New Type';
document.getElementById('typeModalTitle').innerText = '<?= library_trans('add_new_type') ?>';
document.getElementById('type_action').value = 'create_type';
document.getElementById('type_id').value = '';
document.getElementById('type_name_en').value = '';
@ -192,7 +206,7 @@ admin_render_header('Document Types', 'types');
const nameEn = btn.getAttribute('data-name-en');
const nameAr = btn.getAttribute('data-name-ar');
document.getElementById('typeModalTitle').innerText = 'Edit Type';
document.getElementById('typeModalTitle').innerText = '<?= library_trans('edit') ?>';
document.getElementById('type_action').value = 'update_type';
document.getElementById('type_id').value = id;
document.getElementById('type_name_en').value = nameEn;
@ -201,7 +215,7 @@ admin_render_header('Document Types', 'types');
}
function deleteType(id) {
if (confirm('Are you sure you want to delete this type?')) {
if (confirm('<?= library_trans('confirm_delete') ?>')) {
document.getElementById('deleteAction').value = 'delete_type';
document.getElementById('deleteId').value = id;
document.getElementById('deleteForm').submit();

View File

@ -24,10 +24,29 @@ html {
body {
background: var(--bg);
color: var(--text);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-family: Inter, "Noto Sans Arabic", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Tahoma, sans-serif;
min-height: 100vh;
}
[lang="ar"],
[dir="rtl"] {
font-family: "Noto Sans Arabic", "Segoe UI", Tahoma, Arial, sans-serif;
line-height: 1.75;
text-align: start;
unicode-bidi: plaintext;
}
[lang="ar"] .display-6,
[lang="ar"] .h3,
[lang="ar"] .h4,
[lang="ar"] .h5,
[dir="rtl"] .display-6,
[dir="rtl"] .h3,
[dir="rtl"] .h4,
[dir="rtl"] .h5 {
letter-spacing: 0;
}
::selection {
background: #dbe2ea;
}

View File

@ -40,7 +40,7 @@ if ($context !== 'admin') {
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.',
'Read a library document online, review metadata, and generate a bilingual AI summary from the document content.',
$context === 'admin' ? 'admin' : 'catalog'
);
?>
@ -57,7 +57,7 @@ library_render_header(
<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>
<div class="lead text-secondary" dir="rtl" lang="ar"><?= h((string) $document['title_ar']) ?></div>
<?php endif; ?>
</div>
<div class="d-flex flex-wrap gap-2 align-content-start">
@ -96,13 +96,13 @@ library_render_header(
</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-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>Loading Book...</div>
</div>
<!-- The actual book container for PageFlip -->
<div id="flipbook" class="shadow-lg" style="display:none;"></div>
<div id="flipbook" class="shadow-lg" dir="ltr" style="display:none; direction: ltr;"></div>
</div>
<!-- Custom Toolbar -->
@ -156,7 +156,7 @@ library_render_header(
<?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>
<div class="summary-box summary-box-muted">No AI summary yet. Click "Generate" to create a bilingual summary from the document content.</div>
<?php endif; ?>
</div>
@ -183,7 +183,7 @@ library_render_header(
</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>
<p class="mb-0 text-secondary" dir="rtl" lang="ar"><?= h((string) ($document['description_ar'] ?: 'لا يوجد مقتطف عربي حتى الآن.')) ?></p>
</div>
</div>
</div>
@ -201,6 +201,11 @@ document.addEventListener('DOMContentLoaded', async () => {
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');
@ -222,18 +227,15 @@ document.addEventListener('DOMContentLoaded', async () => {
// For this demo, we render all to allow smooth flipping.
const canvasPromises = [];
// Calculate dimensions based on the first page
// 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 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 });
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');
@ -249,13 +251,16 @@ document.addEventListener('DOMContentLoaded', async () => {
canvasPromises.push(async () => {
const page = await pdf.getPage(i);
const vp = page.getViewport({ scale: scale });
canvas.height = vp.height;
canvas.width = vp.width;
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'),
viewport: vp
canvasContext: canvas.getContext('2d', { alpha: false }),
viewport: renderViewport
};
await page.render(renderContext).promise;
});
@ -285,10 +290,11 @@ document.addEventListener('DOMContentLoaded', async () => {
// maxHeight: 1200,
showCover: true,
maxShadowOpacity: 0.5,
mobileScrollSupport: false // Disable mobile scroll to prevent conflicts
mobileScrollSupport: false, // Disable mobile scroll to prevent conflicts
direction: bookDirection
});
pageFlip.loadFromHTML(document.querySelectorAll('.page'));
pageFlip.loadFromHTML(container.querySelectorAll('.page'));
// Enable toolbar
toolbar.classList.remove('opacity-50', 'pe-none');
@ -327,4 +333,4 @@ document.addEventListener('DOMContentLoaded', async () => {
<?php endif; ?>
<?php
library_render_footer();
library_render_footer();

View File

@ -5,6 +5,10 @@ require_once __DIR__ . '/library.php';
function admin_render_header(string $title, string $activePage = 'dashboard'): void {
library_bootstrap();
$lang = library_get_language();
$dir = $lang === 'ar' ? 'rtl' : 'ltr';
$isRtl = $lang === 'ar';
// Get flashes and clear them
$flashes = [];
if (isset($_SESSION['library_flash'])) {
@ -13,12 +17,16 @@ function admin_render_header(string $title, string $activePage = 'dashboard'): v
}
?>
<!doctype html>
<html lang="en">
<html lang="<?= $lang ?>" dir="<?= $dir ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= h($title) ?> · Admin Studio</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<title><?= h($title) ?> · <?= library_trans('admin_panel') ?></title>
<?php if ($isRtl): ?>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.rtl.min.css" integrity="sha384-dpuaG1suU0eT09tx5plTaGMLBsfDLzUCCUXOY2j/LSvXYuG6Bqs43ALlhIqAJVRb" crossorigin="anonymous">
<?php else: ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<?php endif; ?>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<style>
.admin-sidebar {
@ -27,7 +35,7 @@ function admin_render_header(string $title, string $activePage = 'dashboard'): v
position: sticky;
top: 0;
background: #fff;
border-right: 1px solid #dee2e6;
border-<?= $isRtl ? 'left' : 'right' ?>: 1px solid #dee2e6;
padding: 20px;
}
.nav-link {
@ -47,29 +55,38 @@ function admin_render_header(string $title, string $activePage = 'dashboard'): v
<body class="bg-light">
<div class="d-flex">
<aside class="admin-sidebar d-flex flex-column">
<h4 class="mb-4 text-primary fw-bold"><i class="bi bi-grid-fill me-2"></i>Admin Panel</h4>
<div class="d-flex align-items-center justify-content-between mb-4">
<h4 class="mb-0 text-primary fw-bold"><i class="bi bi-grid-fill me-2"></i><?= library_trans('admin_panel') ?></h4>
</div>
<div class="mb-3">
<a href="?lang=<?= library_trans('switch_lang_code') ?>" class="btn btn-sm btn-outline-secondary w-100">
<i class="bi bi-translate me-2"></i><?= library_trans('switch_lang') ?>
</a>
</div>
<nav class="nav flex-column flex-grow-1">
<a class="nav-link rounded <?= $activePage === 'dashboard' ? 'active' : '' ?>" href="/admin.php">
<i class="bi bi-speedometer2 me-2"></i> Dashboard
<i class="bi bi-speedometer2 me-2"></i> <?= library_trans('dashboard') ?>
</a>
<a class="nav-link rounded <?= $activePage === 'documents' ? 'active' : '' ?>" href="/admin_documents.php">
<i class="bi bi-folder2-open me-2"></i> Material Entry
<i class="bi bi-folder2-open me-2"></i> <?= library_trans('material_entry') ?>
</a>
<div class="my-2 border-top"></div>
<a class="nav-link rounded <?= $activePage === 'categories' ? 'active' : '' ?>" href="/admin_categories.php">
<i class="bi bi-tags me-2"></i> Categories
<i class="bi bi-tags me-2"></i> <?= library_trans('categories') ?>
</a>
<a class="nav-link rounded <?= $activePage === 'subcategories' ? 'active' : '' ?>" href="/admin_subcategories.php">
<i class="bi bi-diagram-3 me-2"></i> Subcategories
<i class="bi bi-diagram-3 me-2"></i> <?= library_trans('subcategories') ?>
</a>
<a class="nav-link rounded <?= $activePage === 'types' ? 'active' : '' ?>" href="/admin_types.php">
<i class="bi bi-file-earmark-text me-2"></i> Types
<i class="bi bi-file-earmark-text me-2"></i> <?= library_trans('types') ?>
</a>
</nav>
<div class="mt-auto pt-3 border-top">
<a class="nav-link text-secondary rounded" href="/index.php">
<i class="bi bi-box-arrow-right me-2"></i> Return to Site
<i class="bi bi-box-arrow-right me-2"></i> <?= library_trans('return_to_site') ?>
</a>
</div>
</aside>
@ -110,10 +127,6 @@ function admin_render_footer(): void {
target.style.opacity = '0.7';
try {
// Simple mock translation for demo purposes or real API if configured
// In a real scenario, this would call /api/translate.php
// For now, let's assume /api/translate.php exists (it does)
const response = await fetch('/api/translate.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@ -141,4 +154,4 @@ function admin_render_footer(): void {
</html>
<?php
}
?>
?>

View File

@ -16,9 +16,21 @@ function library_render_header(string $pageTitle, string $pageDescription, strin
$projectImageUrl = $project['image'];
$fullTitle = $pageTitle . ' · ' . $project['name'];
$flashes = library_get_flashes();
// Language Logic
$lang = library_get_language();
$isRtl = $lang === 'ar';
$targetLang = $isRtl ? 'en' : 'ar';
$switchLabel = library_trans('switch_lang', $lang);
// Build Switch URL
$params = $_GET;
$params['lang'] = $targetLang;
$switchUrl = '?' . http_build_query($params);
?>
<!doctype html>
<html lang="en">
<html lang="<?= h($lang) ?>" dir="<?= $isRtl ? 'rtl' : 'ltr' ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@ -32,7 +44,16 @@ function library_render_header(string $pageTitle, string $pageDescription, strin
<meta property="og:image" content="<?= h($projectImageUrl) ?>">
<meta property="twitter:image" content="<?= h($projectImageUrl) ?>">
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<?php if ($isRtl): ?>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.rtl.min.css" integrity="sha384-dpuaG1suU0eT09tx5plTaGMLBsfDLzUCCUXOY2j/LSvXYuG6Bqs43ALlhIqAJVRb" crossorigin="anonymous">
<?php else: ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Noto+Sans+Arabic:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/assets/css/custom.css?v=<?= time() ?>">
</head>
<body>
@ -42,8 +63,8 @@ function library_render_header(string $pageTitle, string $pageDescription, strin
<a class="navbar-brand d-flex align-items-center gap-2" href="/index.php">
<span class="brand-mark">NL</span>
<span>
<span class="d-block brand-title">Nabd Library</span>
<small class="text-secondary">Arabic · English e-library</small>
<span class="d-block brand-title"><?= library_trans('nabd_library') ?></span>
<small class="text-secondary"><?= library_trans('tagline') ?></small>
</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#libraryNav" aria-controls="libraryNav" aria-expanded="false" aria-label="Toggle navigation">
@ -51,8 +72,16 @@ function library_render_header(string $pageTitle, string $pageDescription, strin
</button>
<div class="collapse navbar-collapse" id="libraryNav">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2">
<li class="nav-item"><a class="nav-link <?= library_active_nav($activeNav, 'catalog') ?>" href="/index.php">Catalog</a></li>
<li class="nav-item"><a class="nav-link <?= library_active_nav($activeNav, 'admin') ?>" href="/admin.php">Admin Studio</a></li>
<li class="nav-item"><a class="nav-link <?= library_active_nav($activeNav, 'catalog') ?>" href="/index.php"><?= library_trans('catalog') ?></a></li>
<li class="nav-item"><a class="nav-link <?= library_active_nav($activeNav, 'admin') ?>" href="/admin.php"><?= library_trans('admin_studio') ?></a></li>
<li class="nav-item border-start border-secondary mx-2 d-none d-lg-block" style="opacity: 0.3; height: 24px;"></li>
<li class="nav-item">
<a class="nav-link fw-bold text-primary" href="<?= h($switchUrl) ?>">
<?= h($switchLabel) ?>
</a>
</li>
</ul>
</div>
</div>
@ -85,12 +114,12 @@ function library_render_footer(): void
<footer class="border-top border-subtle bg-white">
<div class="container py-4 d-flex flex-column flex-lg-row justify-content-between gap-3 small text-secondary">
<div>
<div class="fw-semibold text-dark mb-1">Bilingual reader MVP</div>
<div>Upload documents, publish public/private titles, read online, and request AI summaries.</div>
<div class="fw-semibold text-dark mb-1"><?= library_trans('mvp_label') ?></div>
<div><?= library_trans('upload_docs') ?></div>
</div>
<div class="text-lg-end">
<div><a class="text-decoration-none" href="/admin.php">Open Admin Studio</a></div>
<div><a class="text-decoration-none" href="/index.php">Browse public catalog</a></div>
<div><a class="text-decoration-none" href="/admin.php"><?= library_trans('open_admin') ?></a></div>
<div><a class="text-decoration-none" href="/index.php"><?= library_trans('browse_public') ?></a></div>
</div>
</div>
</footer>
@ -100,4 +129,4 @@ function library_render_footer(): void
</body>
</html>
<?php
}
}

View File

@ -9,6 +9,214 @@ if (session_status() !== PHP_SESSION_ACTIVE) {
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../ai/LocalAIApi.php';
function library_get_language(): string
{
// Check GET
if (isset($_GET['lang']) && in_array($_GET['lang'], ['en', 'ar'])) {
$_SESSION['lang'] = $_GET['lang'];
}
// Check Session or Default
return $_SESSION['lang'] ?? 'en';
}
function library_is_arabic_text(?string $text): bool
{
if ($text === null) {
return false;
}
return preg_match('/\p{Arabic}/u', $text) === 1;
}
function library_text_lang(?string $text, ?string $fallback = null): string
{
if (library_is_arabic_text($text)) {
return 'ar';
}
return $fallback ?? 'en';
}
function library_text_dir(?string $text, ?string $fallback = null): string
{
return library_text_lang($text, $fallback) === 'ar' ? 'rtl' : 'ltr';
}
function library_trans(string $key, ?string $lang = null): string
{
$lang = $lang ?? library_get_language();
$translations = [
'en' => [
'catalog' => 'Catalog',
'admin_studio' => 'Admin Studio',
'nabd_library' => 'Nabd Library',
'tagline' => 'Arabic · English e-library',
'search' => 'Search...',
'switch_lang' => 'العربية',
'switch_lang_code' => 'ar',
'browse_public' => 'Browse public catalog',
'open_admin' => 'Open Admin Studio',
'upload_docs' => 'Upload documents, publish public/private titles, read online, and request AI summaries.',
'mvp_label' => 'Bilingual reader MVP',
'admin_panel' => 'Admin Panel',
'dashboard' => 'Dashboard',
'material_entry' => 'Material Entry',
'categories' => 'Categories',
'subcategories' => 'Subcategories',
'types' => 'Types',
'return_to_site' => 'Return to Site',
'total_documents' => 'Total Documents',
'public_titles' => 'Public Titles',
'total_downloads' => 'Total Downloads',
'recent_documents' => 'Recent Documents',
'title_author' => 'Title / Author',
'type_category' => 'Type / Category',
'visibility' => 'Visibility',
'actions' => 'Actions',
'view' => 'View',
'showing_last_10' => 'Showing last 10 uploads',
'manage_documents' => 'Manage Documents',
'manage_documents_desc' => 'Manage your library documents, upload new materials, and edit metadata including bilingual titles, summaries, and more.',
'create_new' => 'Create New',
'edit' => 'Edit',
'delete' => 'Delete',
'save' => 'Save',
'cancel' => 'Cancel',
'name_en' => 'English Name',
'name_ar' => 'Arabic Name',
'description_en' => 'English Description',
'description_ar' => 'Arabic Description',
'confirm_delete' => 'Are you sure you want to delete this item?',
'manage_categories_desc' => 'Manage document categories.',
'add_new_category' => 'Add New Category',
'search_placeholder' => 'Search...',
'clear' => 'Clear',
'name' => 'Name',
'no_categories_found' => 'No categories found.',
'add_new_document' => 'Add New Material',
'search_docs_placeholder' => 'Search by title or author...',
'id' => 'ID',
'cover' => 'Cover',
'year' => 'Year',
'publisher' => 'Publisher',
'country' => 'Country',
'page_count' => 'Total Pages',
'summary_en' => 'Summary (English)',
'summary_ar' => 'Summary (Arabic)',
'cover_image' => 'Front Cover Image',
'document_file' => 'Document File',
'visibility_permissions' => 'Visibility & Permissions',
'download' => 'Download',
'print' => 'Print',
'copy' => 'Copy',
'author' => 'Author',
'select_type' => 'Select Type...',
'select_category' => 'Select Category...',
'select_subcategory' => 'Select Subcategory...',
'required_new_docs' => 'Required for new documents.',
'leave_empty_keep' => 'Leave empty to keep existing file.',
'save_changes' => 'Save Changes',
'manage_subcategories_desc' => 'Manage document subcategories.',
'add_new_subcategory' => 'Add New Subcategory',
'no_subcategories_found' => 'No subcategories found.',
'category' => 'Category',
'manage_types_desc' => 'Manage document types.',
'add_new_type' => 'Add New Type',
'no_types_found' => 'No types found.',
'previous' => 'Previous',
'next' => 'Next',
'showing_page' => 'Showing page',
'of' => 'of'
],
'ar' => [
'catalog' => 'الفهرس',
'admin_studio' => 'استوديو الإدارة',
'nabd_library' => 'مكتبة نبض',
'tagline' => 'مكتبة إلكترونية عربية · إنجليزية',
'search' => 'بحث...',
'switch_lang' => 'English',
'switch_lang_code' => 'en',
'browse_public' => 'تصفح الفهرس العام',
'open_admin' => 'فتح استوديو الإدارة',
'upload_docs' => 'ارفع المستندات، انشر العناوين العامة/الخاصة، اقرأ عبر الإنترنت، واطلب ملخصات الذكاء الاصطناعي.',
'mvp_label' => 'نموذج القارئ ثنائي اللغة',
'admin_panel' => 'لوحة التحكم',
'dashboard' => 'لوحة القيادة',
'material_entry' => 'إدخال المواد',
'categories' => 'الفئات',
'subcategories' => 'الفئات الفرعية',
'types' => 'الأنواع',
'return_to_site' => 'العودة للموقع',
'total_documents' => 'إجمالي المستندات',
'public_titles' => 'العناوين العامة',
'total_downloads' => 'إجمالي التنزيلات',
'recent_documents' => 'أحدث المستندات',
'title_author' => 'العنوان / المؤلف',
'type_category' => 'النوع / الفئة',
'visibility' => 'الظهور',
'actions' => 'الإجراءات',
'view' => 'عرض',
'showing_last_10' => 'عرض آخر 10 ملفات',
'manage_documents' => 'إدارة المستندات',
'manage_documents_desc' => 'إدارة مستندات المكتبة، رفع مواد جديدة، وتعديل البيانات الوصفية بما في ذلك العناوين والملخصات ثنائية اللغة.',
'create_new' => 'إنشاء جديد',
'edit' => 'تعديل',
'delete' => 'حذف',
'save' => 'حفظ',
'cancel' => 'إلغاء',
'name_en' => 'الاسم (إنجليزي)',
'name_ar' => 'الاسم (عربي)',
'description_en' => 'الوصف (إنجليزي)',
'description_ar' => 'الوصف (عربي)',
'confirm_delete' => 'هل أنت متأكد من حذف هذا العنصر؟',
'manage_categories_desc' => 'إدارة فئات المستندات.',
'add_new_category' => 'إضافة فئة جديدة',
'search_placeholder' => 'بحث...',
'clear' => 'مسح',
'name' => 'الاسم',
'no_categories_found' => 'لم يتم العثور على فئات.',
'add_new_document' => 'إضافة مادة جديدة',
'search_docs_placeholder' => 'بحث بالعنوان أو المؤلف...',
'id' => 'المعرف',
'cover' => 'الغلاف',
'year' => 'السنة',
'publisher' => 'الناشر',
'country' => 'الدولة',
'page_count' => 'عدد الصفحات',
'summary_en' => 'الملخص (إنجليزي)',
'summary_ar' => 'الملخص (عربي)',
'cover_image' => 'صورة الغلاف الأمامي',
'document_file' => 'ملف المستند',
'visibility_permissions' => 'الظهور والصلاحيات',
'download' => 'تنزيل',
'print' => 'طباعة',
'copy' => 'نسخ',
'author' => 'المؤلف',
'select_type' => 'اختر النوع...',
'select_category' => 'اختر الفئة...',
'select_subcategory' => 'اختر الفئة الفرعية...',
'required_new_docs' => 'مطلوب للمستندات الجديدة.',
'leave_empty_keep' => 'اتركه فارغاً للاحتفاظ بالملف الحالي.',
'save_changes' => 'حفظ التغييرات',
'manage_subcategories_desc' => 'إدارة الفئات الفرعية للمستندات.',
'add_new_subcategory' => 'إضافة فئة فرعية جديدة',
'no_subcategories_found' => 'لم يتم العثور على فئات فرعية.',
'category' => 'الفئة',
'manage_types_desc' => 'إدارة أنواع المستندات.',
'add_new_type' => 'إضافة نوع جديد',
'no_types_found' => 'لم يتم العثور على أنواع.',
'previous' => 'السابق',
'next' => 'التالي',
'showing_page' => 'عرض صفحة',
'of' => 'من'
]
];
return $translations[$lang][$key] ?? $key;
}
function library_bootstrap(): void
{
static $booted = false;
@ -203,26 +411,60 @@ function library_generate_summary(int $id): array
return ['success' => false, 'message' => 'Document not found.'];
}
$descEn = trim((string) ($doc['description_en'] ?? ''));
$descAr = trim((string) ($doc['description_ar'] ?? ''));
// 1. Try to extract text from the file
$text = '';
$filePath = __DIR__ . '/../' . ($doc['file_path'] ?? '');
if (file_exists($filePath)) {
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
if ($ext === 'pdf') {
// pdftotext is available via poppler-utils
// -layout preserves structure
// -f 1 -l 20 limits to first 20 pages (usually sufficient for summary)
$cmd = sprintf("pdftotext -layout -f 1 -l 20 %s -", escapeshellarg($filePath));
$text = shell_exec($cmd);
} elseif (in_array($ext, ['txt', 'md', 'csv', 'json'])) {
$text = file_get_contents($filePath);
}
}
$text = trim((string)$text);
if ($descEn === '' && $descAr === '') {
// Fallback to title if description is missing
$descEn = trim((string) ($doc['title_en'] ?? ''));
$descAr = trim((string) ($doc['title_ar'] ?? ''));
// 2. Fallback to description/title if extraction failed or empty
if ($text === '') {
$descEn = trim((string) ($doc['description_en'] ?? ''));
$descAr = trim((string) ($doc['description_ar'] ?? ''));
$titleEn = trim((string) ($doc['title_en'] ?? ''));
$titleAr = trim((string) ($doc['title_ar'] ?? ''));
$text = "Title (EN): $titleEn\nTitle (AR): $titleAr\n\nDescription (EN): $descEn\nDescription (AR): $descAr";
}
$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'.";
// 3. Truncate text to avoid token limits (approx 20k chars)
if (strlen($text) > 20000) {
$text = substr($text, 0, 20000) . "\n... [truncated]";
}
// Call AI
// Use HEREDOC for cleaner string with quotes
$prompt = <<<PROMPT
You are a bilingual summarization assistant.
Please summarize the following document content into:
1. A concise paragraph in English (summary_en).
2. A concise paragraph in Arabic (summary_ar).
Document Content:
"""
$text
"""
Return ONLY valid JSON with keys 'summary_en' and 'summary_ar'. Do not include markdown formatting.
PROMPT;
// 4. Call AI (using 'input' key for LocalAIApi compatibility)
$resp = LocalAIApi::createResponse([
'model' => 'gpt-5-mini',
'messages' => [
['role' => 'system', 'content' => 'You are a helpful bilingual assistant.'],
'input' => [
['role' => 'system', 'content' => 'You are a helpful bilingual assistant that outputs JSON.'],
['role' => 'user', 'content' => $prompt],
]
]);
@ -231,76 +473,123 @@ function library_generate_summary(int $id): array
return ['success' => false, 'message' => 'AI request failed: ' . ($resp['error'] ?? 'Unknown error')];
}
$text = LocalAIApi::extractText($resp);
$outputText = LocalAIApi::extractText($resp);
// 5. Parse JSON
$sumEn = '';
$sumAr = '';
// Try to parse JSON
$jsonStart = strpos($text, '{');
$jsonEnd = strrpos($text, '}');
// Attempt to parse JSON from response
$jsonStart = strpos($outputText, '{');
$jsonEnd = strrpos($outputText, '}');
if ($jsonStart !== false && $jsonEnd !== false) {
$jsonStr = substr($text, $jsonStart, $jsonEnd - $jsonStart + 1);
$jsonStr = substr($outputText, $jsonStart, $jsonEnd - $jsonStart + 1);
$data = json_decode($jsonStr, true);
$sumEn = $data['summary_en'] ?? '';
$sumAr = $data['summary_ar'] ?? '';
if (is_array($data)) {
$sumEn = $data['summary_en'] ?? '';
$sumAr = $data['summary_ar'] ?? '';
}
}
// If JSON parsing failed or returned empty strings, try to infer or use raw text
// Fallback if JSON parsing failed
if (!$sumEn && !$sumAr) {
// If the AI just returned text, use it for English (or both)
$sumEn = $text;
$sumEn = $outputText; // Just dump the text
$sumAr = '';
}
// Update DB
// 6. 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.'];
return ['success' => true, 'message' => 'Summary generated successfully from document content.'];
}
// --- Category Functions ---
function library_get_categories(string $search = ''): array
{
// Backward compatibility
$result = library_get_categories_paginated($search);
return $result['data'];
}
function library_get_categories_paginated(string $search = '', int $limit = 0, int $offset = 0): array
{
library_bootstrap();
$sql = 'SELECT * FROM library_categories';
$sql = 'SELECT * FROM library_categories WHERE 1=1';
$countSql = 'SELECT COUNT(*) FROM library_categories WHERE 1=1';
$params = [];
if ($search !== '') {
$sql .= ' WHERE name_en LIKE ? OR name_ar LIKE ?';
$clause = ' AND (name_en LIKE ? OR name_ar LIKE ?)';
$sql .= $clause;
$countSql .= $clause;
$params[] = "%$search%";
$params[] = "%$search%";
}
$sql .= ' ORDER BY name_en ASC';
if ($limit > 0) {
$sql .= ' LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset;
}
$stmt = db()->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll() ?: [];
$data = $stmt->fetchAll() ?: [];
$stmtCount = db()->prepare($countSql);
$stmtCount->execute($params);
$total = (int)$stmtCount->fetchColumn();
return ['data' => $data, 'total' => $total];
}
function library_get_subcategories(?int $categoryId = null, string $search = ''): array
{
// Backward compatibility
$result = library_get_subcategories_paginated($categoryId, $search);
return $result['data'];
}
function library_get_subcategories_paginated(?int $categoryId = null, string $search = '', int $limit = 0, int $offset = 0): array
{
library_bootstrap();
$sql = 'SELECT * FROM library_subcategories WHERE 1=1';
$countSql = 'SELECT COUNT(*) FROM library_subcategories WHERE 1=1';
$params = [];
if ($categoryId !== null) {
$sql .= ' AND category_id = ?';
$clause = ' AND category_id = ?';
$sql .= $clause;
$countSql .= $clause;
$params[] = $categoryId;
}
if ($search !== '') {
$sql .= ' AND (name_en LIKE ? OR name_ar LIKE ?)';
$clause = ' AND (name_en LIKE ? OR name_ar LIKE ?)';
$sql .= $clause;
$countSql .= $clause;
$params[] = "%$search%";
$params[] = "%$search%";
}
$sql .= ' ORDER BY name_en ASC';
if ($limit > 0) {
$sql .= ' LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset;
}
$stmt = db()->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll() ?: [];
$data = $stmt->fetchAll() ?: [];
$stmtCount = db()->prepare($countSql);
$stmtCount->execute($params);
$total = (int)$stmtCount->fetchColumn();
return ['data' => $data, 'total' => $total];
}
function library_create_category(string $nameEn, string $nameAr): int
@ -368,19 +657,41 @@ function library_delete_subcategory(int $id): void
// --- Type Functions ---
function library_get_types(string $search = ''): array
{
$result = library_get_types_paginated($search);
return $result['data'];
}
function library_get_types_paginated(string $search = '', int $limit = 0, int $offset = 0): array
{
library_bootstrap();
$sql = 'SELECT * FROM library_types';
$sql = 'SELECT * FROM library_types WHERE 1=1';
$countSql = 'SELECT COUNT(*) FROM library_types WHERE 1=1';
$params = [];
if ($search !== '') {
$sql .= ' WHERE name_en LIKE ? OR name_ar LIKE ?';
$clause = ' AND (name_en LIKE ? OR name_ar LIKE ?)';
$sql .= $clause;
$countSql .= $clause;
$params[] = "%$search%";
$params[] = "%$search%";
}
$sql .= ' ORDER BY name_en ASC';
if ($limit > 0) {
$sql .= ' LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset;
}
$stmt = db()->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll() ?: [];
$data = $stmt->fetchAll() ?: [];
$stmtCount = db()->prepare($countSql);
$stmtCount->execute($params);
$total = (int)$stmtCount->fetchColumn();
return ['data' => $data, 'total' => $total];
}
function library_create_type(string $nameEn, string $nameAr): int
@ -416,6 +727,13 @@ function library_delete_type(int $id): void
// --- End Type Functions ---
function library_fetch_documents(bool $publicOnly = false, array $filters = []): array
{
// Backward compatibility
$result = library_fetch_documents_paginated($publicOnly, $filters);
return $result['data'];
}
function library_fetch_documents_paginated(bool $publicOnly = false, array $filters = [], int $limit = 0, int $offset = 0): array
{
library_bootstrap();
@ -428,21 +746,52 @@ function library_fetch_documents(bool $publicOnly = false, array $filters = []):
LEFT JOIN library_subcategories sc ON d.subcategory_id = sc.id
LEFT JOIN library_types t ON d.type_id = t.id
WHERE 1=1';
$countSql = 'SELECT COUNT(*) FROM library_documents d
LEFT JOIN library_categories c ON d.category_id = c.id
LEFT JOIN library_subcategories sc ON d.subcategory_id = sc.id
LEFT JOIN library_types t ON d.type_id = t.id
WHERE 1=1';
$params = [];
if ($publicOnly) {
$sql .= ' AND d.visibility = :visibility';
$clause = ' AND d.visibility = :visibility';
$sql .= $clause;
$countSql .= $clause;
$params[':visibility'] = 'public';
}
// Apply Search Filters
if (!empty($filters['q'])) {
$q = trim($filters['q']);
$clause = ' AND (d.title_en LIKE :q OR d.title_ar LIKE :q OR d.author LIKE :q OR d.description_en LIKE :q OR d.description_ar LIKE :q OR d.summary_en LIKE :q)';
$sql .= $clause;
$countSql .= $clause;
$params[':q'] = "%$q%";
}
$sql .= ' ORDER BY d.is_featured DESC, d.created_at DESC';
if ($limit > 0) {
$sql .= ' LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset;
}
$stmt = db()->prepare($sql);
foreach ($params as $key => $value) {
$stmt->bindValue($key, $value);
}
$stmt->execute();
$data = $stmt->fetchAll() ?: [];
$stmtCount = db()->prepare($countSql);
foreach ($params as $key => $value) {
$stmtCount->bindValue($key, $value);
}
$stmtCount->execute();
$total = (int)$stmtCount->fetchColumn();
return $stmt->fetchAll() ?: [];
return ['data' => $data, 'total' => $total];
}
function library_fetch_document(int $id, bool $publicOnly = false): ?array
@ -623,7 +972,7 @@ function library_create_document(array $payload, array $file, array $coverFile =
$descriptionEn = trim((string) ($payload['description_en'] ?? ''));
$descriptionAr = trim((string) ($payload['description_ar'] ?? ''));
$fileData = library_handle_uploaded_file($file);
$fileData = library_handle_uploaded_file(file: $file);
$coverPath = library_handle_cover_image($coverFile);
$stmt = db()->prepare('INSERT INTO library_documents (
@ -636,7 +985,7 @@ function library_create_document(array $payload, array $file, array $coverFile =
description_en, description_ar
) VALUES (
:title_en, :title_ar,
:category, :category_ar, :sub_category, :sub_category_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,
@ -805,3 +1154,59 @@ function library_delete_document(int $id): void
$stmt = db()->prepare('DELETE FROM library_documents WHERE id = ?');
$stmt->execute([$id]);
}
function library_render_pagination(int $currentPage, int $totalPages, string $baseUrl): void
{
if ($totalPages <= 1) {
return;
}
$queryParts = parse_url($baseUrl, PHP_URL_QUERY);
parse_str($queryParts ?? '', $queryParams);
$buildUrl = function(int $page) use ($baseUrl, $queryParams) {
$queryParams['page'] = $page;
$path = parse_url($baseUrl, PHP_URL_PATH);
return $path . '?' . http_build_query($queryParams);
};
echo '<nav aria-label="Page navigation" class="mt-4">';
echo '<ul class="pagination justify-content-center">';
// Previous
$prevClass = $currentPage <= 1 ? 'disabled' : '';
echo '<li class="page-item ' . $prevClass . '">';
echo '<a class="page-link" href="' . h($buildUrl($currentPage - 1)) . '">' . library_trans('previous') . '</a>';
echo '</li>';
// Numbers
$start = max(1, $currentPage - 2);
$end = min($totalPages, $currentPage + 2);
if ($start > 1) {
echo '<li class="page-item"><a class="page-link" href="' . h($buildUrl(1)) . '">1</a></li>';
if ($start > 2) echo '<li class="page-item disabled"><span class="page-link">...</span></li>';
}
for ($i = $start; $i <= $end; $i++) {
$active = $i === $currentPage ? 'active' : '';
echo '<li class="page-item ' . $active . '">';
echo '<a class="page-link" href="' . h($buildUrl($i)) . '">' . $i . '</a>';
echo '</li>';
}
if ($end < $totalPages) {
if ($end < $totalPages - 1) echo '<li class="page-item disabled"><span class="page-link">...</span></li>';
echo '<li class="page-item"><a class="page-link" href="' . h($buildUrl($totalPages)) . '">' . $totalPages . '</a></li>';
}
// Next
$nextClass = $currentPage >= $totalPages ? 'disabled' : '';
echo '<li class="page-item ' . $nextClass . '">';
echo '<a class="page-link" href="' . h($buildUrl($currentPage + 1)) . '">' . library_trans('next') . '</a>';
echo '</li>';
echo '</ul>';
echo '<div class="text-center text-secondary small mt-2">' . library_trans('showing_page') . ' ' . $currentPage . ' ' . library_trans('of') . ' ' . $totalPages . '</div>';
echo '</nav>';
}

View File

@ -8,7 +8,17 @@ library_bootstrap();
$query = trim((string) ($_GET['q'] ?? ''));
$language = trim((string) ($_GET['language'] ?? ''));
$documents = library_fetch_documents(true, ['q' => $query, 'language' => $language]);
// Pagination Logic
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$limit = 12; // Grid layout 3x4
$offset = ($page - 1) * $limit;
$result = library_fetch_documents_paginated(true, ['q' => $query, 'language' => $language], $limit, $offset);
$documents = $result['data'];
$totalDocuments = $result['total'];
$totalPages = (int)ceil($totalDocuments / $limit);
$metrics = library_catalog_metrics();
$recentDocuments = library_recent_documents(3, true);
@ -103,7 +113,7 @@ library_render_header(
<div class="section-kicker">Catalog</div>
<h2 class="h3 mb-0">Available public titles</h2>
</div>
<span class="text-secondary small"><?= h((string) count($documents)) ?> result<?= count($documents) === 1 ? '' : 's' ?></span>
<span class="text-secondary small"><?= h((string) $totalDocuments) ?> result<?= $totalDocuments === 1 ? '' : 's' ?></span>
</div>
<?php if (!$documents): ?>
@ -116,6 +126,14 @@ library_render_header(
<?php else: ?>
<div class="row g-4">
<?php foreach ($documents as $document): ?>
<?php
$cardSummary = (string) ($document['summary_text'] ?: ($document['description_en'] ?: $document['description_ar'] ?: 'No summary yet.'));
$cardSummaryLang = library_text_lang(
$cardSummary,
!empty($document['description_ar']) && empty($document['summary_text']) && empty($document['description_en']) ? 'ar' : 'en'
);
$cardSummaryDir = library_text_dir($cardSummary, $cardSummaryLang);
?>
<div class="col-md-6 col-xl-4">
<article class="panel h-100 d-flex flex-column">
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
@ -132,7 +150,7 @@ library_render_header(
<h3 class="h5 mb-1"><?= h((string) $document['title_en']) ?></h3>
<?php endif; ?>
<?php if (!empty($document['title_ar'])): ?>
<div class="text-secondary" dir="rtl"><?= h((string) $document['title_ar']) ?></div>
<div class="text-secondary" dir="rtl" lang="ar"><?= h((string) $document['title_ar']) ?></div>
<?php endif; ?>
</div>
<dl class="row small text-secondary mb-3 gx-2 gy-1">
@ -143,7 +161,7 @@ library_render_header(
<dt class="col-4">Tags</dt>
<dd class="col-8 mb-0"><?= h((string) ($document['tags'] ?: '—')) ?></dd>
</dl>
<p class="text-secondary flex-grow-1"><?= h((string) ($document['summary_text'] ?: ($document['description_en'] ?: $document['description_ar'] ?: 'No summary yet.'))) ?></p>
<p class="text-secondary flex-grow-1" lang="<?= h($cardSummaryLang) ?>" dir="<?= h($cardSummaryDir) ?>"><?= h($cardSummary) ?></p>
<div class="d-flex gap-2 mt-3">
<a class="btn btn-dark btn-sm" href="/document.php?id=<?= h((string) $document['id']) ?>">Open reader</a>
<a class="btn btn-outline-secondary btn-sm" href="/document.php?id=<?= h((string) $document['id']) ?>#summary-card">View summary</a>
@ -152,6 +170,14 @@ library_render_header(
</div>
<?php endforeach; ?>
</div>
<!-- Pagination -->
<?php if ($totalPages > 1): ?>
<div class="mt-5">
<?php library_render_pagination($page, $totalPages, '/index.php'); ?>
</div>
<?php endif; ?>
<?php endif; ?>
</section>
@ -178,10 +204,15 @@ library_render_header(
</div>
<div class="row g-3">
<?php foreach ($recentDocuments as $document): ?>
<?php
$recentTitle = (string) ($document['title_en'] ?: $document['title_ar'] ?: 'Untitled');
$recentTitleLang = library_text_lang($recentTitle, !empty($document['title_ar']) && empty($document['title_en']) ? 'ar' : 'en');
$recentTitleDir = library_text_dir($recentTitle, $recentTitleLang);
?>
<div class="col-md-4">
<a class="recent-card text-decoration-none" href="/document.php?id=<?= h((string) $document['id']) ?>">
<span class="small text-secondary d-block mb-2"><?= h(library_language_label((string) $document['document_language'])) ?></span>
<strong class="d-block text-dark mb-1"><?= h((string) ($document['title_en'] ?: $document['title_ar'] ?: 'Untitled')) ?></strong>
<strong class="d-block text-dark mb-1" lang="<?= h($recentTitleLang) ?>" dir="<?= h($recentTitleDir) ?>"><?= h($recentTitle) ?></strong>
<span class="small text-secondary"><?= h((string) ($document['author'] ?: 'Unknown author')) ?></span>
</a>
</div>
@ -191,4 +222,4 @@ library_render_header(
</div>
</section>
<?php
library_render_footer();
library_render_footer();

View File

@ -18,20 +18,91 @@ if (!$document || empty($document['file_path'])) {
}
$fileUrl = library_file_url((string) $document['file_path']);
// --- Localization ---
// Use global helper to respect session/get
$lang = library_get_language();
$isRtl = $lang === 'ar';
$trans = [
'en' => [
'title_suffix' => 'Reader',
'default_title' => 'Document Viewer',
'back_tooltip' => 'Back to Details',
'search_placeholder' => 'Search...',
'prev_match' => 'Previous Match',
'next_match' => 'Next Match',
'read_aloud' => 'Read Aloud (Play/Stop)',
'prev_page' => 'Previous Page',
'next_page' => 'Next Page',
'download' => 'Download PDF',
'loading' => 'Loading Document...',
'downloading' => 'Downloading...',
'rendering' => 'Rendering pages...',
'error_prefix' => 'Error: ',
'searching' => 'Searching...',
'no_matches' => 'No matches',
'matches_found' => 'matches',
'page_label' => 'Page',
'switch_lang' => 'العربية',
'switch_lang_code' => 'ar'
],
'ar' => [
'title_suffix' => 'القارئ',
'default_title' => 'عارض المستندات',
'back_tooltip' => 'العودة للتفاصيل',
'search_placeholder' => 'بحث...',
'prev_match' => 'المطابقة السابقة',
'next_match' => 'المطابقة التالية',
'read_aloud' => 'قراءة بصوت عالٍ (تشغيل/إيقاف)',
'prev_page' => 'الصفحة السابقة',
'next_page' => 'الصفحة التالية',
'download' => 'تحميل PDF',
'loading' => 'جاري تحميل المستند...',
'downloading' => 'جاري التنزيل...',
'rendering' => 'جاري عرض الصفحات...',
'error_prefix' => 'خطأ: ',
'searching' => 'جاري البحث...',
'no_matches' => 'لا يوجد نتائج',
'matches_found' => 'تطابقات',
'page_label' => 'صفحة',
'switch_lang' => 'English',
'switch_lang_code' => 'en'
]
];
$t = $trans[$lang];
// Determine Title
$docTitle = $document['title_' . $lang] ?: $document['title_en'] ?: $document['title_ar'] ?: $t['default_title'];
$docTitleLang = library_text_lang(
(string) $docTitle,
!empty($document['title_ar']) && empty($document['title_en']) ? 'ar' : $lang
);
$docTitleDir = library_text_dir((string) $docTitle, $docTitleLang);
?>
<!DOCTYPE html>
<html lang="en">
<html lang="<?= $lang ?>" dir="<?= $isRtl ? 'rtl' : 'ltr' ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= h($document['title_en'] ?: 'Document Viewer') ?> - Reader</title>
<title><?= h($docTitle) ?> - <?= $t['title_suffix'] ?></title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Noto+Sans+Arabic:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<!-- Bootstrap -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<?php if ($isRtl): ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.rtl.min.css" rel="stylesheet">
<?php else: ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<?php endif; ?>
<style>
body {
margin: 0;
font-family: Inter, "Noto Sans Arabic", "Segoe UI", Tahoma, sans-serif;
padding: 0;
background-color: #2d3035;
height: 100vh;
@ -92,6 +163,10 @@ $fileUrl = library_file_url((string) $document['file_path']);
padding: 5px 10px;
border-radius: 4px;
transition: color 0.2s, background-color 0.2s;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
}
.toolbar-btn:hover {
@ -99,6 +174,11 @@ $fileUrl = library_file_url((string) $document['file_path']);
background-color: rgba(255,255,255,0.1);
}
.toolbar-btn.active {
color: #ffc107; /* Active state color (yellow) */
background-color: rgba(255, 255, 255, 0.15);
}
.page-indicator {
font-family: monospace;
font-size: 1.1rem;
@ -108,6 +188,8 @@ $fileUrl = library_file_url((string) $document['file_path']);
.doc-title {
font-weight: 500;
unicode-bidi: plaintext;
text-align: start;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@ -139,9 +221,16 @@ $fileUrl = library_file_url((string) $document['file_path']);
color: #adb5bd;
}
[lang="ar"],
[dir="rtl"] {
font-family: "Noto Sans Arabic", "Segoe UI", Tahoma, sans-serif;
unicode-bidi: plaintext;
text-align: start;
}
.search-controls {
display: flex;
border-left: 1px solid #495057;
border-inline-start: 1px solid #495057; /* RTL safe border */
}
.search-btn {
@ -161,7 +250,7 @@ $fileUrl = library_file_url((string) $document['file_path']);
#search-status {
font-size: 0.8rem;
color: #adb5bd;
margin-left: 8px;
margin-inline-start: 8px; /* RTL safe margin */
white-space: nowrap;
}
</style>
@ -170,38 +259,54 @@ $fileUrl = library_file_url((string) $document['file_path']);
<div id="toolbar">
<div class="toolbar-group">
<a href="/document.php?id=<?= $documentId ?><?= $context === 'admin' ? '&context=admin' : '' ?>" class="toolbar-btn" title="Back to Details">
<i class="bi bi-arrow-left"></i>
<a href="/document.php?id=<?= $documentId ?><?= $context === 'admin' ? '&context=admin' : '' ?>" class="toolbar-btn" title="<?= h($t['back_tooltip']) ?>">
<i class="bi bi-arrow-left<?= $isRtl ? '-right' : '' /* flip arrow if needed? Actually left arrow usually points back in LTR, but in RTL 'Back' might be Right arrow? Let's check Bootstrap icons. bi-arrow-left points left. In RTL, back is usually Right. */ ?>"></i>
<!-- Note: In RTL interfaces, 'Back' usually implies moving to the 'parent' or 'previous' screen.
The arrow should point in the direction of flow.
Bootstrap 5 RTL mode mirrors some things, but icons often need manual flipping.
Let's use specific logic: if RTL, use arrow-right for 'Back'. -->
</a>
<div class="doc-title d-none d-lg-block"><?= h($document['title_en'] ?: 'Document') ?></div>
<div class="doc-title d-none d-lg-block" lang="<?= h($docTitleLang) ?>" dir="<?= h($docTitleDir) ?>"><?= h($docTitle) ?></div>
</div>
<!-- Center: Search -->
<div class="toolbar-group d-none d-md-flex">
<div class="search-container">
<input type="text" id="searchInput" placeholder="Search..." autocomplete="off">
<input type="text" id="searchInput" placeholder="<?= h($t['search_placeholder']) ?>" autocomplete="off">
<div class="search-controls">
<button id="btn-search-prev" class="search-btn" title="Previous Match"><i class="bi bi-chevron-up"></i></button>
<button id="btn-search-next" class="search-btn" title="Next Match"><i class="bi bi-chevron-down"></i></button>
<button id="btn-search-prev" class="search-btn" title="<?= h($t['prev_match']) ?>"><i class="bi bi-chevron-up"></i></button>
<button id="btn-search-next" class="search-btn" title="<?= h($t['next_match']) ?>"><i class="bi bi-chevron-down"></i></button>
</div>
</div>
<span id="search-status"></span>
</div>
<div class="toolbar-group">
<button id="btn-prev" class="toolbar-btn" title="Previous Page">
<button id="btn-read" class="toolbar-btn" title="<?= h($t['read_aloud']) ?>">
<i class="bi bi-megaphone"></i>
</button>
<div class="vr mx-2 bg-secondary" style="opacity: 0.5;"></div>
<!-- Lang Switcher -->
<a href="?id=<?= $documentId ?>&context=<?= $context ?>&lang=<?= $t['switch_lang_code'] ?>" class="toolbar-btn" style="font-size: 0.9rem; font-weight: bold;">
<?= $t['switch_lang'] ?>
</a>
<div class="vr mx-2 bg-secondary" style="opacity: 0.5;"></div>
<button id="btn-prev" class="toolbar-btn" title="<?= h($t['prev_page']) ?>">
<i class="bi bi-caret-left-fill"></i>
</button>
<div class="page-indicator">
<span id="page-current">1</span> <span class="text-secondary">/</span> <span id="page-total">--</span>
</div>
<button id="btn-next" class="toolbar-btn" title="Next Page">
<button id="btn-next" class="toolbar-btn" title="<?= h($t['next_page']) ?>">
<i class="bi bi-caret-right-fill"></i>
</button>
</div>
<div class="toolbar-group">
<a href="<?= h($fileUrl) ?>" download class="toolbar-btn" title="Download PDF">
<a href="<?= h($fileUrl) ?>" download class="toolbar-btn" title="<?= h($t['download']) ?>">
<i class="bi bi-download"></i>
</a>
</div>
@ -210,7 +315,7 @@ $fileUrl = library_file_url((string) $document['file_path']);
<div id="viewer-container">
<div id="flipbook-loader">
<div class="spinner-border text-light mb-3" role="status"></div>
<div>Loading Document...</div>
<div><?= h($t['loading']) ?></div>
<div id="loader-status" class="text-secondary small mt-2"></div>
</div>
<div id="flipbook"></div>
@ -220,6 +325,10 @@ $fileUrl = library_file_url((string) $document['file_path']);
<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>
// Translations from PHP
const TR = <?= json_encode($t) ?>;
const IS_RTL = <?= $isRtl ? 'true' : 'false' ?>;
// Global State
let pdfDoc = null;
let pageFlip = null;
@ -227,6 +336,11 @@ $fileUrl = library_file_url((string) $document['file_path']);
let currentMatchIndex = -1;
let pageHighlighters = {}; // Map of pageIndex -> CanvasContext (for highlights)
let pageViewports = {}; // Map of pageIndex -> viewport (for rect calc)
// Speech State
let isSpeaking = false;
let speechUtterance = null;
let shouldContinueReading = false;
document.addEventListener('DOMContentLoaded', async () => {
const pdfUrl = '<?= h($fileUrl) ?>';
@ -245,14 +359,14 @@ $fileUrl = library_file_url((string) $document['file_path']);
loadingTask.onProgress = (p) => {
if (p.total > 0) {
const percent = Math.round((p.loaded / p.total) * 100);
loaderStatus.textContent = `Downloading... ${percent}%`;
loaderStatus.textContent = `${TR.downloading} ${percent}%`;
}
};
pdfDoc = await loadingTask.promise;
const totalPages = pdfDoc.numPages;
totalSpan.textContent = totalPages;
loaderStatus.textContent = 'Rendering pages...';
loaderStatus.textContent = TR.rendering;
// --- Layout Logic ---
const availWidth = viewerContainer.clientWidth - 40;
@ -268,7 +382,9 @@ $fileUrl = library_file_url((string) $document['file_path']);
bookHeight = bookWidth / aspectRatio;
}
const renderScale = (bookHeight / viewport.height) * 1.5;
const deviceScale = Math.max(1, window.devicePixelRatio || 1);
const displayScale = (bookHeight / viewport.height);
const renderScale = Math.max(displayScale * deviceScale * 1.6, 2.2);
// --- Page Generation ---
const canvasPromises = [];
@ -310,23 +426,28 @@ $fileUrl = library_file_url((string) $document['file_path']);
// Render Async
canvasPromises.push(async () => {
const page = await pdfDoc.getPage(i);
const vp = page.getViewport({ scale: renderScale });
const renderViewport = page.getViewport({ scale: renderScale });
const displayViewport = page.getViewport({ scale: displayScale });
// Setup PDF Canvas
pdfCanvas.height = vp.height;
pdfCanvas.width = vp.width;
const ctx = pdfCanvas.getContext('2d');
pdfCanvas.height = Math.ceil(renderViewport.height);
pdfCanvas.width = Math.ceil(renderViewport.width);
pdfCanvas.style.width = `${displayViewport.width}px`;
pdfCanvas.style.height = `${displayViewport.height}px`;
const ctx = pdfCanvas.getContext('2d', { alpha: false });
// Setup Highlight Canvas (match dims)
hlCanvas.height = vp.height;
hlCanvas.width = vp.width;
// Setup Highlight Canvas (match display dims for search overlay)
hlCanvas.height = Math.ceil(displayViewport.height);
hlCanvas.width = Math.ceil(displayViewport.width);
hlCanvas.style.width = `${displayViewport.width}px`;
hlCanvas.style.height = `${displayViewport.height}px`;
const hlCtx = hlCanvas.getContext('2d');
// Store refs for search
pageHighlighters[i] = hlCtx;
pageViewports[i] = vp;
pageViewports[i] = displayViewport;
await page.render({ canvasContext: ctx, viewport: vp }).promise;
await page.render({ canvasContext: ctx, viewport: renderViewport }).promise;
});
}
@ -341,7 +462,8 @@ $fileUrl = library_file_url((string) $document['file_path']);
height: bookHeight,
size: 'fixed',
showCover: true,
maxShadowOpacity: 0.5
maxShadowOpacity: 0.5,
direction: IS_RTL ? 'rtl' : 'ltr'
});
pageFlip.loadFromHTML(document.querySelectorAll('.page'));
@ -349,16 +471,62 @@ $fileUrl = library_file_url((string) $document['file_path']);
// --- Events ---
pageFlip.on('flip', (e) => {
document.getElementById('page-current').textContent = (pageFlip.getCurrentPageIndex() + 1);
// Note: Manual flips are now handled by button/key listeners stopping speech.
// But if flip is triggered programmatically by advancePage(), we want to keep reading.
// The 'shouldContinueReading' flag handles the advancePage() recursion.
});
document.getElementById('btn-prev').addEventListener('click', () => pageFlip.flipPrev());
document.getElementById('btn-next').addEventListener('click', () => pageFlip.flipNext());
document.getElementById('btn-prev').addEventListener('click', () => {
if (isSpeaking) stopSpeech();
pageFlip.flipPrev();
});
document.getElementById('btn-next').addEventListener('click', () => {
if (isSpeaking) stopSpeech();
pageFlip.flipNext();
});
document.getElementById('btn-read').addEventListener('click', toggleRead);
// Keyboard
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT') return; // Don't flip when typing
if (e.key === 'ArrowLeft') pageFlip.flipPrev();
if (e.key === 'ArrowRight') pageFlip.flipNext();
// In RTL, left arrow should technically go next?
// Usually: Right Arrow = Next in LTR.
// In RTL Book: Left Arrow = Next (turn page to left).
// But PageFlip library might handle 'flipNext' logically.
// Let's test standard logical mapping first:
// Right Arrow -> Next Page (Logic)
// Left Arrow -> Prev Page (Logic)
if (e.key === 'ArrowLeft') {
if (isSpeaking) stopSpeech();
// If RTL, Left Arrow might mean "Next" physically?
// Let's stick to logical "Prev" and "Next" buttons which call flipPrev/flipNext.
// Ideally:
// LTR: Left = Prev, Right = Next
// RTL: Right = Prev, Left = Next
if (IS_RTL) {
pageFlip.flipNext(); // Left goes forward in RTL book (turning page from left to right? No, right to left.)
// RTL book: Pages 1, 2, 3...
// Page 1 is on Right.
// To go to Page 2, you drag Right page to Left.
// So "Left" direction is Next.
} else {
pageFlip.flipPrev();
}
}
if (e.key === 'ArrowRight') {
if (isSpeaking) stopSpeech();
if (IS_RTL) {
pageFlip.flipPrev(); // Right goes back
} else {
pageFlip.flipNext();
}
}
});
// --- Search Implementation ---
@ -366,10 +534,115 @@ $fileUrl = library_file_url((string) $document['file_path']);
} catch (error) {
console.error(error);
loader.innerHTML = '<div class="text-danger p-3">Error: ' + error.message + '</div>';
loader.innerHTML = '<div class="text-danger p-3">' + TR.error_prefix + error.message + '</div>';
}
});
// --- Speech Logic ---
function toggleRead() {
if (isSpeaking) {
stopSpeech();
} else {
startReading();
}
}
async function startReading() {
if (!pdfDoc) return;
const btn = document.getElementById('btn-read');
const icon = btn.querySelector('i');
// UI Update
isSpeaking = true;
shouldContinueReading = true; // Enable auto-flip
btn.classList.add('active');
icon.className = 'bi bi-stop-circle'; // Change to stop icon
await readCurrentPage();
}
function stopSpeech() {
window.speechSynthesis.cancel();
isSpeaking = false;
shouldContinueReading = false;
const btn = document.getElementById('btn-read');
const icon = btn.querySelector('i');
btn.classList.remove('active');
icon.className = 'bi bi-megaphone';
}
async function readCurrentPage() {
if (!isSpeaking) return;
// Get current PDF page index (0-based from PageFlip + 1 for PDF)
// PageFlip index 0 = PDF page 1
const pageIndex = pageFlip.getCurrentPageIndex();
const pdfPageIndex = pageIndex + 1;
if (pdfPageIndex > pdfDoc.numPages) {
stopSpeech();
return;
}
try {
const page = await pdfDoc.getPage(pdfPageIndex);
const textContent = await page.getTextContent();
// Simple text extraction: join items with space
const text = textContent.items.map(item => item.str).join(' ');
if (!text || text.trim().length === 0) {
// Empty page? Try next
if (shouldContinueReading) {
advancePage();
} else {
stopSpeech();
}
return;
}
speechUtterance = new SpeechSynthesisUtterance(text);
speechUtterance.rate = 1.0;
if (IS_RTL) {
speechUtterance.lang = 'ar-SA'; // Suggest Arabic voice if in Arabic mode
}
speechUtterance.onend = () => {
if (isSpeaking && shouldContinueReading) {
advancePage();
}
};
speechUtterance.onerror = (e) => {
console.error('Speech error:', e);
stopSpeech();
};
window.speechSynthesis.speak(speechUtterance);
} catch (e) {
console.error('Reading failed:', e);
stopSpeech();
}
}
function advancePage() {
// Flip to next page
// Logic: if current page < total - 1
const nextIndex = pageFlip.getCurrentPageIndex() + 1;
if (nextIndex < pdfDoc.numPages) {
pageFlip.flipNext();
setTimeout(() => {
if (isSpeaking) readCurrentPage();
}, 800); // 800ms for page turn animation approx
} else {
stopSpeech();
}
}
// --- Search Logic ---
function setupSearch() {
const input = document.getElementById('searchInput');
@ -421,7 +694,7 @@ $fileUrl = library_file_url((string) $document['file_path']);
return;
}
status.textContent = 'Searching...';
status.textContent = TR.searching;
searchResults = [];
currentMatchIndex = -1;
@ -433,25 +706,10 @@ $fileUrl = library_file_url((string) $document['file_path']);
const viewport = pageViewports[i]; // Use stored viewport
const ctx = pageHighlighters[i];
// Simple Item Search
// Note: This matches text *within* individual text items.
// Complex multi-item matches (spanning lines/chunks) are harder.
textContent.items.forEach(item => {
if (item.str.toLowerCase().includes(query.toLowerCase())) {
// Found a match!
// item.transform is [scaleX, skewY, skewX, scaleY, tx, ty]
// The PDF coordinate system origin is bottom-left (usually).
// Calculate approximate bounding box
// We need the width of the item. 'item.width' is available in recent pdf.js
// If not, we estimate.
const tx = item.transform;
// Basic rect in PDF coords:
// x = tx[4], y = tx[5]
// w = item.width, h = item.height (or font size?)
// Note: item.height is often 0 or undefined in raw items, use tx[3] (scaleY) as approx font height
let x = tx[4];
let y = tx[5];
let w = item.width;
@ -459,24 +717,9 @@ $fileUrl = library_file_url((string) $document['file_path']);
if (!w) w = h * item.str.length * 0.5; // fallback
// Adjust for y-flip?
// pdf.js viewport.convertToViewportRectangle handles the coordinate transform
// including the y-flip if the viewport is set up that way.
// Text items are bottom-left origin?
// We usually need to move y up by 'h' because rects are top-left?
// Actually, let's just transform (x, y) and (x+w, y+h)
// PDF text is usually baseline.
// So the rect starts at y (baseline) and goes up by h?
// Or starts at y-h?
// Let's assume y is baseline.
const rect = [x, y, x + w, y + h];
const viewRect = viewport.convertToViewportRectangle(rect);
// viewRect is [x1, y1, x2, y2]
// normalize
const rx = Math.min(viewRect[0], viewRect[2]);
const ry = Math.min(viewRect[1], viewRect[3]);
const rw = Math.abs(viewRect[0] - viewRect[2]);
@ -491,13 +734,9 @@ $fileUrl = library_file_url((string) $document['file_path']);
ctx.lineWidth = 1;
ctx.strokeRect(rx, ry, rw, rh);
// Add to results (one per page is enough for navigation, but we highlight all)
// We only push to searchResults if it's the *first* match on this page
// OR we push every match?
// Let's push every match for "Next" button granularity.
searchResults.push({
pageIndex: i - 1, // 0-based for PageFlip
label: `Page ${i}`
label: `${TR.page_label} ${i}`
});
}
});
@ -505,15 +744,15 @@ $fileUrl = library_file_url((string) $document['file_path']);
if (searchResults.length > 0) {
currentMatchIndex = 0;
status.textContent = `${searchResults.length} matches`;
status.textContent = `${searchResults.length} ${TR.matches_found}`;
showMatch(0);
} else {
status.textContent = 'No matches';
status.textContent = TR.no_matches;
}
} catch (e) {
console.error('Search failed', e);
status.textContent = 'Error';
status.textContent = TR.error_prefix + 'Search';
}
}
@ -522,15 +761,8 @@ $fileUrl = library_file_url((string) $document['file_path']);
const match = searchResults[index];
// Update status text
document.getElementById('search-status').textContent = `${index + 1} / ${searchResults.length}`;
document.getElementById('search-status').textContent = `${index + 1} / ${searchResults.length} ${TR.matches_found}`;
// Flip to page
// PageFlip 0-based index
// Check if we are already there to avoid animation loop?
// PageFlip handles it.
// Note: If view is 'spread', we might need to check if the page is visible.
// But flip() is safe.
pageFlip.flip(match.pageIndex);
}