// PWA Service Worker Registration и установка class PWAManager { constructor() { this.deferredPrompt = null; this.init(); } init() { console.log("PWA Manager инициализирован"); this.registerServiceWorker(); this.setupInstallPrompt(); this.setupAppInstalled(); this.checkPWARequirements(); this.setupServiceWorkerMessages(); } // Проверка требований PWA checkPWARequirements() { console.log("Проверка требований PWA:"); console.log("- Service Worker:", "serviceWorker" in navigator); console.log( "- HTTPS:", location.protocol === "https:" || location.hostname === "localhost" ); console.log( "- Manifest:", document.querySelector('link[rel="manifest"]') !== null ); console.log( "- Icons:", document.querySelector('link[rel="icon"]') !== null ); } // Регистрация Service Worker registerServiceWorker() { if ("serviceWorker" in navigator) { window.addEventListener("load", () => { navigator.serviceWorker .register("/sw.js") .then((registration) => { console.log("SW зарегистрирован успешно:", registration.scope); // Проверяем обновления registration.addEventListener("updatefound", () => { const newWorker = registration.installing; newWorker.addEventListener("statechange", () => { if ( newWorker.state === "installed" && navigator.serviceWorker.controller ) { this.showUpdateNotification(); } }); }); }) .catch((error) => { console.log("Ошибка регистрации SW:", error); }); }); } else { console.log("Service Worker не поддерживается"); } } // Показ уведомления об обновлении showUpdateNotification() { if (confirm("Доступна новая версия приложения. Обновить?")) { if (navigator.serviceWorker.controller) { navigator.serviceWorker.controller.postMessage({ type: "SKIP_WAITING", }); } window.location.reload(); } } // Настройка промпта установки setupInstallPrompt() { window.addEventListener("beforeinstallprompt", (e) => { console.log("beforeinstallprompt событие получено"); // На мобильных устройствах позволяем браузеру показать нативный баннер if (this.isMobileDevice()) { console.log( "Мобильное устройство - разрешаем нативный баннер установки" ); // Не вызываем preventDefault() для мобильных устройств this.deferredPrompt = e; return; } // На десктопе показываем кастомную кнопку e.preventDefault(); this.deferredPrompt = e; // Показываем кнопку установки с задержкой для лучшего UX setTimeout(() => { this.showInstallButton(); }, 1000); }); } // Показ кнопки установки showInstallButton() { console.log("Попытка показать кнопку установки"); // Проверяем, не установлено ли уже приложение if (this.isPWAInstalled()) { console.log("Приложение уже установлено"); return; } // Показываем кнопку только на десктопе (на мобильных используем нативный баннер) if (this.isMobileDevice()) { console.log( "Мобильное устройство - используем нативный баннер установки" ); return; } // Проверяем, поддерживает ли браузер установку PWA if (!this.deferredPrompt) { console.log("Установка PWA не поддерживается в этом браузере"); return; } const installButton = this.createInstallButton(); this.addInstallButtonToPage(installButton); } // Проверка на мобильное устройство isMobileDevice() { const ua = navigator.userAgent; return ( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( ua ) || (navigator.maxTouchPoints && navigator.maxTouchPoints > 2) || window.matchMedia("(max-width: 768px)").matches || /Mobile|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua) ); } // Проверка на мобильный Safari isMobileSafari() { const ua = navigator.userAgent; return ( /iPad|iPhone|iPod/.test(ua) && /Safari/.test(ua) && !/CriOS|FxiOS|OPiOS|mercury/.test(ua) ); } // Создание кнопки установки createInstallButton() { const installButton = document.createElement("button"); // Разный текст для разных браузеров if (this.isMobileSafari()) { installButton.textContent = "📱 Добавить на главный экран"; } else { installButton.textContent = "📱 Установить приложение"; } installButton.className = "btnSave"; installButton.style.marginTop = "10px"; installButton.style.width = "100%"; installButton.style.fontSize = "14px"; installButton.id = "pwa-install-button"; installButton.addEventListener("click", () => { this.installApp(); }); return installButton; } // Добавление кнопки на страницу addInstallButtonToPage(installButton) { // Удаляем существующую кнопку, если есть const existingButton = document.getElementById("pwa-install-button"); if (existingButton) { existingButton.remove(); } // Ищем подходящее место для кнопки const authLink = document.querySelector(".auth-link"); const footer = document.querySelector(".footer"); const container = document.querySelector(".container"); if (authLink) { authLink.appendChild(installButton); console.log("Кнопка установки добавлена в auth-link"); } else if (footer) { footer.insertBefore(installButton, footer.firstChild); console.log("Кнопка установки добавлена в footer"); } else if (container) { container.appendChild(installButton); console.log("Кнопка установки добавлена в container"); } else { document.body.appendChild(installButton); console.log("Кнопка установки добавлена в body"); } } // Установка приложения installApp() { console.log("Попытка установки приложения"); if (this.isMobileSafari()) { // Для iOS Safari показываем инструкции this.showSafariInstructions(); return; } if (this.deferredPrompt) { // На мобильных устройствах вызываем prompt() для показа нативного баннера if (this.isMobileDevice()) { console.log( "Показываем нативный баннер установки на мобильном устройстве" ); this.deferredPrompt.prompt(); this.deferredPrompt.userChoice.then((choiceResult) => { console.log("Результат установки:", choiceResult.outcome); if (choiceResult.outcome === "accepted") { console.log("Пользователь установил приложение"); this.showNotification("Приложение успешно установлено!", "success"); } else { console.log("Пользователь отклонил установку"); this.showNotification("Установка отменена", "warning"); } this.deferredPrompt = null; }); } else { // На десктопе используем обычную логику this.deferredPrompt.prompt(); this.deferredPrompt.userChoice.then((choiceResult) => { console.log("Результат установки:", choiceResult.outcome); if (choiceResult.outcome === "accepted") { console.log("Пользователь установил приложение"); this.showNotification("Приложение успешно установлено!", "success"); } else { console.log("Пользователь отклонил установку"); this.showNotification("Установка отменена", "warning"); } this.deferredPrompt = null; this.removeInstallButton(); }); } } else { console.log("deferredPrompt не доступен"); this.showManualInstallInstructions(); } } // Принудительная проверка возможности установки async checkInstallability() { if (!this.isMobileDevice()) { return false; } // Проверяем все требования PWA const requirements = { hasManifest: document.querySelector('link[rel="manifest"]') !== null, hasServiceWorker: "serviceWorker" in navigator, isSecure: location.protocol === "https:" || location.hostname === "localhost", hasIcons: document.querySelector('link[rel="icon"]') !== null, }; const allRequirementsMet = Object.values(requirements).every((req) => req); if (!allRequirementsMet) { console.log("Не все требования PWA выполнены:", requirements); return false; } // Проверяем, есть ли deferredPrompt if (this.deferredPrompt) { return true; } // Для мобильных устройств без deferredPrompt показываем инструкции if (this.isMobileDevice()) { this.showMobileInstallInstructions(); return true; } return false; } // Показать инструкции для Safari showSafariInstructions() { const instructions = ` Для установки приложения на iOS: 1. Нажмите кнопку "Поделиться" (□↗) внизу экрана 2. Выберите "На экран Домой" 3. Нажмите "Добавить" `; alert(instructions); } // Показать инструкции для ручной установки showManualInstallInstructions() { const instructions = ` Для установки приложения: 1. Откройте меню браузера (⋮ или ☰) 2. Найдите "Установить приложение" или "Добавить на главный экран" 3. Следуйте инструкциям браузера `; alert(instructions); } // Показать инструкции для мобильных устройств showMobileInstallInstructions() { const isAndroid = /Android/i.test(navigator.userAgent); const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); let instructions = ""; if (isAndroid) { instructions = ` Для установки приложения на Android: 1. Нажмите на меню браузера (⋮) 2. Выберите "Установить приложение" или "Добавить на главный экран" 3. Подтвердите установку Или нажмите на иконку установки в адресной строке, если она появилась. `; } else if (isIOS) { instructions = ` Для установки приложения на iOS: 1. Нажмите кнопку "Поделиться" (□↗) внизу экрана 2. Выберите "На экран Домой" 3. Нажмите "Добавить" `; } else { instructions = ` Для установки приложения: 1. Откройте меню браузера 2. Найдите "Установить приложение" или "Добавить на главный экран" 3. Следуйте инструкциям браузера `; } alert(instructions); } // Удаление кнопки установки removeInstallButton() { const installButton = document.getElementById("pwa-install-button"); if (installButton) { installButton.remove(); console.log("Кнопка установки удалена"); } } // Обработка успешной установки setupAppInstalled() { window.addEventListener("appinstalled", () => { console.log("PWA установлено успешно"); this.removeInstallButton(); }); } // Проверка статуса PWA isPWAInstalled() { return ( window.matchMedia("(display-mode: standalone)").matches || window.navigator.standalone === true || document.referrer.includes("android-app://") || window.matchMedia("(display-mode: fullscreen)").matches ); } // Получение информации о PWA getPWAInfo() { return { isInstalled: this.isPWAInstalled(), isOnline: navigator.onLine, hasServiceWorker: "serviceWorker" in navigator, userAgent: navigator.userAgent, hasDeferredPrompt: this.deferredPrompt !== null, isMobileDevice: this.isMobileDevice(), isMobileSafari: this.isMobileSafari(), platform: navigator.platform, language: navigator.language, displayMode: window.matchMedia("(display-mode: standalone)").matches ? "standalone" : "browser", }; } // Принудительное обновление кэша async forceUpdateCache() { console.log("Принудительное обновление кэша..."); if ("serviceWorker" in navigator && navigator.serviceWorker.controller) { try { // Отправляем сообщение Service Worker для обновления кэша navigator.serviceWorker.controller.postMessage({ type: "FORCE_UPDATE_CACHE", }); console.log("Запрос на обновление кэша отправлен"); return true; } catch (error) { console.error("Ошибка при обновлении кэша:", error); return false; } } else { console.log("Service Worker не доступен"); return false; } } // Полная очистка кэша async clearAllCache() { console.log("Полная очистка кэша..."); if ("serviceWorker" in navigator && navigator.serviceWorker.controller) { try { // Отправляем сообщение Service Worker для очистки кэша navigator.serviceWorker.controller.postMessage({ type: "CLEAR_ALL_CACHE", }); console.log("Запрос на очистку кэша отправлен"); return true; } catch (error) { console.error("Ошибка при очистке кэша:", error); return false; } } else { console.log("Service Worker не доступен"); return false; } } // Получение версии кэша async getCacheVersion() { return new Promise((resolve) => { if ("serviceWorker" in navigator && navigator.serviceWorker.controller) { const messageChannel = new MessageChannel(); messageChannel.port1.onmessage = (event) => { resolve(event.data.version || "Неизвестно"); }; navigator.serviceWorker.controller.postMessage( { type: "GET_VERSION" }, [messageChannel.port2] ); } else { resolve("Service Worker не доступен"); } }); } // Проверка обновлений и принудительное обновление async checkForUpdates() { console.log("Проверка обновлений..."); if ("serviceWorker" in navigator) { try { const registration = await navigator.serviceWorker.getRegistration(); if (registration) { await registration.update(); console.log("Проверка обновлений завершена"); return true; } } catch (error) { console.error("Ошибка при проверке обновлений:", error); return false; } } return false; } // Настройка обработки сообщений от Service Worker setupServiceWorkerMessages() { if ("serviceWorker" in navigator) { navigator.serviceWorker.addEventListener("message", (event) => { console.log("Получено сообщение от SW:", event.data); switch (event.data.type) { case "CACHE_UPDATED": console.log("Кэш обновлен до версии:", event.data.version); this.showNotification("Кэш успешно обновлен!", "success"); break; case "CACHE_CLEARED": console.log("Кэш полностью очищен"); this.showNotification("Кэш полностью очищен!", "info"); break; } }); } } // Показ уведомления showNotification(message, type = "info") { // Создаем уведомление const notification = document.createElement("div"); notification.className = `pwa-notification pwa-notification-${type}`; notification.textContent = message; // Стили для уведомления notification.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 12px 20px; border-radius: 8px; color: white; font-weight: bold; z-index: 10000; max-width: 300px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); transform: translateX(100%); transition: transform 0.3s ease; `; // Цвета для разных типов уведомлений switch (type) { case "success": notification.style.backgroundColor = "#28a745"; break; case "error": notification.style.backgroundColor = "#dc3545"; break; case "warning": notification.style.backgroundColor = "#ffc107"; notification.style.color = "#000"; break; default: notification.style.backgroundColor = "#007bff"; } // Добавляем на страницу document.body.appendChild(notification); // Анимация появления setTimeout(() => { notification.style.transform = "translateX(0)"; }, 100); // Автоматическое удаление через 3 секунды setTimeout(() => { notification.style.transform = "translateX(100%)"; setTimeout(() => { if (notification.parentNode) { notification.parentNode.removeChild(notification); } }, 300); }, 3000); } } // Инициализация PWA Manager const pwaManager = new PWAManager(); // Экспорт для использования в других скриптах window.PWAManager = pwaManager; // Добавляем глобальные функции для управления кэшем window.debugPWA = () => { console.log("PWA Debug Info:", pwaManager.getPWAInfo()); }; // Принудительное обновление кэша window.updateCache = () => { return pwaManager.forceUpdateCache(); }; // Полная очистка кэша window.clearCache = () => { return pwaManager.clearAllCache(); }; // Получение версии кэша window.getCacheVersion = () => { return pwaManager.getCacheVersion(); }; // Проверка обновлений window.checkUpdates = () => { return pwaManager.checkForUpdates(); }; // Комбинированная функция: проверка обновлений + принудительное обновление кэша window.forceUpdate = async () => { console.log("Принудительное обновление приложения..."); // Сначала проверяем обновления await pwaManager.checkForUpdates(); // Затем принудительно обновляем кэш await pwaManager.forceUpdateCache(); // Перезагружаем страницу setTimeout(() => { window.location.reload(); }, 1000); }; // Проверка возможности установки PWA window.checkInstallability = () => { return pwaManager.checkInstallability(); }; // Принудительная попытка установки window.forceInstall = () => { if (pwaManager.isMobileDevice()) { // На мобильных устройствах пытаемся вызвать prompt() если есть deferredPrompt if (pwaManager.deferredPrompt) { pwaManager.installApp(); } else { pwaManager.showMobileInstallInstructions(); } } else { pwaManager.showManualInstallInstructions(); } }; // Показать нативный баннер установки (для мобильных устройств) window.showInstallBanner = () => { if (pwaManager.deferredPrompt && pwaManager.isMobileDevice()) { pwaManager.deferredPrompt.prompt(); pwaManager.deferredPrompt.userChoice.then((choiceResult) => { console.log("Результат установки:", choiceResult.outcome); if (choiceResult.outcome === "accepted") { pwaManager.showNotification( "Приложение успешно установлено!", "success" ); } else { pwaManager.showNotification("Установка отменена", "warning"); } pwaManager.deferredPrompt = null; }); } else { console.log("Нативный баннер установки недоступен"); pwaManager.showMobileInstallInstructions(); } };