const analyticsRoot = document.querySelector('[data-sama-mag-analytics]');
const reader = document.querySelector('[data-sama-mag-reader]');
function sourceLabel() {
const params = new URLSearchParams(window.location.search);
const utm = (params.get('utm_source') || '').trim().toLowerCase();
if (utm) return utm.slice(0, 64);
try {
if (document.referrer) {
const host = new URL(document.referrer).hostname.replace(/^www\./, '').toLowerCase();
if (host && host !== window.location.hostname.replace(/^www\./, '').toLowerCase()) return host.slice(0, 64);
}
} catch (e) {}
return 'direct';
}
function endpointData(el) {
if (!el) return null;
const endpoint = el.dataset.endpoint || '';
const token = el.dataset.token || '';
if (!endpoint || !token) return null;
return { endpoint, token };
}
function recordEvent(event, overrideSource = '') {
const data = endpointData(analyticsRoot || reader);
if (!data) return;
const payload = JSON.stringify({ event, source: overrideSource || sourceLabel(), token: data.token });
if (navigator.sendBeacon) {
try {
const blob = new Blob([payload], { type: 'application/json' });
if (navigator.sendBeacon(data.endpoint, blob)) return;
} catch (e) {}
}
fetch(data.endpoint, {
method: 'POST',
credentials: 'same-origin',
keepalive: true,
headers: { 'Content-Type': 'application/json' },
body: payload,
}).catch(() => {});
}
if (analyticsRoot) window.setTimeout(() => recordEvent('page_view'), 900);
document.addEventListener('click', (event) => {
const action = event.target.closest('[data-mag-event]');
if (!action) return;
const metric = action.dataset.magEvent;
if (metric) recordEvent(metric, action.dataset.magSource || '');
});
const webShare = document.querySelector('[data-mag-web-share]');
if (webShare && navigator.share) {
webShare.hidden = false;
webShare.addEventListener('click', async () => {
try {
await navigator.share({
title: document.title,
text: webShare.dataset.shareText || document.title,
url: webShare.dataset.shareUrl || window.location.href,
});
recordEvent('share', 'web-share');
} catch (e) {}
});
}
const copyButton = document.querySelector('[data-mag-copy-url]');
if (copyButton) {
copyButton.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(copyButton.dataset.url || window.location.href);
const original = copyButton.textContent;
copyButton.textContent = copyButton.dataset.done || 'تم النسخ';
window.setTimeout(() => { copyButton.textContent = original; }, 1600);
recordEvent('share', 'copy');
} catch (e) {}
});
}
document.querySelectorAll('[data-mag-reader-reload]').forEach((button) => {
button.addEventListener('click', () => window.location.reload());
});
if (reader) {
const pdfUrl = reader.dataset.pdf || '';
const pdfjsUrl = reader.dataset.pdfjs || '';
const workerUrl = reader.dataset.worker || '';
const stage = reader.querySelector('[data-mag-stage]');
const pagesHost = reader.querySelector('[data-mag-pages]');
const pageInput = reader.querySelector('[data-mag-page-input]');
const totalEl = reader.querySelector('[data-mag-page-total]');
const prev = reader.querySelector('[data-mag-prev]');
const next = reader.querySelector('[data-mag-next]');
const zoomIn = reader.querySelector('[data-mag-zoom-in]');
const zoomOut = reader.querySelector('[data-mag-zoom-out]');
const fit = reader.querySelector('[data-mag-fit]');
const spreadBtn = reader.querySelector('[data-mag-spread]');
const full = reader.querySelector('[data-mag-fullscreen]');
const searchInput = reader.querySelector('[data-mag-search-input]');
const searchBtn = reader.querySelector('[data-mag-search]');
const searchStatus = reader.querySelector('[data-mag-search-status]');
const progress = reader.querySelector('[data-mag-progress]');
const progressBar = progress ? progress.querySelector('span') : null;
const runtime = reader.querySelector('.sama-mag-reader-runtime');
let pdfjs = null;
let pdfDoc = null;
let loadingTask = null;
let runtimePromise = null;
let loadPromise = null;
let currentPage = 1;
let zoom = 1;
let fitMode = true;
let spread = false;
let renderSerial = 0;
let searchResults = [];
let searchIndex = -1;
let lastSearchQuery = '';
const textCache = new Map();
const pageCache = new Map();
const activeRenderTasks = new Set();
let touchX = null;
let openedRecorded = false;
function setProgress(value, visible = true) {
if (!progress || !progressBar) return;
progress.hidden = !visible;
progressBar.style.width = `${Math.max(0, Math.min(100, value))}%`;
}
function clampPage(page) {
const max = pdfDoc ? pdfDoc.numPages : Math.max(1, Number(reader.dataset.pages || 1));
return Math.min(max, Math.max(1, Number(page) || 1));
}
function updateControls() {
const total = pdfDoc ? pdfDoc.numPages : Number(reader.dataset.pages || 0);
if (pageInput) {
pageInput.value = String(currentPage);
pageInput.max = String(total || 1);
}
if (totalEl) totalEl.textContent = total ? String(total) : '—';
if (prev) prev.disabled = !pdfDoc || currentPage <= 1;
if (next) next.disabled = !pdfDoc || (total ? currentPage >= total : true);
if (spreadBtn) {
spreadBtn.setAttribute('aria-pressed', spread ? 'true' : 'false');
spreadBtn.classList.toggle('is-active', spread);
spreadBtn.disabled = !pdfDoc || window.innerWidth < 900;
}
}
function pageSet() {
if (!pdfDoc || !spread || window.innerWidth < 900) return [currentPage];
if (currentPage === 1) return [1];
const first = currentPage % 2 === 0 ? currentPage : currentPage - 1;
return [first, Math.min(first + 1, pdfDoc.numPages)].filter((value, index, array) => array.indexOf(value) === index);
}
function getPage(pageNumber) {
const number = clampPage(pageNumber);
if (!pageCache.has(number)) {
pageCache.set(number, pdfDoc.getPage(number).catch((error) => {
pageCache.delete(number);
throw error;
}));
}
return pageCache.get(number);
}
function cancelActiveRenders() {
activeRenderTasks.forEach((task) => {
try { task.cancel(); } catch (e) {}
});
activeRenderTasks.clear();
}
async function renderPage(pageNumber, availableWidth, count, serial) {
const page = await getPage(pageNumber);
if (serial !== renderSerial) return null;
const base = page.getViewport({ scale: 1 });
const gap = count > 1 ? 18 : 0;
const targetWidth = Math.max(280, (availableWidth - gap) / count);
const fitScale = targetWidth / base.width;
const scale = Math.max(0.42, Math.min(3.2, fitScale * zoom));
const viewport = page.getViewport({ scale });
const dprLimit = window.innerWidth < 768 ? 1.45 : 1.7;
const dpr = Math.min(dprLimit, window.devicePixelRatio || 1);
const canvas = document.createElement('canvas');
canvas.className = 'sama-mag-canvas';
canvas.dataset.page = String(pageNumber);
canvas.width = Math.max(1, Math.floor(viewport.width * dpr));
canvas.height = Math.max(1, Math.floor(viewport.height * dpr));
canvas.style.width = `${Math.floor(viewport.width)}px`;
canvas.style.height = `${Math.floor(viewport.height)}px`;
canvas.setAttribute('aria-label', `صفحة ${pageNumber}`);
const context = canvas.getContext('2d', { alpha: false, desynchronized: true });
const renderTask = page.render({
canvasContext: context,
viewport,
transform: dpr === 1 ? null : [dpr, 0, 0, dpr, 0, 0],
intent: 'display',
});
activeRenderTasks.add(renderTask);
try {
await renderTask.promise;
} finally {
activeRenderTasks.delete(renderTask);
}
if (serial !== renderSerial) return null;
return canvas;
}
function prefetchNeighbors() {
if (!pdfDoc) return;
const run = () => {
const candidates = [currentPage - 1, currentPage + 1, currentPage + 2]
.filter((page) => page >= 1 && page <= pdfDoc.numPages);
candidates.forEach((page) => { getPage(page).catch(() => {}); });
};
if ('requestIdleCallback' in window) window.requestIdleCallback(run, { timeout: 800 });
else window.setTimeout(run, 150);
}
async function render() {
if (!pdfDoc || !pagesHost || !stage) return;
cancelActiveRenders();
const serial = ++renderSerial;
updateControls();
pagesHost.classList.toggle('is-spread', spread && window.innerWidth >= 900);
pagesHost.innerHTML = '
يتم تجهيز الصفحة…
';
const numbers = pageSet();
const width = Math.max(320, stage.clientWidth - (window.innerWidth < 768 ? 18 : 36));
try {
const canvases = await Promise.all(numbers.map((page) => renderPage(page, width, numbers.length, serial)));
if (serial !== renderSerial) return;
pagesHost.innerHTML = '';
canvases.filter(Boolean).forEach((canvas) => pagesHost.appendChild(canvas));
if (!openedRecorded) {
openedRecorded = true;
recordEvent('reader_open');
}
prefetchNeighbors();
} catch (error) {
if (error && (error.name === 'RenderingCancelledException' || /cancel/i.test(error.message || ''))) return;
showReaderError('تعذر رسم الصفحة داخل القارئ.');
}
}
function showReaderError(message = '') {
cancelActiveRenders();
if (!pagesHost) return;
if (runtime) runtime.textContent = 'القارئ غير متاح';
pagesHost.classList.remove('is-spread');
pagesHost.innerHTML = '';
const box = document.createElement('div');
box.className = 'sama-mag-reader-error';
const title = document.createElement('h3');
title.textContent = 'تعذر تشغيل القارئ';
const note = document.createElement('p');
note.textContent = message || 'حدث خطأ أثناء تجهيز العدد.';
const retry = document.createElement('button');
retry.type = 'button';
retry.className = 'sama-mag-btn sama-mag-btn--primary';
retry.textContent = 'إعادة المحاولة';
retry.addEventListener('click', () => window.location.reload());
box.append(title, note, retry);
pagesHost.appendChild(box);
[prev, next, zoomIn, zoomOut, fit, spreadBtn, searchBtn, searchInput].forEach((el) => { if (el) el.disabled = true; });
}
function warmReaderRuntime() {
if (runtimePromise) return runtimePromise;
if (!pdfjsUrl || !workerUrl) return Promise.reject(new Error('PDF.js runtime unavailable'));
runtimePromise = import(/* webpackIgnore: true */ pdfjsUrl).then((module) => {
if (!module || !module.getDocument) throw new Error('PDF.js module unavailable');
module.GlobalWorkerOptions.workerSrc = workerUrl;
pdfjs = module;
return module;
}).catch((error) => {
runtimePromise = null;
throw error;
});
return runtimePromise;
}
async function loadPdf() {
if (!pdfUrl) return showReaderError('لا يوجد ملف PDF مرتبط بهذا العدد.');
setProgress(5, true);
try {
const module = await warmReaderRuntime();
setProgress(12, true);
loadingTask = module.getDocument({
url: pdfUrl,
enableXfa: false,
disableAutoFetch: true,
disableStream: true,
rangeChunkSize: 262144,
isEvalSupported: true,
});
loadingTask.onProgress = ({ loaded, total }) => {
if (total) setProgress(12 + Math.min(76, (loaded / total) * 76), true);
};
pdfDoc = await loadingTask.promise;
currentPage = clampPage(Number(new URLSearchParams(window.location.search).get('page') || currentPage || 1));
if (totalEl) totalEl.textContent = String(pdfDoc.numPages);
reader.dataset.pages = String(pdfDoc.numPages);
setProgress(100, true);
window.setTimeout(() => setProgress(100, false), 180);
await render();
} catch (error) {
setProgress(0, false);
showReaderError('تعذر تحميل العدد داخل القارئ. أعد المحاولة بعد تحديث الصفحة.');
}
}
function startLoad() {
if (!loadPromise) {
loadPromise = loadPdf().finally(() => {
if (!pdfDoc) loadPromise = null;
});
}
return loadPromise;
}
function goTo(page) {
currentPage = clampPage(page);
if (pdfDoc) render();
else startLoad();
}
async function pageText(pageNumber) {
if (textCache.has(pageNumber)) return textCache.get(pageNumber);
const page = await getPage(pageNumber);
const content = await page.getTextContent({ disableNormalization: false });
const text = content.items.map((item) => item.str || '').join(' ').replace(/\s+/g, ' ').toLocaleLowerCase('ar');
textCache.set(pageNumber, text);
return text;
}
async function searchPdf() {
if (!pdfDoc || !searchInput || !searchStatus) return;
const query = searchInput.value.trim().toLocaleLowerCase('ar');
lastSearchQuery = query;
if (!query) {
searchResults = [];
searchIndex = -1;
searchStatus.textContent = '';
return;
}
if (searchBtn) searchBtn.disabled = true;
searchStatus.textContent = 'جارٍ البحث…';
searchResults = [];
searchIndex = -1;
try {
const batchSize = 4;
for (let start = 1; start <= pdfDoc.numPages; start += batchSize) {
const end = Math.min(pdfDoc.numPages, start + batchSize - 1);
const batch = [];
for (let page = start; page <= end; page += 1) batch.push(pageText(page).then((text) => ({ page, text })));
const rows = await Promise.all(batch);
rows.forEach(({ page, text }) => { if (text.includes(query)) searchResults.push(page); });
searchStatus.textContent = `بحث ${end}/${pdfDoc.numPages}`;
}
if (searchResults.length) {
searchIndex = 0;
searchStatus.textContent = `1 / ${searchResults.length}`;
goTo(searchResults[0]);
} else {
searchStatus.textContent = 'لا توجد نتائج';
}
} catch (e) {
searchStatus.textContent = 'تعذر البحث';
} finally {
if (searchBtn) searchBtn.disabled = false;
}
}
function nextSearchResult() {
const query = searchInput ? searchInput.value.trim().toLocaleLowerCase('ar') : '';
if (!searchResults.length || query !== lastSearchQuery) return searchPdf();
searchIndex = (searchIndex + 1) % searchResults.length;
searchStatus.textContent = `${searchIndex + 1} / ${searchResults.length}`;
goTo(searchResults[searchIndex]);
}
prev?.addEventListener('click', () => goTo(currentPage - (spread && currentPage > 1 && window.innerWidth >= 900 ? 2 : 1)));
next?.addEventListener('click', () => goTo(currentPage + (spread && currentPage > 1 && window.innerWidth >= 900 ? 2 : 1)));
pageInput?.addEventListener('change', () => goTo(pageInput.value));
pageInput?.addEventListener('keydown', (event) => { if (event.key === 'Enter') goTo(pageInput.value); });
zoomIn?.addEventListener('click', () => { fitMode = false; zoom = Math.min(2.5, zoom + 0.15); render(); });
zoomOut?.addEventListener('click', () => { fitMode = false; zoom = Math.max(0.55, zoom - 0.15); render(); });
fit?.addEventListener('click', () => { fitMode = true; zoom = 1; render(); });
spreadBtn?.addEventListener('click', () => { if (window.innerWidth < 900) return; spread = !spread; updateControls(); render(); });
searchBtn?.addEventListener('click', nextSearchResult);
searchInput?.addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); nextSearchResult(); } });
full?.addEventListener('click', async () => {
try {
if (!document.fullscreenElement) await reader.requestFullscreen();
else await document.exitFullscreen();
} catch (e) {}
});
const openLinks = document.querySelectorAll('[data-mag-reader-open], [data-mag-page]');
openLinks.forEach((link) => {
['pointerenter', 'focus', 'touchstart'].forEach((name) => link.addEventListener(name, () => { warmReaderRuntime().catch(() => {}); }, { passive: true, once: true }));
});
document.querySelectorAll('[data-mag-page]').forEach((link) => {
link.addEventListener('click', (event) => {
event.preventDefault();
currentPage = clampPage(Number(link.dataset.magPage || 1));
startLoad();
reader.scrollIntoView({ behavior: 'smooth', block: 'start' });
if (pdfDoc) render();
});
});
document.querySelectorAll('[data-mag-reader-open]').forEach((link) => {
link.addEventListener('click', () => {
startLoad();
window.setTimeout(() => stage?.focus({ preventScroll: true }), 300);
});
});
stage?.addEventListener('keydown', (event) => {
if (event.key === 'ArrowLeft' || event.key === 'PageDown') { event.preventDefault(); next?.click(); }
if (event.key === 'ArrowRight' || event.key === 'PageUp') { event.preventDefault(); prev?.click(); }
});
stage?.addEventListener('touchstart', (event) => { if (event.touches.length === 1) touchX = event.touches[0].clientX; }, { passive: true });
stage?.addEventListener('touchend', (event) => {
if (touchX === null || !event.changedTouches.length) return;
const delta = event.changedTouches[0].clientX - touchX;
touchX = null;
if (Math.abs(delta) < 55) return;
if (delta < 0) next?.click(); else prev?.click();
}, { passive: true });
reader.addEventListener('contextmenu', (event) => {
if (event.target.closest('.sama-mag-pages')) event.preventDefault();
});
document.addEventListener('keydown', (event) => {
if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== 's') return;
if (document.fullscreenElement === reader || reader.contains(document.activeElement)) event.preventDefault();
});
let resizeTimer = null;
window.addEventListener('resize', () => {
window.clearTimeout(resizeTimer);
resizeTimer = window.setTimeout(() => {
if (window.innerWidth < 900) spread = false;
if (fitMode) zoom = 1;
updateControls();
if (pdfDoc) render();
}, 160);
});
updateControls();
if (window.location.hash === '#reader') {
startLoad();
window.setTimeout(() => reader.scrollIntoView({ block: 'start' }), 30);
} else if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
if (entries.some((entry) => entry.isIntersecting)) {
observer.disconnect();
startLoad();
}
}, { rootMargin: '1000px 0px' });
observer.observe(reader);
} else {
startLoad();
}
}