// =================================================================== // 🚀 نظام التحميل الديناميكي الذكي - إصدار هجين صامت ومستقر (v13.2-final-local) // ✅ التعديل النهائي: // - تحميل FullCalendar من الملفات المحلية فقط (js/fullcalendar/) // - إزالة جميع مراجع CDN (cdnjs, unpkg, jsdelivr) // - إزالة تحميل أي ملف CSS خاص بـ FullCalendar // - استخدام Loader.loadScript() لتحميل الملفات المحلية // - التحقق من window.FullCalendar قبل تحميل اللغة // - عدم الرجوع إلى الإنترنت عند الفشل // إعداد أبو علي © 2026 // =================================================================== const requiredFiles = [ // --- البنية التحتية والتخزين --- { path: 'js/offline-storage.js', priority: 'critical', dependencies: [] }, { path: 'js/services/activity-logger.js', priority: 'high', dependencies: [] }, { path: 'js/services/smart-search.js', priority: 'medium', dependencies: [] }, // --- الخدمات والمكونات --- { path: 'js/services/shared-service.js', priority: 'high', dependencies: ['js/offline-storage.js'] }, { path: 'js/components.js', priority: 'high', dependencies: ['js/services/shared-service.js'] }, // --- 📌 FullCalendar يتم تحميله ديناميكياً من الملفات المحلية عبر Loader --- // --- ملفات الإعداد الخاصة بالتقويم --- { path: 'js/config-calendar.js', priority: 'high', dependencies: [] }, { path: 'js/modules/calendar-countdown.js', priority: 'high', dependencies: ['js/config-calendar.js'] }, // --- موديلات النظام --- { path: 'js/modules/auth.js', priority: 'high', dependencies: ['js/components.js'] }, { path: 'js/modules/navigation.js', priority: 'high', dependencies: ['js/components.js'] }, { path: 'js/modules/content.js', priority: 'high', dependencies: ['js/components.js'] }, { path: 'js/modules/data.js', priority: 'high', dependencies: ['js/services/activity-logger.js', 'js/services/shared-service.js', 'js/offline-storage.js'] }, { path: 'js/modules/detail.js', priority: 'medium', dependencies: ['js/modules/content.js', 'js/components.js', 'js/services/shared-service.js'] }, { path: 'js/modules/modal.js', priority: 'low', dependencies: ['js/components.js'] }, { path: 'js/modules/dropdown.js', priority: 'low', dependencies: ['js/components.js'] }, { path: 'js/modules/admin.js', priority: 'medium', dependencies: ['js/modules/auth.js', 'js/services/activity-logger.js', 'js/components.js'] }, { path: 'js/modules/admin-activity-log.js', priority: 'medium', dependencies: ['js/modules/admin.js'] }, { path: 'js/modules/admin-user-management.js', priority: 'medium', dependencies: ['js/modules/admin.js'] }, { path: 'js/modules/admin-settings-profile.js', priority: 'medium', dependencies: ['js/modules/admin.js'] }, { path: 'js/modules/admin-backup.js', priority: 'medium', dependencies: ['js/modules/admin.js'] }, { path: 'js/modules/admin-calendar-manager.js', priority: 'medium', dependencies: ['js/modules/admin.js', 'js/config-calendar.js'] } ]; const Loader = { loadedFiles: new Set(), isLoading: false, loadingPromises: new Map(), fileCache: new Map(), maxRetries: 3, retryDelay: 300, syncTimeout: null, _commentsListener: null, _initialized: false, _pageRendered: false, _retryCount: 0, _maxDataRetries: 3, _ratingsTimers: [], _onlineSyncTimer: null, _dataLoadingInProgress: false, _lastRenderedPage: null, _firebaseLoaded: false, _fullCalendarLoaded: false, _fullCalendarLoading: false, _connectivityInitialized: false, _domReadyPromise: null, _domReadyResolve: null, _moduleReadyCallbacks: {}, _dirtyFlags: { books: false, quizzes: false, articles: false, comments: false, users: false, settings: false }, _ordered: false, get isOffline() { return window.ConnectionManager ? window.ConnectionManager.isOffline() : !navigator.onLine; }, // ========== توحيد المسار ========== _normalizePath(path) { if (!path) return ''; if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('//')) { return path; } let normalized = path.replace(/^\.\//, '').replace(/^\/+/, ''); return normalized; }, // ========== انتظار DOM مرة واحدة ========== _waitForDOM() { if (!this._domReadyPromise) { this._domReadyPromise = new Promise(resolve => { if (document.readyState !== 'loading') { resolve(); } else { this._domReadyResolve = resolve; document.addEventListener('DOMContentLoaded', resolve, { once: true }); } }); } return this._domReadyPromise; }, // ========== تحميل Firebase الديناميكي ========== async loadFirebase() { if (this._firebaseLoaded && window.firebase && window.firebase.database) return true; if (this.isOffline) return false; try { await this.loadScript('https://www.gstatic.com/firebasejs/9.22.0/firebase-app-compat.js'); await this.loadScript('https://www.gstatic.com/firebasejs/9.22.0/firebase-database-compat.js'); await this.loadScript('https://www.gstatic.com/firebasejs/9.22.0/firebase-auth-compat.js'); if (window.firebase && window.firebase.database) { if (window.retryInitFirebase && typeof window.retryInitFirebase === 'function') { await window.retryInitFirebase(); } else if (window.AppConfigContainer && typeof window.AppConfigContainer.retryInitFirebase === 'function') { await window.AppConfigContainer.retryInitFirebase(); } this._firebaseLoaded = true; return true; } return false; } catch(e) { console.error('⚠️ [Loader] فشل تحميل أو تهيئة Firebase:', e); return false; } }, // =================================================================== // 🔄 تحميل FullCalendar من الملفات المحلية فقط (بدون CDN أو CSS) // =================================================================== async loadFullCalendar() { // 1. منع التحميل المكرر if (this._fullCalendarLoaded && window.FullCalendar) return true; if (this._fullCalendarLoading) { return new Promise((resolve) => { const check = () => { if (this._fullCalendarLoaded) resolve(true); else if (!this._fullCalendarLoading) resolve(false); else setTimeout(check, 200); }; check(); }); } this._fullCalendarLoading = true; try { // 2. تحميل المكتبة الرئيسية من الملف المحلي const mainLoaded = await this.loadScript('js/fullcalendar/index.global.min.js'); if (!mainLoaded) { this._fullCalendarLoading = false; console.error('❌ [Loader] فشل تحميل FullCalendar من المسار المحلي: js/fullcalendar/index.global.min.js'); return false; } // 3. التحقق من وجود FullCalendar في window await this._waitForFullCalendar(3000); if (!window.FullCalendar) { this._fullCalendarLoading = false; console.error('❌ [Loader] تم تحميل الملف لكن window.FullCalendar غير متاح'); return false; } // 4. تحميل ملف اللغة العربية (اختياري، لكن نحاول) try { const localeLoaded = await this.loadScript('js/fullcalendar/ar.global.min.js'); if (!localeLoaded) { console.warn('⚠️ [Loader] تعذر تحميل ملف اللغة العربية، سيتم استخدام الإنجليزية'); } } catch (e) { console.warn('⚠️ [Loader] تعذر تحميل ملف اللغة العربية:', e); } // 5. تأكيد النجاح this._fullCalendarLoaded = true; this._fullCalendarLoading = false; window.dispatchEvent(new CustomEvent('fullcalendar-loaded')); console.log('✅ [Loader] تم تحميل FullCalendar من الملفات المحلية بنجاح'); return true; } catch (error) { this._fullCalendarLoading = false; console.error('❌ [Loader] فشل تحميل FullCalendar من الملفات المحلية:', error); return false; } }, // انتظار توفر FullCalendar في window _waitForFullCalendar(timeout = 3000) { return new Promise((resolve) => { if (window.FullCalendar) { resolve(true); return; } const start = Date.now(); const check = () => { if (window.FullCalendar) { resolve(true); return; } if (Date.now() - start > timeout) { resolve(false); return; } setTimeout(check, 100); }; check(); }); }, // ========== تحميل سكريبت عام ========== async loadScript(src, retryCount = 0) { const normalizedSrc = this._normalizePath(src); if (this.fileCache.has(normalizedSrc) || this.loadedFiles.has(normalizedSrc)) return true; if (this.loadingPromises.has(normalizedSrc)) return this.loadingPromises.get(normalizedSrc); const loadingPromise = new Promise(async (resolve) => { await this._waitForDOM(); if (this.isOffline && !normalizedSrc.startsWith('http')) { const cached = await this.loadFromCache(normalizedSrc); if (cached) { this.loadedFiles.add(normalizedSrc); this.fileCache.set(normalizedSrc, true); resolve(true); return; } resolve(false); return; } const script = document.createElement('script'); let finalSrc = normalizedSrc; if (!finalSrc.startsWith('http') && !finalSrc.startsWith('//')) { if (finalSrc.startsWith('/')) { finalSrc = '.' + finalSrc; } else if (!finalSrc.startsWith('./')) { finalSrc = './' + finalSrc; } } script.src = finalSrc; script.type = 'text/javascript'; script.async = false; if (finalSrc.startsWith('http')) { script.crossOrigin = 'anonymous'; } script.onload = () => { this.loadedFiles.add(normalizedSrc); this.fileCache.set(normalizedSrc, true); this.loadingPromises.delete(normalizedSrc); resolve(true); }; script.onerror = async () => { this.loadingPromises.delete(normalizedSrc); if (!finalSrc.startsWith('http')) { const cached = await this.loadFromCache(normalizedSrc); if (cached) { this.loadedFiles.add(normalizedSrc); this.fileCache.set(normalizedSrc, true); resolve(true); return; } } if (retryCount < this.maxRetries) { const delay = this.retryDelay * (retryCount + 1); await new Promise(r => setTimeout(r, delay)); const result = await this.loadScript(normalizedSrc, retryCount + 1); resolve(result); } else { resolve(false); } }; document.body.appendChild(script); }); this.loadingPromises.set(normalizedSrc, loadingPromise); return loadingPromise; }, async loadFromCache(src) { if (!('caches' in window)) return false; const normalizedSrc = this._normalizePath(src); let blobUrl = null; try { const cacheNames = await caches.keys(); const prefixedSrc = `./${normalizedSrc}`; for (const cacheName of cacheNames) { const cache = await caches.open(cacheName); const cachedResponse = await cache.match(prefixedSrc) || await cache.match(normalizedSrc) || await cache.match(src); if (cachedResponse) { const scriptCode = await cachedResponse.text(); const blob = new Blob([scriptCode], { type: 'text/javascript' }); blobUrl = URL.createObjectURL(blob); return new Promise((resolve) => { const script = document.createElement('script'); script.src = blobUrl; script.type = 'text/javascript'; script.async = false; script.onload = () => { resolve(true); }; script.onerror = () => { resolve(false); }; document.body.appendChild(script); }).finally(() => { if (blobUrl) URL.revokeObjectURL(blobUrl); }); } } } catch (e) { console.warn('⚠️ [Loader] خطأ أثناء القراءة من الكاش:', e); } return false; }, async loadBatch(files) { const results = await Promise.allSettled(files.map(file => this.loadScript(file))); return results; }, async waitForModule(moduleName, methodName = null, timeout = 5000) { const key = methodName ? `${moduleName}.${methodName}` : moduleName; const module = window[moduleName]; if (module && (!methodName || typeof module[methodName] === 'function')) { return true; } return new Promise((resolve) => { const callback = () => { const mod = window[moduleName]; if (mod && (!methodName || typeof mod[methodName] === 'function')) { cleanup(); resolve(true); } }; const timer = setTimeout(() => { cleanup(); resolve(false); }, timeout); const eventName = `moduleReady:${key}`; const cleanup = () => { clearTimeout(timer); window.removeEventListener(eventName, callback); }; window.addEventListener(eventName, callback); callback(); }); }, async processGroupWithDependencies(groupFiles) { let remaining = [...groupFiles]; let stuckCounter = 0; const maxStuckAttempts = 3; while (remaining.length > 0) { const readyToLoad = remaining.filter(f => f.dependencies.every(dep => this.loadedFiles.has(this._normalizePath(dep))) ); if (readyToLoad.length === 0) { const missingDeps = remaining.flatMap(f => f.dependencies) .filter(dep => !this.loadedFiles.has(this._normalizePath(dep))); if (missingDeps.length > 0) { stuckCounter++; if (stuckCounter > maxStuckAttempts) break; await this.loadBatch(missingDeps); continue; } break; } stuckCounter = 0; await this.loadBatch(readyToLoad.map(f => f.path)); remaining = remaining.filter(f => !this.loadedFiles.has(this._normalizePath(f.path))); } }, async loadAllFiles() { if (this.isLoading) return; this.isLoading = true; const groups = { critical: requiredFiles.filter(f => f.priority === 'critical'), high: requiredFiles.filter(f => f.priority === 'high'), medium: requiredFiles.filter(f => f.priority === 'medium'), low: requiredFiles.filter(f => f.priority === 'low') }; if (groups.critical.length > 0) { await this.loadBatch(groups.critical.map(f => f.path)); } if (window.OfflineStorage) { try { await window.OfflineStorage.init(); } catch(e) {} } await this.processGroupWithDependencies(groups.high); await this.processGroupWithDependencies(groups.medium); await this.processGroupWithDependencies(groups.low); await this.waitForCriticalModules(); await this.initializeApp(); this.isLoading = false; }, async waitForCriticalModules() { const modules = [ { name: 'Navigation', method: 'showPage' }, { name: 'UIComponents', method: 'createContentCard' }, { name: 'SharedService', method: 'showToast' }, { name: 'Data', method: 'loadCollection' } ]; const timeout = this.isOffline ? 2000 : 4000; await Promise.allSettled(modules.map(m => this.waitForModule(m.name, m.method, timeout))); }, async initializeApp() { if (this._initialized) return; this._initialized = true; window.AppState = window.AppState || {}; this.initializeDarkMode(); this.setupGlobalErrorHandler(); this.setupConnectivityMonitoring(); if (!this.isOffline) { await this.loadFirebase(); } if (!this.isOffline && window.Auth?.init) { try { await window.Auth.init(); } catch (e) {} } await this.loadDataHybrid(); if (window.Navigation?.initBackButtonHandler) { try { window.Navigation.initBackButtonHandler(); } catch (e) {} } if (window.Dropdown?.init) { try { window.Dropdown.init(); } catch (e) {} } await this.handleInitialURL(); this.setupDeferredTasks(); this._fireModuleReadyEvents(); const loader = document.getElementById('loading-indicator'); if (loader) loader.classList.add('hidden'); }, _fireModuleReadyEvents() { const modulesToFire = ['Navigation', 'UIComponents', 'SharedService', 'Data', 'Auth', 'Content', 'Detail', 'Admin', 'Modal', 'Dropdown', 'SmartSearch', 'CalendarCountdown']; modulesToFire.forEach(name => { if (window[name]) { window.dispatchEvent(new CustomEvent(`moduleReady:${name}`)); if (name === 'Navigation' && window.Navigation.showPage) { window.dispatchEvent(new CustomEvent(`moduleReady:Navigation.showPage`)); } } }); }, async loadDataHybrid() { if (this._dataLoadingInProgress) return; this._dataLoadingInProgress = true; let dataLoaded = false; try { if (this.isOffline) { dataLoaded = await this.tryLoadFromCache(); if (dataLoaded) { this._pageRendered = false; this._lastRenderedPage = null; this.renderCurrentPage(); this.scheduleRatingsRefresh(); const loader = document.getElementById('loading-indicator'); if (loader) loader.classList.add('hidden'); return; } this.showOfflineEmptyScreen(); return; } if (window.Data && window.database) { dataLoaded = await this.tryLoadFromServer(); } if (!dataLoaded) { dataLoaded = await this.tryLoadFromCache(); } if (!dataLoaded && !this.isOffline && this._retryCount < this._maxDataRetries) { this._retryCount++; const waitTime = Math.min(this._retryCount * 2000, 10000); await new Promise(r => setTimeout(r, waitTime)); this._dataLoadingInProgress = false; await this.loadDataHybrid(); return; } if (!dataLoaded) { this.showRetryScreen(); return; } this._pageRendered = false; this._lastRenderedPage = null; this.renderCurrentPage(); this.scheduleRatingsRefresh(); } catch (error) { if (!dataLoaded) { dataLoaded = await this.tryLoadFromCache(); if (dataLoaded) { this._pageRendered = false; this._lastRenderedPage = null; this.renderCurrentPage(); this.scheduleRatingsRefresh(); } } } finally { this._dataLoadingInProgress = false; const loader = document.getElementById('loading-indicator'); if (loader) loader.classList.add('hidden'); } }, async tryLoadFromServer() { const db = window.database; const state = window.AppState; if (!db) return false; try { if (window.loadPinnedItems) { try { await window.loadPinnedItems(); } catch(e) {} } if (window.loadOrdering) { try { await window.loadOrdering(); } catch(e) {} } const [navSnap, settingsSnap] = await Promise.allSettled([ db.ref('navigation').once('value'), db.ref('settings').once('value') ]); if (navSnap.status === 'fulfilled' && navSnap.value?.val()) { state.navigationSettings = navSnap.value.val(); } if (settingsSnap.status === 'fulfilled' && settingsSnap.value?.val()) { state.siteSettings = settingsSnap.value.val(); } await Promise.allSettled([ window.Data.loadCollection('books', 'allBooks'), window.Data.loadCollection('quizzes', 'allQuizzes'), window.Data.loadCollection('articles', 'allArticles'), window.Data.loadCollection('users', 'allUsers') ]); const hasBooks = state.allBooks && state.allBooks.length > 0; const hasQuizzes = state.allQuizzes && state.allQuizzes.length > 0; const hasArticles = state.allArticles && state.allArticles.length > 0; if (!hasBooks && !hasQuizzes && !hasArticles) return false; this.applyOrdering(true); this.updateUIStats(); await this.saveDataToOffline(); this.setupCommentsListener(db); return true; } catch (error) { return false; } }, async tryLoadFromCache() { const state = window.AppState; if (!state) return false; let dataChanged = false; try { if (window.OfflineStorage && typeof window.OfflineStorage.getAll === 'function') { const [books, quizzes, articles, comments, users] = await Promise.allSettled([ window.OfflineStorage.getAll('books'), window.OfflineStorage.getAll('quizzes'), window.OfflineStorage.getAll('articles'), window.OfflineStorage.getAll('comments'), window.OfflineStorage.getAll('users') ]); if (books.status === 'fulfilled' && books.value?.length > 0) { state.allBooks = books.value; dataChanged = true; } if (quizzes.status === 'fulfilled' && quizzes.value?.length > 0) { state.allQuizzes = quizzes.value; dataChanged = true; } if (articles.status === 'fulfilled' && articles.value?.length > 0) { state.allArticles = articles.value; dataChanged = true; } if (comments.status === 'fulfilled' && comments.value?.length > 0) { state.allComments = comments.value; } if (users.status === 'fulfilled' && users.value?.length > 0) { state.allUsers = users.value; } const [siteSettings, navSettings, currentUser] = await Promise.allSettled([ window.OfflineStorage.getSetting('siteSettings'), window.OfflineStorage.getSetting('navigationSettings'), window.OfflineStorage.getCurrentUser() ]); if (siteSettings.status === 'fulfilled' && siteSettings.value) state.siteSettings = siteSettings.value; if (navSettings.status === 'fulfilled' && navSettings.value) state.navigationSettings = navSettings.value; if (currentUser.status === 'fulfilled' && currentUser.value) state.currentUser = currentUser.value; } if (!state.allBooks?.length && !state.allQuizzes?.length && !state.allArticles?.length) { state.allBooks = this.getLocalJSON('allBooks'); state.allQuizzes = this.getLocalJSON('allQuizzes'); state.allArticles = this.getLocalJSON('allArticles'); state.allComments = this.getLocalJSON('allComments'); state.allUsers = this.getLocalJSON('allUsers'); try { const savedSettings = localStorage.getItem('siteSettings'); if (savedSettings) state.siteSettings = JSON.parse(savedSettings); const savedNav = localStorage.getItem('navigationSettings'); if (savedNav) state.navigationSettings = JSON.parse(savedNav); } catch(e) {} } if (!state.allBooks?.length && !state.allQuizzes?.length && !state.allArticles?.length) { state.allBooks = this.getLocalJSON('offline_cache_books'); state.allQuizzes = this.getLocalJSON('offline_cache_quizzes'); state.allArticles = this.getLocalJSON('offline_cache_articles'); state.allComments = this.getLocalJSON('offline_cache_comments'); } const hasData = (state.allBooks?.length > 0) || (state.allQuizzes?.length > 0) || (state.allArticles?.length > 0); if (hasData) { if (dataChanged) { this.applyOrdering(true); } else if (!this._ordered) { this.applyOrdering(); } this.updateUIStats(); window.dispatchEvent(new CustomEvent('offline-data-ready', { detail: { source: 'cache', timestamp: Date.now() } })); return true; } return false; } catch (error) { return false; } }, getLocalJSON(key) { try { const raw = localStorage.getItem(key); return raw ? JSON.parse(raw) : []; } catch (e) { return []; } }, scheduleRatingsRefresh() { if (this._ratingsTimers && this._ratingsTimers.length > 0) { this._ratingsTimers.forEach(timer => clearTimeout(timer)); this._ratingsTimers = []; } const currentPage = window.AppState?.currentPage || this._getCurrentPageFromURL(); if (currentPage !== 'home') return; this._ratingsTimers.push(setTimeout(() => this.refreshAllRatings(), 1500)); this._ratingsTimers.push(setTimeout(() => this.refreshAllRatings(), 4000)); }, _getCurrentPageFromURL() { const hash = window.location.hash ? window.location.hash.substring(1) : ''; if (hash) return hash.split('/')[0]; const path = window.location.pathname.substring(1); return (path && path !== 'index.html') ? path.split('/')[0] : 'home'; }, refreshAllRatings() { if (window.forceRefreshRatings && typeof window.forceRefreshRatings === 'function') { try { window.forceRefreshRatings(); } catch(e) {} } if (window.SharedService?.updateStats) { try { window.SharedService.updateStats(); } catch(e) {} } if (window.CalendarCountdown && typeof window.CalendarCountdown.init === 'function') { const currentHash = window.location.hash ? window.location.hash.substring(1) : ''; const activePage = currentHash.split('/')[0] || window.AppState?.currentPage || 'home'; if (activePage === 'calendar') { try { window.CalendarCountdown.init(); } catch(e) {} } } }, applyOrdering(force = false) { if (!force && this._ordered) return; const state = window.AppState; if (!state) return; let ordered = false; ['allBooks', 'allQuizzes', 'allArticles'].forEach(key => { if (state[key] && state[key].length > 0) { try { const type = key.replace('all', '').toLowerCase(); if (window.applyFullOrdering && typeof window.applyFullOrdering === 'function') { state[key] = window.applyFullOrdering(state[key], type, 'createdAt'); } else if (window.applyOrdering && typeof window.applyOrdering === 'function') { state[key] = window.applyOrdering(state[key], type, 'createdAt'); } ordered = true; } catch (e) { console.warn(`⚠️ [Loader] فشل ترتيب المصفوفة ${key}:`, e); } } }); if (ordered) this._ordered = true; }, updateUIStats() { if (window.SharedService?.updateStats) { try { window.SharedService.updateStats(); } catch(e) {} } if (window.SharedService?.updateUI) { try { window.SharedService.updateUI(); } catch(e) {} } }, setupCommentsListener(db) { if (!db) return; if (this._commentsListener) { try { db.ref('comments').off('value', this._commentsListener); } catch(e) {} this._commentsListener = null; } this._commentsListener = async (snapshot) => { try { const commentsData = snapshot.val() || {}; window.AppState.allComments = Object.keys(commentsData).map(key => ({ id: key, ...commentsData[key] })); this._dirtyFlags.comments = true; if (window.OfflineStorage) { try { await window.OfflineStorage.saveAll('comments', window.AppState.allComments); } catch(e) {} this._dirtyFlags.comments = false; } if (window.AppState.currentAdminTab === 'comments' || window.location.hash.includes('admin/comments')) { if (window.Admin?.renderComments) { window.Admin.renderComments(); } } if (window.SharedService?.updateStats) window.SharedService.updateStats(); this.scheduleRatingsRefresh(); } catch(e) {} }; db.ref('comments').on('value', this._commentsListener); }, cleanupCommentsListener() { if (this._commentsListener && window.database) { try { window.database.ref('comments').off('value', this._commentsListener); } catch(e) {} this._commentsListener = null; } }, async saveDataToOffline(forceAll = false) { const storage = window.OfflineStorage; if (!storage || !window.AppState) return; try { const state = window.AppState; const promises = []; if (forceAll || this._dirtyFlags.books) { promises.push(storage.saveAll('books', state.allBooks || [])); this._dirtyFlags.books = false; } if (forceAll || this._dirtyFlags.quizzes) { promises.push(storage.saveAll('quizzes', state.allQuizzes || [])); this._dirtyFlags.quizzes = false; } if (forceAll || this._dirtyFlags.articles) { promises.push(storage.saveAll('articles', state.allArticles || [])); this._dirtyFlags.articles = false; } if (forceAll || this._dirtyFlags.comments) { promises.push(storage.saveAll('comments', state.allComments || [])); this._dirtyFlags.comments = false; } if (forceAll || this._dirtyFlags.users) { promises.push(storage.saveAll('users', state.allUsers || [])); this._dirtyFlags.users = false; } if (state.siteSettings) promises.push(storage.saveSetting('siteSettings', state.siteSettings)); if (state.navigationSettings) promises.push(storage.saveSetting('navigationSettings', state.navigationSettings)); if (state.currentUser) promises.push(storage.saveCurrentUser(state.currentUser)); await Promise.allSettled(promises); try { if (forceAll || this._dirtyFlags.books) localStorage.setItem('allBooks', JSON.stringify(state.allBooks || [])); if (forceAll || this._dirtyFlags.quizzes) localStorage.setItem('allQuizzes', JSON.stringify(state.allQuizzes || [])); if (forceAll || this._dirtyFlags.articles) localStorage.setItem('allArticles', JSON.stringify(state.allArticles || [])); if (forceAll || this._dirtyFlags.comments) localStorage.setItem('allComments', JSON.stringify(state.allComments || [])); } catch(e) {} } catch (e) {} }, markDirty(type) { if (this._dirtyFlags.hasOwnProperty(type)) { this._dirtyFlags[type] = true; } }, renderCurrentPage() { if (this._pageRendered) return; if (!window.Navigation || typeof window.Navigation.showPage !== 'function') return; if (window.Navigation.navigationLock) { window.Navigation.navigationLock = false; } this._pageRendered = true; let page = 'home'; const hash = window.location.hash ? window.location.hash.substring(1) : ''; if (hash) { page = hash.split('/')[0]; } else { const path = window.location.pathname.substring(1); if (path && path !== 'index.html' && path !== '') { page = path.split('/')[0]; } } this._lastRenderedPage = page; const loader = document.getElementById('loading-indicator'); if (loader) loader.classList.add('hidden'); window.Navigation.showPage(page); setTimeout(() => { const currentHash = window.location.hash ? window.location.hash.substring(1) : ''; let currentPage = currentHash.split('/')[0]; if (!currentPage) { const currentPath = window.location.pathname.substring(1); currentPage = (currentPath && currentPath !== 'index.html') ? currentPath.split('/')[0] : 'home'; } if (currentPage === this._lastRenderedPage) { const container = document.getElementById('page-container'); if (container && container.innerHTML.length < 100) { window.Navigation.showPage(page); } } }, 500); }, showRetryScreen() { const container = document.getElementById('page-container'); const loader = document.getElementById('loading-indicator'); if (loader) loader.classList.add('hidden'); if (container) { container.innerHTML = ` `; } }, showOfflineEmptyScreen() { const container = document.getElementById('page-container'); const loader = document.getElementById('loading-indicator'); if (loader) loader.classList.add('hidden'); if (container) { container.innerHTML = ` `; } }, showLoadingError(message) { const container = document.getElementById('page-container'); if (container) { container.innerHTML = ` `; } }, async handleInitialURL() { if (!window.Navigation) return; const validPages = (window.Navigation.validPages) || (window.AppConfig?.validRoutes) || ['home', 'books', 'quizzes', 'articles', 'calendar', 'about', 'detail', 'admin']; let path = window.location.hash ? window.location.hash.substring(1) : ''; if (!path) { path = window.location.pathname.substring(1); if (path === 'index.html') path = ''; } if (!path || path === 'home') return; const parts = path.split('/'); if (!validPages.includes(parts[0])) { await window.Navigation.showPage('home'); return; } try { if (parts[0] === 'detail' && parts.length >= 3) { window.AppState.currentDetailType = parts[1]; window.AppState.currentDetailId = parts[2]; await window.Navigation.showPage('detail'); } else if (parts[0] === 'admin' && parts.length >= 2) { window.AppState.currentAdminTab = parts[1]; await window.Navigation.showPage('admin'); } else if (parts[0]) { await window.Navigation.showPage(parts[0]); } } catch (error) {} }, setupConnectivityMonitoring() { if (this._connectivityInitialized) return; this._connectivityInitialized = true; const handleOnlineEvent = async () => { if (this._onlineSyncTimer) clearTimeout(this._onlineSyncTimer); this._onlineSyncTimer = setTimeout(async () => { if (window.OfflineStorage) { await window.OfflineStorage.syncPendingActions().catch(() => {}); } this._pageRendered = false; this._retryCount = 0; this._dataLoadingInProgress = false; this._lastRenderedPage = null; await this.loadDataHybrid(); }, 3000); }; const handleOfflineEvent = () => { if (this._onlineSyncTimer) clearTimeout(this._onlineSyncTimer); this.cleanupCommentsListener(); }; if (window.ConnectionManager && typeof window.ConnectionManager.onStatusChange === 'function') { window.ConnectionManager.onStatusChange((isOnline) => { if (isOnline) handleOnlineEvent(); else handleOfflineEvent(); }); } else { window.addEventListener('online', handleOnlineEvent); window.addEventListener('offline', handleOfflineEvent); } }, initializeDarkMode() { if (window.UIComponents?.initDarkMode) { try { window.UIComponents.initDarkMode(); } catch (e) {} } }, setupGlobalErrorHandler() { window.addEventListener('error', (event) => { if (event.error?.message?.includes('initBackButtonHandler')) event.preventDefault(); }); }, setupDeferredTasks() { setTimeout(() => { if (window.SmartSearch && typeof window.SmartSearch.updateSearchPlaceholder === 'function') { try { window.SmartSearch.updateSearchPlaceholder(); } catch(e) {} } if (window.SharedService?.updateUI) { try { window.SharedService.updateUI(); } catch(e) {} } }, 300); }, destroy() { this.cleanupCommentsListener(); if (this._onlineSyncTimer) clearTimeout(this._onlineSyncTimer); if (this._ratingsTimers) { this._ratingsTimers.forEach(timer => clearTimeout(timer)); this._ratingsTimers = []; } this.loadedFiles.clear(); this.loadingPromises.clear(); this.fileCache.clear(); this._initialized = false; this._connectivityInitialized = false; this._domReadyPromise = null; this._domReadyResolve = null; this._moduleReadyCallbacks = {}; } }; // ========== بدء التحميل ========== if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => Loader.loadAllFiles()); } else { Loader.loadAllFiles(); } // ========== تنظيف عند إغلاق الصفحة ========== window.addEventListener('beforeunload', () => { Loader.cleanupCommentsListener(); }); // ========== السحب للتحديث (Pull to Refresh) ========== let touchStartY = 0, touchMovedY = 0, isPulling = false; const refreshThreshold = 140; window.addEventListener('touchstart', (e) => { if (window.scrollY <= 5) { touchStartY = e.touches[0].clientY; isPulling = true; } else { isPulling = false; } }, { passive: true }); window.addEventListener('touchmove', (e) => { if (!isPulling) return; touchMovedY = e.touches[0].clientY; }, { passive: true }); window.addEventListener('touchend', () => { if (!isPulling) return; if ((touchMovedY - touchStartY) > refreshThreshold && window.scrollY <= 5) { if (window.Loader) { window.Loader._retryCount = 0; window.Loader._pageRendered = false; window.Loader._dataLoadingInProgress = false; window.Loader._lastRenderedPage = null; window.Loader.loadDataHybrid(); } else { window.location.reload(); } } touchStartY = 0; touchMovedY = 0; isPulling = false; }); window.Loader = Loader;