Autosave: 20260409-170145
This commit is contained in:
parent
3e7ac25120
commit
721fc0af97
20
GEMINI_RESPONSE.md
Normal file
20
GEMINI_RESPONSE.md
Normal 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!
|
||||
30
admin.php
30
admin.php
@ -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(); ?>
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
58
document.php
58
document.php
@ -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();
|
||||
@ -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
|
||||
}
|
||||
?>
|
||||
?>
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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>';
|
||||
}
|
||||
|
||||
43
index.php
43
index.php
@ -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();
|
||||
392
viewer.php
392
viewer.php
@ -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);
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user