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(); } }