✨ Обновлены мета-теги и улучшена функциональность PWA
- Оптимизированы мета-теги в index.html и test-pwa.html для лучшей поддержки PWA. - Улучшена структура кода с использованием многострочных атрибутов для мета-тегов. - Обновлен сервисный работник для более эффективного кэширования и обработки запросов. - Добавлены новые функции в pwa.js для управления установкой и обновлением PWA.
This commit is contained in:
parent
efc3c4c777
commit
091fc6cc1e
@ -4,13 +4,19 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Вход в систему заметок</title>
|
||||
|
||||
|
||||
<!-- PWA Meta Tags -->
|
||||
<meta name="description" content="NoteJS - современная система заметок с поддержкой Markdown, изображений, тегов и календаря" />
|
||||
<meta
|
||||
name="description"
|
||||
content="NoteJS - современная система заметок с поддержкой Markdown, изображений, тегов и календаря"
|
||||
/>
|
||||
<meta name="theme-color" content="#007bff" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-title" content="NoteJS" />
|
||||
<meta name="apple-touch-fullscreen" content="yes" />
|
||||
<meta name="msapplication-TileColor" content="#007bff" />
|
||||
@ -18,28 +24,58 @@
|
||||
<meta name="msapplication-TileImage" content="/icons/icon-144x144.png" />
|
||||
<meta name="application-name" content="NoteJS" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
|
||||
|
||||
<!-- Icons -->
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/icons/icon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/icons/icon-16x16.png" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/icons/icon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/icons/icon-16x16.png"
|
||||
/>
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/icons/icon-48x48.png" />
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/icons/icon-48x48.png" />
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/icons/icon-72x72.png" />
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/icons/icon-72x72.png" />
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/icons/icon-128x128.png" />
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/icons/icon-128x128.png" />
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/icons/icon-144x144.png" />
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/icons/icon-152x152.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-192x192.png" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="114x114"
|
||||
href="/icons/icon-128x128.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="120x120"
|
||||
href="/icons/icon-128x128.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="144x144"
|
||||
href="/icons/icon-144x144.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="152x152"
|
||||
href="/icons/icon-152x152.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/icons/icon-192x192.png"
|
||||
/>
|
||||
<link rel="mask-icon" href="/icon.svg" color="#007bff" />
|
||||
|
||||
|
||||
<!-- Manifest -->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
|
||||
<!-- Styles -->
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js"></script>
|
||||
</head>
|
||||
@ -86,23 +122,29 @@
|
||||
<p>Создатель: <span>Fovway</span></p>
|
||||
</div>
|
||||
<script src="/login.js"></script>
|
||||
|
||||
|
||||
<!-- PWA Service Worker Registration -->
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", () => {
|
||||
navigator.serviceWorker
|
||||
.register("/sw.js")
|
||||
.then((registration) => {
|
||||
console.log('SW зарегистрирован успешно:', registration.scope);
|
||||
|
||||
console.log("SW зарегистрирован успешно:", registration.scope);
|
||||
|
||||
// Проверяем обновления
|
||||
registration.addEventListener('updatefound', () => {
|
||||
registration.addEventListener("updatefound", () => {
|
||||
const newWorker = registration.installing;
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||
newWorker.addEventListener("statechange", () => {
|
||||
if (
|
||||
newWorker.state === "installed" &&
|
||||
navigator.serviceWorker.controller
|
||||
) {
|
||||
// Новый контент доступен, можно показать уведомление
|
||||
if (confirm('Доступна новая версия приложения. Обновить?')) {
|
||||
newWorker.postMessage({ type: 'SKIP_WAITING' });
|
||||
if (
|
||||
confirm("Доступна новая версия приложения. Обновить?")
|
||||
) {
|
||||
newWorker.postMessage({ type: "SKIP_WAITING" });
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
@ -110,48 +152,53 @@
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('Ошибка регистрации SW:', error);
|
||||
console.log("Ошибка регистрации SW:", error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Обработка установки PWA
|
||||
let deferredPrompt;
|
||||
window.addEventListener('beforeinstallprompt', (e) => {
|
||||
e.preventDefault();
|
||||
window.addEventListener("beforeinstallprompt", (e) => {
|
||||
// На мобильных устройствах позволяем браузеру показать нативный баннер
|
||||
const isMobile =
|
||||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
navigator.userAgent
|
||||
) ||
|
||||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2) ||
|
||||
window.matchMedia("(max-width: 768px)").matches;
|
||||
|
||||
if (!isMobile) {
|
||||
e.preventDefault();
|
||||
}
|
||||
deferredPrompt = e;
|
||||
|
||||
// Проверяем, является ли устройство мобильным
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
|
||||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2) ||
|
||||
window.matchMedia('(max-width: 768px)').matches;
|
||||
|
||||
|
||||
// Показываем кнопку установки только на мобильных устройствах
|
||||
if (isMobile) {
|
||||
const installButton = document.createElement('button');
|
||||
installButton.textContent = 'Установить приложение';
|
||||
installButton.className = 'btnSave';
|
||||
installButton.style.marginTop = '10px';
|
||||
installButton.style.width = '100%';
|
||||
|
||||
installButton.addEventListener('click', () => {
|
||||
const installButton = document.createElement("button");
|
||||
installButton.textContent = "Установить приложение";
|
||||
installButton.className = "btnSave";
|
||||
installButton.style.marginTop = "10px";
|
||||
installButton.style.width = "100%";
|
||||
|
||||
installButton.addEventListener("click", () => {
|
||||
deferredPrompt.prompt();
|
||||
deferredPrompt.userChoice.then((choiceResult) => {
|
||||
if (choiceResult.outcome === 'accepted') {
|
||||
console.log('Пользователь установил приложение');
|
||||
if (choiceResult.outcome === "accepted") {
|
||||
console.log("Пользователь установил приложение");
|
||||
}
|
||||
deferredPrompt = null;
|
||||
installButton.remove();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector('.auth-link').appendChild(installButton);
|
||||
|
||||
document.querySelector(".auth-link").appendChild(installButton);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Обработка успешной установки
|
||||
window.addEventListener('appinstalled', () => {
|
||||
console.log('PWA установлено успешно');
|
||||
window.addEventListener("appinstalled", () => {
|
||||
console.log("PWA установлено успешно");
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
230
public/mobile-install-test.html
Normal file
230
public/mobile-install-test.html
Normal file
@ -0,0 +1,230 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Тест установки PWA на мобильном</title>
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="icon" href="/icon.svg" />
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.btn {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin: 10px 5px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.btn:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
.btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.info {
|
||||
background: #e7f3ff;
|
||||
border: 1px solid #b3d9ff;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
.success {
|
||||
background: #d4edda;
|
||||
border-color: #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
.warning {
|
||||
background: #fff3cd;
|
||||
border-color: #ffeaa7;
|
||||
color: #856404;
|
||||
}
|
||||
.error {
|
||||
background: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
.status {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>📱 Тест установки PWA на мобильном</h1>
|
||||
|
||||
<div class="info">
|
||||
<h3>Инструкции:</h3>
|
||||
<p>1. Откройте эту страницу на мобильном устройстве</p>
|
||||
<p>2. Должен появиться нативный баннер установки браузера</p>
|
||||
<p>3. Если баннер не появился, используйте кнопки ниже</p>
|
||||
</div>
|
||||
|
||||
<div id="status" class="status"></div>
|
||||
|
||||
<div>
|
||||
<button id="install-btn" class="btn" disabled>
|
||||
Установить приложение
|
||||
</button>
|
||||
<button id="show-banner-btn" class="btn">Показать баннер</button>
|
||||
<button id="check-status-btn" class="btn">Проверить статус</button>
|
||||
</div>
|
||||
|
||||
<div id="debug-info" class="info" style="margin-top: 20px">
|
||||
<h3>Отладочная информация:</h3>
|
||||
<div id="debug-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let deferredPrompt;
|
||||
const statusDiv = document.getElementById("status");
|
||||
const installBtn = document.getElementById("install-btn");
|
||||
const showBannerBtn = document.getElementById("show-banner-btn");
|
||||
const checkStatusBtn = document.getElementById("check-status-btn");
|
||||
const debugContent = document.getElementById("debug-content");
|
||||
|
||||
function updateStatus(message, type = "info") {
|
||||
statusDiv.textContent = message;
|
||||
statusDiv.className = `status ${type}`;
|
||||
}
|
||||
|
||||
function updateDebugInfo() {
|
||||
const info = {
|
||||
userAgent: navigator.userAgent,
|
||||
isMobile:
|
||||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
navigator.userAgent
|
||||
),
|
||||
hasDeferredPrompt: !!deferredPrompt,
|
||||
isPWAInstalled: window.matchMedia("(display-mode: standalone)")
|
||||
.matches,
|
||||
isOnline: navigator.onLine,
|
||||
hasServiceWorker: "serviceWorker" in navigator,
|
||||
};
|
||||
|
||||
debugContent.innerHTML = Object.entries(info)
|
||||
.map(([key, value]) => `<strong>${key}:</strong> ${value}`)
|
||||
.join("<br>");
|
||||
}
|
||||
|
||||
// Обработка события beforeinstallprompt
|
||||
window.addEventListener("beforeinstallprompt", (e) => {
|
||||
console.log("beforeinstallprompt событие получено");
|
||||
|
||||
// На мобильных устройствах позволяем браузеру показать нативный баннер
|
||||
const isMobile =
|
||||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
navigator.userAgent
|
||||
) ||
|
||||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2) ||
|
||||
window.matchMedia("(max-width: 768px)").matches;
|
||||
|
||||
if (!isMobile) {
|
||||
e.preventDefault();
|
||||
updateStatus(
|
||||
"Десктоп: preventDefault() вызван, показываем кастомную кнопку",
|
||||
"warning"
|
||||
);
|
||||
} else {
|
||||
updateStatus(
|
||||
"Мобильное устройство: разрешаем нативный баннер установки",
|
||||
"success"
|
||||
);
|
||||
}
|
||||
|
||||
deferredPrompt = e;
|
||||
installBtn.disabled = false;
|
||||
updateDebugInfo();
|
||||
});
|
||||
|
||||
// Обработка успешной установки
|
||||
window.addEventListener("appinstalled", () => {
|
||||
console.log("PWA установлено успешно");
|
||||
updateStatus("Приложение установлено успешно!", "success");
|
||||
installBtn.disabled = true;
|
||||
deferredPrompt = null;
|
||||
updateDebugInfo();
|
||||
});
|
||||
|
||||
// Кнопка установки
|
||||
installBtn.addEventListener("click", async () => {
|
||||
if (deferredPrompt) {
|
||||
updateStatus("Показываем баннер установки...", "info");
|
||||
deferredPrompt.prompt();
|
||||
const choiceResult = await deferredPrompt.userChoice;
|
||||
|
||||
if (choiceResult.outcome === "accepted") {
|
||||
updateStatus("Пользователь установил приложение!", "success");
|
||||
} else {
|
||||
updateStatus("Пользователь отклонил установку", "warning");
|
||||
}
|
||||
|
||||
deferredPrompt = null;
|
||||
installBtn.disabled = true;
|
||||
} else {
|
||||
updateStatus("Баннер установки недоступен", "error");
|
||||
}
|
||||
updateDebugInfo();
|
||||
});
|
||||
|
||||
// Кнопка показа баннера
|
||||
showBannerBtn.addEventListener("click", () => {
|
||||
if (deferredPrompt) {
|
||||
updateStatus("Принудительно показываем баннер...", "info");
|
||||
deferredPrompt.prompt();
|
||||
} else {
|
||||
updateStatus(
|
||||
"Баннер установки недоступен. Попробуйте перезагрузить страницу.",
|
||||
"error"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Кнопка проверки статуса
|
||||
checkStatusBtn.addEventListener("click", () => {
|
||||
updateDebugInfo();
|
||||
if (deferredPrompt) {
|
||||
updateStatus("Баннер установки доступен", "success");
|
||||
} else {
|
||||
updateStatus("Баннер установки недоступен", "warning");
|
||||
}
|
||||
});
|
||||
|
||||
// Инициализация
|
||||
updateDebugInfo();
|
||||
updateStatus("Ожидание события beforeinstallprompt...", "info");
|
||||
|
||||
// Регистрация Service Worker
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker
|
||||
.register("/sw.js")
|
||||
.then((registration) => {
|
||||
console.log("SW зарегистрирован:", registration);
|
||||
updateStatus("Service Worker зарегистрирован", "success");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Ошибка регистрации SW:", error);
|
||||
updateStatus("Ошибка регистрации Service Worker", "error");
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
393
public/pwa.js
393
public/pwa.js
@ -6,7 +6,7 @@ class PWAManager {
|
||||
}
|
||||
|
||||
init() {
|
||||
console.log('PWA Manager инициализирован');
|
||||
console.log("PWA Manager инициализирован");
|
||||
this.registerServiceWorker();
|
||||
this.setupInstallPrompt();
|
||||
this.setupAppInstalled();
|
||||
@ -16,45 +16,60 @@ class PWAManager {
|
||||
|
||||
// Проверка требований 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);
|
||||
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')
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", () => {
|
||||
navigator.serviceWorker
|
||||
.register("/sw.js")
|
||||
.then((registration) => {
|
||||
console.log('SW зарегистрирован успешно:', registration.scope);
|
||||
|
||||
console.log("SW зарегистрирован успешно:", registration.scope);
|
||||
|
||||
// Проверяем обновления
|
||||
registration.addEventListener('updatefound', () => {
|
||||
registration.addEventListener("updatefound", () => {
|
||||
const newWorker = registration.installing;
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||
newWorker.addEventListener("statechange", () => {
|
||||
if (
|
||||
newWorker.state === "installed" &&
|
||||
navigator.serviceWorker.controller
|
||||
) {
|
||||
this.showUpdateNotification();
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('Ошибка регистрации SW:', error);
|
||||
console.log("Ошибка регистрации SW:", error);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
console.log('Service Worker не поддерживается');
|
||||
console.log("Service Worker не поддерживается");
|
||||
}
|
||||
}
|
||||
|
||||
// Показ уведомления об обновлении
|
||||
showUpdateNotification() {
|
||||
if (confirm('Доступна новая версия приложения. Обновить?')) {
|
||||
if (confirm("Доступна новая версия приложения. Обновить?")) {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
navigator.serviceWorker.controller.postMessage({ type: 'SKIP_WAITING' });
|
||||
navigator.serviceWorker.controller.postMessage({
|
||||
type: "SKIP_WAITING",
|
||||
});
|
||||
}
|
||||
window.location.reload();
|
||||
}
|
||||
@ -62,11 +77,23 @@ class PWAManager {
|
||||
|
||||
// Настройка промпта установки
|
||||
setupInstallPrompt() {
|
||||
window.addEventListener('beforeinstallprompt', (e) => {
|
||||
console.log('beforeinstallprompt событие получено');
|
||||
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();
|
||||
@ -76,23 +103,25 @@ class PWAManager {
|
||||
|
||||
// Показ кнопки установки
|
||||
showInstallButton() {
|
||||
console.log('Попытка показать кнопку установки');
|
||||
|
||||
console.log("Попытка показать кнопку установки");
|
||||
|
||||
// Проверяем, не установлено ли уже приложение
|
||||
if (this.isPWAInstalled()) {
|
||||
console.log('Приложение уже установлено');
|
||||
console.log("Приложение уже установлено");
|
||||
return;
|
||||
}
|
||||
|
||||
// Показываем кнопку только на мобильных устройствах
|
||||
if (!this.isMobileDevice()) {
|
||||
console.log('Кнопка установки скрыта для ПК версии');
|
||||
// Показываем кнопку только на десктопе (на мобильных используем нативный баннер)
|
||||
if (this.isMobileDevice()) {
|
||||
console.log(
|
||||
"Мобильное устройство - используем нативный баннер установки"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, поддерживает ли браузер установку PWA
|
||||
if (!this.deferredPrompt && !this.isMobileSafari()) {
|
||||
console.log('Установка PWA не поддерживается в этом браузере');
|
||||
if (!this.deferredPrompt) {
|
||||
console.log("Установка PWA не поддерживается в этом браузере");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -103,36 +132,44 @@ class PWAManager {
|
||||
// Проверка на мобильное устройство
|
||||
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);
|
||||
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);
|
||||
return (
|
||||
/iPad|iPhone|iPod/.test(ua) &&
|
||||
/Safari/.test(ua) &&
|
||||
!/CriOS|FxiOS|OPiOS|mercury/.test(ua)
|
||||
);
|
||||
}
|
||||
|
||||
// Создание кнопки установки
|
||||
createInstallButton() {
|
||||
const installButton = document.createElement('button');
|
||||
|
||||
const installButton = document.createElement("button");
|
||||
|
||||
// Разный текст для разных браузеров
|
||||
if (this.isMobileSafari()) {
|
||||
installButton.textContent = '📱 Добавить на главный экран';
|
||||
installButton.textContent = "📱 Добавить на главный экран";
|
||||
} else {
|
||||
installButton.textContent = '📱 Установить приложение';
|
||||
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', () => {
|
||||
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();
|
||||
});
|
||||
|
||||
@ -142,57 +179,77 @@ class PWAManager {
|
||||
// Добавление кнопки на страницу
|
||||
addInstallButtonToPage(installButton) {
|
||||
// Удаляем существующую кнопку, если есть
|
||||
const existingButton = document.getElementById('pwa-install-button');
|
||||
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');
|
||||
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');
|
||||
console.log("Кнопка установки добавлена в auth-link");
|
||||
} else if (footer) {
|
||||
footer.insertBefore(installButton, footer.firstChild);
|
||||
console.log('Кнопка установки добавлена в footer');
|
||||
console.log("Кнопка установки добавлена в footer");
|
||||
} else if (container) {
|
||||
container.appendChild(installButton);
|
||||
console.log('Кнопка установки добавлена в container');
|
||||
console.log("Кнопка установки добавлена в container");
|
||||
} else {
|
||||
document.body.appendChild(installButton);
|
||||
console.log('Кнопка установки добавлена в body');
|
||||
console.log("Кнопка установки добавлена в body");
|
||||
}
|
||||
}
|
||||
|
||||
// Установка приложения
|
||||
installApp() {
|
||||
console.log('Попытка установки приложения');
|
||||
|
||||
console.log("Попытка установки приложения");
|
||||
|
||||
if (this.isMobileSafari()) {
|
||||
// Для iOS Safari показываем инструкции
|
||||
this.showSafariInstructions();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (this.deferredPrompt) {
|
||||
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();
|
||||
});
|
||||
// На мобильных устройствах вызываем 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 не доступен');
|
||||
console.log("deferredPrompt не доступен");
|
||||
this.showManualInstallInstructions();
|
||||
}
|
||||
}
|
||||
@ -206,15 +263,16 @@ class PWAManager {
|
||||
// Проверяем все требования 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
|
||||
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);
|
||||
|
||||
const allRequirementsMet = Object.values(requirements).every((req) => req);
|
||||
|
||||
if (!allRequirementsMet) {
|
||||
console.log('Не все требования PWA выполнены:', requirements);
|
||||
console.log("Не все требования PWA выполнены:", requirements);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -258,9 +316,9 @@ class PWAManager {
|
||||
showMobileInstallInstructions() {
|
||||
const isAndroid = /Android/i.test(navigator.userAgent);
|
||||
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||
|
||||
let instructions = '';
|
||||
|
||||
|
||||
let instructions = "";
|
||||
|
||||
if (isAndroid) {
|
||||
instructions = `
|
||||
Для установки приложения на Android:
|
||||
@ -285,33 +343,35 @@ class PWAManager {
|
||||
3. Следуйте инструкциям браузера
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
alert(instructions);
|
||||
}
|
||||
|
||||
// Удаление кнопки установки
|
||||
removeInstallButton() {
|
||||
const installButton = document.getElementById('pwa-install-button');
|
||||
const installButton = document.getElementById("pwa-install-button");
|
||||
if (installButton) {
|
||||
installButton.remove();
|
||||
console.log('Кнопка установки удалена');
|
||||
console.log("Кнопка установки удалена");
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка успешной установки
|
||||
setupAppInstalled() {
|
||||
window.addEventListener('appinstalled', () => {
|
||||
console.log('PWA установлено успешно');
|
||||
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;
|
||||
return (
|
||||
window.matchMedia("(display-mode: standalone)").matches ||
|
||||
window.navigator.standalone === true ||
|
||||
document.referrer.includes("android-app://") ||
|
||||
window.matchMedia("(display-mode: fullscreen)").matches
|
||||
);
|
||||
}
|
||||
|
||||
// Получение информации о PWA
|
||||
@ -319,59 +379,61 @@ class PWAManager {
|
||||
return {
|
||||
isInstalled: this.isPWAInstalled(),
|
||||
isOnline: navigator.onLine,
|
||||
hasServiceWorker: 'serviceWorker' in navigator,
|
||||
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'
|
||||
displayMode: window.matchMedia("(display-mode: standalone)").matches
|
||||
? "standalone"
|
||||
: "browser",
|
||||
};
|
||||
}
|
||||
|
||||
// Принудительное обновление кэша
|
||||
async forceUpdateCache() {
|
||||
console.log('Принудительное обновление кэша...');
|
||||
|
||||
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
|
||||
console.log("Принудительное обновление кэша...");
|
||||
|
||||
if ("serviceWorker" in navigator && navigator.serviceWorker.controller) {
|
||||
try {
|
||||
// Отправляем сообщение Service Worker для обновления кэша
|
||||
navigator.serviceWorker.controller.postMessage({
|
||||
type: 'FORCE_UPDATE_CACHE'
|
||||
type: "FORCE_UPDATE_CACHE",
|
||||
});
|
||||
|
||||
console.log('Запрос на обновление кэша отправлен');
|
||||
|
||||
console.log("Запрос на обновление кэша отправлен");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при обновлении кэша:', error);
|
||||
console.error("Ошибка при обновлении кэша:", error);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
console.log('Service Worker не доступен');
|
||||
console.log("Service Worker не доступен");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Полная очистка кэша
|
||||
async clearAllCache() {
|
||||
console.log('Полная очистка кэша...');
|
||||
|
||||
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
|
||||
console.log("Полная очистка кэша...");
|
||||
|
||||
if ("serviceWorker" in navigator && navigator.serviceWorker.controller) {
|
||||
try {
|
||||
// Отправляем сообщение Service Worker для очистки кэша
|
||||
navigator.serviceWorker.controller.postMessage({
|
||||
type: 'CLEAR_ALL_CACHE'
|
||||
type: "CLEAR_ALL_CACHE",
|
||||
});
|
||||
|
||||
console.log('Запрос на очистку кэша отправлен');
|
||||
|
||||
console.log("Запрос на очистку кэша отправлен");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при очистке кэша:', error);
|
||||
console.error("Ошибка при очистке кэша:", error);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
console.log('Service Worker не доступен');
|
||||
console.log("Service Worker не доступен");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -379,37 +441,37 @@ class PWAManager {
|
||||
// Получение версии кэша
|
||||
async getCacheVersion() {
|
||||
return new Promise((resolve) => {
|
||||
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
|
||||
if ("serviceWorker" in navigator && navigator.serviceWorker.controller) {
|
||||
const messageChannel = new MessageChannel();
|
||||
|
||||
|
||||
messageChannel.port1.onmessage = (event) => {
|
||||
resolve(event.data.version || 'Неизвестно');
|
||||
resolve(event.data.version || "Неизвестно");
|
||||
};
|
||||
|
||||
|
||||
navigator.serviceWorker.controller.postMessage(
|
||||
{ type: 'GET_VERSION' },
|
||||
{ type: "GET_VERSION" },
|
||||
[messageChannel.port2]
|
||||
);
|
||||
} else {
|
||||
resolve('Service Worker не доступен');
|
||||
resolve("Service Worker не доступен");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Проверка обновлений и принудительное обновление
|
||||
async checkForUpdates() {
|
||||
console.log('Проверка обновлений...');
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
console.log("Проверка обновлений...");
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.getRegistration();
|
||||
if (registration) {
|
||||
await registration.update();
|
||||
console.log('Проверка обновлений завершена');
|
||||
console.log("Проверка обновлений завершена");
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при проверке обновлений:', error);
|
||||
console.error("Ошибка при проверке обновлений:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -418,19 +480,19 @@ class PWAManager {
|
||||
|
||||
// Настройка обработки сообщений от Service Worker
|
||||
setupServiceWorkerMessages() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.addEventListener('message', (event) => {
|
||||
console.log('Получено сообщение от SW:', event.data);
|
||||
|
||||
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');
|
||||
case "CACHE_UPDATED":
|
||||
console.log("Кэш обновлен до версии:", event.data.version);
|
||||
this.showNotification("Кэш успешно обновлен!", "success");
|
||||
break;
|
||||
|
||||
case 'CACHE_CLEARED':
|
||||
console.log('Кэш полностью очищен');
|
||||
this.showNotification('Кэш полностью очищен!', 'info');
|
||||
|
||||
case "CACHE_CLEARED":
|
||||
console.log("Кэш полностью очищен");
|
||||
this.showNotification("Кэш полностью очищен!", "info");
|
||||
break;
|
||||
}
|
||||
});
|
||||
@ -438,12 +500,12 @@ class PWAManager {
|
||||
}
|
||||
|
||||
// Показ уведомления
|
||||
showNotification(message, type = 'info') {
|
||||
showNotification(message, type = "info") {
|
||||
// Создаем уведомление
|
||||
const notification = document.createElement('div');
|
||||
const notification = document.createElement("div");
|
||||
notification.className = `pwa-notification pwa-notification-${type}`;
|
||||
notification.textContent = message;
|
||||
|
||||
|
||||
// Стили для уведомления
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
@ -459,34 +521,34 @@ class PWAManager {
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
`;
|
||||
|
||||
|
||||
// Цвета для разных типов уведомлений
|
||||
switch (type) {
|
||||
case 'success':
|
||||
notification.style.backgroundColor = '#28a745';
|
||||
case "success":
|
||||
notification.style.backgroundColor = "#28a745";
|
||||
break;
|
||||
case 'error':
|
||||
notification.style.backgroundColor = '#dc3545';
|
||||
case "error":
|
||||
notification.style.backgroundColor = "#dc3545";
|
||||
break;
|
||||
case 'warning':
|
||||
notification.style.backgroundColor = '#ffc107';
|
||||
notification.style.color = '#000';
|
||||
case "warning":
|
||||
notification.style.backgroundColor = "#ffc107";
|
||||
notification.style.color = "#000";
|
||||
break;
|
||||
default:
|
||||
notification.style.backgroundColor = '#007bff';
|
||||
notification.style.backgroundColor = "#007bff";
|
||||
}
|
||||
|
||||
|
||||
// Добавляем на страницу
|
||||
document.body.appendChild(notification);
|
||||
|
||||
|
||||
// Анимация появления
|
||||
setTimeout(() => {
|
||||
notification.style.transform = 'translateX(0)';
|
||||
notification.style.transform = "translateX(0)";
|
||||
}, 100);
|
||||
|
||||
|
||||
// Автоматическое удаление через 3 секунды
|
||||
setTimeout(() => {
|
||||
notification.style.transform = 'translateX(100%)';
|
||||
notification.style.transform = "translateX(100%)";
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.parentNode.removeChild(notification);
|
||||
@ -504,7 +566,7 @@ window.PWAManager = pwaManager;
|
||||
|
||||
// Добавляем глобальные функции для управления кэшем
|
||||
window.debugPWA = () => {
|
||||
console.log('PWA Debug Info:', pwaManager.getPWAInfo());
|
||||
console.log("PWA Debug Info:", pwaManager.getPWAInfo());
|
||||
};
|
||||
|
||||
// Принудительное обновление кэша
|
||||
@ -529,14 +591,14 @@ window.checkUpdates = () => {
|
||||
|
||||
// Комбинированная функция: проверка обновлений + принудительное обновление кэша
|
||||
window.forceUpdate = async () => {
|
||||
console.log('Принудительное обновление приложения...');
|
||||
|
||||
console.log("Принудительное обновление приложения...");
|
||||
|
||||
// Сначала проверяем обновления
|
||||
await pwaManager.checkForUpdates();
|
||||
|
||||
|
||||
// Затем принудительно обновляем кэш
|
||||
await pwaManager.forceUpdateCache();
|
||||
|
||||
|
||||
// Перезагружаем страницу
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
@ -551,8 +613,35 @@ window.checkInstallability = () => {
|
||||
// Принудительная попытка установки
|
||||
window.forceInstall = () => {
|
||||
if (pwaManager.isMobileDevice()) {
|
||||
pwaManager.showMobileInstallInstructions();
|
||||
// На мобильных устройствах пытаемся вызвать 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();
|
||||
}
|
||||
};
|
||||
|
||||
228
public/sw.js
228
public/sw.js
@ -1,42 +1,43 @@
|
||||
// Service Worker для NoteJS
|
||||
const APP_VERSION = '1.0.3';
|
||||
const APP_VERSION = "1.0.5";
|
||||
const CACHE_NAME = `notejs-v${APP_VERSION}`;
|
||||
const STATIC_CACHE_NAME = `notejs-static-v${APP_VERSION}`;
|
||||
|
||||
// Файлы для кэширования при установке
|
||||
// Файлы для кэширования при установке (только изображения, иконки, логотипы)
|
||||
// Манифест убран - не нужен для офлайн работы
|
||||
const STATIC_FILES = [
|
||||
'/',
|
||||
'/index.html',
|
||||
'/style.css',
|
||||
'/manifest.json',
|
||||
'/icons/icon-72x72.png',
|
||||
'/icons/icon-96x96.png',
|
||||
'/icons/icon-128x128.png',
|
||||
'/icons/icon-144x144.png',
|
||||
'/icons/icon-152x152.png',
|
||||
'/icons/icon-192x192.png',
|
||||
'/icons/icon-384x384.png',
|
||||
'/icons/icon-512x512.png',
|
||||
'/icon.svg',
|
||||
'/logo.svg'
|
||||
"/icons/icon-72x72.png",
|
||||
"/icons/icon-96x96.png",
|
||||
"/icons/icon-128x128.png",
|
||||
"/icons/icon-144x144.png",
|
||||
"/icons/icon-152x152.png",
|
||||
"/icons/icon-192x192.png",
|
||||
"/icons/icon-384x384.png",
|
||||
"/icons/icon-512x512.png",
|
||||
"/icons/icon-32x32.png",
|
||||
"/icons/icon-16x16.png",
|
||||
"/icons/icon-48x48.png",
|
||||
"/icon.svg",
|
||||
"/logo.svg",
|
||||
];
|
||||
|
||||
// Установка Service Worker
|
||||
self.addEventListener('install', (event) => {
|
||||
console.log('[SW] Установка Service Worker');
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
console.log("[SW] Установка Service Worker");
|
||||
|
||||
event.waitUntil(
|
||||
caches.open(STATIC_CACHE_NAME)
|
||||
caches
|
||||
.open(STATIC_CACHE_NAME)
|
||||
.then((cache) => {
|
||||
console.log('[SW] Кэширование статических файлов');
|
||||
console.log("[SW] Кэширование статических файлов");
|
||||
return cache.addAll(STATIC_FILES);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('[SW] Статические файлы закэшированы');
|
||||
console.log("[SW] Статические файлы закэшированы");
|
||||
return self.skipWaiting();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[SW] Ошибка при кэшировании статических файлов:', error);
|
||||
console.error("[SW] Ошибка при кэшировании статических файлов:", error);
|
||||
// Продолжаем работу даже если кэширование не удалось
|
||||
return self.skipWaiting();
|
||||
})
|
||||
@ -44,51 +45,66 @@ self.addEventListener('install', (event) => {
|
||||
});
|
||||
|
||||
// Активация Service Worker
|
||||
self.addEventListener('activate', (event) => {
|
||||
console.log('[SW] Активация Service Worker');
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
console.log("[SW] Активация Service Worker");
|
||||
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
caches
|
||||
.keys()
|
||||
.then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => {
|
||||
// Удаляем старые кэши
|
||||
if (cacheName !== STATIC_CACHE_NAME) {
|
||||
console.log('[SW] Удаление старого кэша:', cacheName);
|
||||
console.log("[SW] Удаление старого кэша:", cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('[SW] Service Worker активирован');
|
||||
console.log("[SW] Service Worker активирован");
|
||||
return self.clients.claim();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Перехват запросов
|
||||
self.addEventListener('fetch', (event) => {
|
||||
self.addEventListener("fetch", (event) => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
|
||||
// Пропускаем запросы к API и загрузкам
|
||||
if (url.pathname.startsWith('/api/') ||
|
||||
url.pathname.startsWith('/uploads/') ||
|
||||
url.pathname.startsWith('/database/')) {
|
||||
if (
|
||||
url.pathname.startsWith("/api/") ||
|
||||
url.pathname.startsWith("/uploads/") ||
|
||||
url.pathname.startsWith("/database/")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Пропускаем запросы к внешним ресурсам
|
||||
if (url.origin !== location.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Обрабатываем только GET запросы
|
||||
if (request.method === 'GET') {
|
||||
event.respondWith(
|
||||
handleRequest(request)
|
||||
);
|
||||
|
||||
// Пропускаем HTML файлы и CSS - не кэшируем их
|
||||
if (
|
||||
url.pathname.endsWith(".html") ||
|
||||
url.pathname.endsWith(".css") ||
|
||||
url.pathname === "/" ||
|
||||
url.pathname === "/index.html" ||
|
||||
url.pathname === "/login.html" ||
|
||||
url.pathname === "/register.html" ||
|
||||
url.pathname === "/notes.html" ||
|
||||
url.pathname === "/profile.html"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Обрабатываем только GET запросы для изображений, иконок и манифеста
|
||||
if (request.method === "GET") {
|
||||
event.respondWith(handleRequest(request));
|
||||
}
|
||||
});
|
||||
|
||||
@ -96,134 +112,124 @@ async function handleRequest(request) {
|
||||
try {
|
||||
// Сначала пытаемся получить из кэша
|
||||
const cachedResponse = await caches.match(request);
|
||||
|
||||
|
||||
if (cachedResponse) {
|
||||
console.log('[SW] Запрос из кэша:', request.url);
|
||||
console.log("[SW] Запрос из кэша:", request.url);
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
|
||||
// Если нет в кэше, загружаем из сети
|
||||
console.log('[SW] Запрос к сети:', request.url);
|
||||
console.log("[SW] Запрос к сети:", request.url);
|
||||
const networkResponse = await fetch(request);
|
||||
|
||||
// Кэшируем успешные ответы
|
||||
if (networkResponse.ok) {
|
||||
|
||||
// Кэшируем только изображения и иконки (без манифеста)
|
||||
if (networkResponse.ok && shouldCache(request)) {
|
||||
const cache = await caches.open(STATIC_CACHE_NAME);
|
||||
cache.put(request, networkResponse.clone());
|
||||
}
|
||||
|
||||
|
||||
return networkResponse;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[SW] Ошибка при обработке запроса:', error);
|
||||
|
||||
// Fallback для HTML страниц
|
||||
if (request.headers.get('accept')?.includes('text/html')) {
|
||||
const fallbackResponse = await caches.match('/index.html');
|
||||
if (fallbackResponse) {
|
||||
return fallbackResponse;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback для иконок
|
||||
if (request.url.includes('/icons/')) {
|
||||
const fallbackIcon = await caches.match('/icons/icon-192x192.png');
|
||||
if (fallbackIcon) {
|
||||
return fallbackIcon;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback для CSS
|
||||
if (request.url.includes('/style.css')) {
|
||||
const fallbackCSS = await caches.match('/style.css');
|
||||
if (fallbackCSS) {
|
||||
return fallbackCSS;
|
||||
}
|
||||
}
|
||||
|
||||
console.error("[SW] Ошибка при обработке запроса:", error);
|
||||
|
||||
// НЕ предоставляем fallback - приложение работает только онлайн
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для определения, нужно ли кэшировать файл
|
||||
function shouldCache(request) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Кэшируем только изображения, иконки и логотипы (без манифеста для офлайн работы)
|
||||
return (
|
||||
url.pathname.includes("/icons/") ||
|
||||
url.pathname.endsWith(".png") ||
|
||||
url.pathname.endsWith(".jpg") ||
|
||||
url.pathname.endsWith(".jpeg") ||
|
||||
url.pathname.endsWith(".gif") ||
|
||||
url.pathname.endsWith(".webp") ||
|
||||
url.pathname.endsWith(".svg")
|
||||
);
|
||||
}
|
||||
|
||||
// Обработка сообщений от основного потока
|
||||
self.addEventListener('message', (event) => {
|
||||
console.log('[SW] Получено сообщение:', event.data);
|
||||
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.addEventListener("message", (event) => {
|
||||
console.log("[SW] Получено сообщение:", event.data);
|
||||
|
||||
if (event.data && event.data.type === "SKIP_WAITING") {
|
||||
self.skipWaiting();
|
||||
}
|
||||
|
||||
if (event.data && event.data.type === 'GET_VERSION') {
|
||||
|
||||
if (event.data && event.data.type === "GET_VERSION") {
|
||||
event.ports[0].postMessage({ version: CACHE_NAME });
|
||||
}
|
||||
|
||||
if (event.data && event.data.type === 'FORCE_UPDATE_CACHE') {
|
||||
|
||||
if (event.data && event.data.type === "FORCE_UPDATE_CACHE") {
|
||||
forceUpdateCache();
|
||||
}
|
||||
|
||||
if (event.data && event.data.type === 'CLEAR_ALL_CACHE') {
|
||||
|
||||
if (event.data && event.data.type === "CLEAR_ALL_CACHE") {
|
||||
clearAllCache();
|
||||
}
|
||||
});
|
||||
|
||||
// Функция принудительного обновления кэша
|
||||
async function forceUpdateCache() {
|
||||
console.log('[SW] Принудительное обновление кэша...');
|
||||
|
||||
console.log("[SW] Принудительное обновление кэша...");
|
||||
|
||||
try {
|
||||
// Удаляем все старые кэши
|
||||
const cacheNames = await caches.keys();
|
||||
await Promise.all(
|
||||
cacheNames.map(cacheName => {
|
||||
console.log('[SW] Удаление кэша:', cacheName);
|
||||
cacheNames.map((cacheName) => {
|
||||
console.log("[SW] Удаление кэша:", cacheName);
|
||||
return caches.delete(cacheName);
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
// Создаем новый кэш с актуальной версией
|
||||
const newCache = await caches.open(STATIC_CACHE_NAME);
|
||||
await newCache.addAll(STATIC_FILES);
|
||||
|
||||
console.log('[SW] Кэш успешно обновлен до версии:', APP_VERSION);
|
||||
|
||||
|
||||
console.log("[SW] Кэш успешно обновлен до версии:", APP_VERSION);
|
||||
|
||||
// Уведомляем клиентов об обновлении
|
||||
const clients = await self.clients.matchAll();
|
||||
clients.forEach(client => {
|
||||
clients.forEach((client) => {
|
||||
client.postMessage({
|
||||
type: 'CACHE_UPDATED',
|
||||
version: APP_VERSION
|
||||
type: "CACHE_UPDATED",
|
||||
version: APP_VERSION,
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[SW] Ошибка при обновлении кэша:', error);
|
||||
console.error("[SW] Ошибка при обновлении кэша:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Функция полной очистки кэша
|
||||
async function clearAllCache() {
|
||||
console.log('[SW] Полная очистка кэша...');
|
||||
|
||||
console.log("[SW] Полная очистка кэша...");
|
||||
|
||||
try {
|
||||
const cacheNames = await caches.keys();
|
||||
await Promise.all(
|
||||
cacheNames.map(cacheName => {
|
||||
console.log('[SW] Удаление кэша:', cacheName);
|
||||
cacheNames.map((cacheName) => {
|
||||
console.log("[SW] Удаление кэша:", cacheName);
|
||||
return caches.delete(cacheName);
|
||||
})
|
||||
);
|
||||
|
||||
console.log('[SW] Весь кэш очищен');
|
||||
|
||||
|
||||
console.log("[SW] Весь кэш очищен");
|
||||
|
||||
// Уведомляем клиентов об очистке
|
||||
const clients = await self.clients.matchAll();
|
||||
clients.forEach(client => {
|
||||
clients.forEach((client) => {
|
||||
client.postMessage({
|
||||
type: 'CACHE_CLEARED'
|
||||
type: "CACHE_CLEARED",
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[SW] Ошибка при очистке кэша:', error);
|
||||
console.error("[SW] Ошибка при очистке кэша:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Тест PWA - NoteJS</title>
|
||||
|
||||
|
||||
<!-- PWA Meta Tags -->
|
||||
<meta name="description" content="Тест PWA для NoteJS" />
|
||||
<meta name="theme-color" content="#007bff" />
|
||||
@ -12,206 +12,276 @@
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="apple-mobile-web-app-title" content="NoteJS" />
|
||||
|
||||
|
||||
<!-- Icons -->
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/icons/icon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/icons/icon-16x16.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-192x192.png" />
|
||||
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/icons/icon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/icons/icon-16x16.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/icons/icon-192x192.png"
|
||||
/>
|
||||
|
||||
<!-- Manifest -->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.status {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.success { background: #d4edda; color: #155724; }
|
||||
.error { background: #f8d7da; color: #721c24; }
|
||||
.info { background: #d1ecf1; color: #0c5460; }
|
||||
button {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
button:hover { background: #0056b3; }
|
||||
button:disabled { background: #6c757d; cursor: not-allowed; }
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.status {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
.info {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
button {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
button:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔧 Тест PWA для NoteJS</h1>
|
||||
|
||||
<div id="status-container">
|
||||
<div class="status info">Проверяем требования PWA...</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button onclick="checkPWAStatus()">Проверить статус PWA</button>
|
||||
<button onclick="installPWA()" id="install-btn" disabled>Установить приложение</button>
|
||||
<button onclick="clearCache()">Очистить кэш</button>
|
||||
</div>
|
||||
|
||||
<div id="debug-info" style="margin-top: 20px; padding: 15px; background: #f8f9fa; border-radius: 5px; font-family: monospace; font-size: 12px;">
|
||||
<h3>Отладочная информация:</h3>
|
||||
<div id="debug-content"></div>
|
||||
</div>
|
||||
<h1>🔧 Тест PWA для NoteJS</h1>
|
||||
|
||||
<div id="status-container">
|
||||
<div class="status info">Проверяем требования PWA...</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button onclick="checkPWAStatus()">Проверить статус PWA</button>
|
||||
<button onclick="installPWA()" id="install-btn" disabled>
|
||||
Установить приложение
|
||||
</button>
|
||||
<button onclick="clearCache()">Очистить кэш</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="debug-info"
|
||||
style="
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
"
|
||||
>
|
||||
<h3>Отладочная информация:</h3>
|
||||
<div id="debug-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PWA Script -->
|
||||
<script src="/pwa.js"></script>
|
||||
|
||||
|
||||
<script>
|
||||
let deferredPrompt;
|
||||
|
||||
// Проверка статуса PWA
|
||||
function checkPWAStatus() {
|
||||
const statusContainer = document.getElementById('status-container');
|
||||
const debugContent = document.getElementById('debug-content');
|
||||
|
||||
// Очищаем предыдущие статусы
|
||||
statusContainer.innerHTML = '';
|
||||
debugContent.innerHTML = '';
|
||||
|
||||
// Проверяем требования PWA
|
||||
const checks = [
|
||||
{
|
||||
name: 'HTTPS или localhost',
|
||||
status: location.protocol === 'https:' || location.hostname === 'localhost',
|
||||
description: `Протокол: ${location.protocol}, Хост: ${location.hostname}`
|
||||
},
|
||||
{
|
||||
name: 'Service Worker',
|
||||
status: 'serviceWorker' in navigator,
|
||||
description: 'Поддержка Service Worker API'
|
||||
},
|
||||
{
|
||||
name: 'Manifest',
|
||||
status: document.querySelector('link[rel="manifest"]') !== null,
|
||||
description: 'Манифест PWA подключен'
|
||||
},
|
||||
{
|
||||
name: 'Иконки',
|
||||
status: document.querySelector('link[rel="icon"]') !== null,
|
||||
description: 'Иконки приложения подключены'
|
||||
},
|
||||
{
|
||||
name: 'Уже установлено',
|
||||
status: window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true,
|
||||
description: 'Приложение уже установлено как PWA'
|
||||
}
|
||||
];
|
||||
|
||||
let allPassed = true;
|
||||
|
||||
checks.forEach(check => {
|
||||
const statusDiv = document.createElement('div');
|
||||
statusDiv.className = `status ${check.status ? 'success' : 'error'}`;
|
||||
statusDiv.innerHTML = `${check.status ? '✅' : '❌'} ${check.name}: ${check.description}`;
|
||||
statusContainer.appendChild(statusDiv);
|
||||
|
||||
if (!check.status) allPassed = false;
|
||||
let deferredPrompt;
|
||||
|
||||
// Проверка статуса PWA
|
||||
function checkPWAStatus() {
|
||||
const statusContainer = document.getElementById("status-container");
|
||||
const debugContent = document.getElementById("debug-content");
|
||||
|
||||
// Очищаем предыдущие статусы
|
||||
statusContainer.innerHTML = "";
|
||||
debugContent.innerHTML = "";
|
||||
|
||||
// Проверяем требования PWA
|
||||
const checks = [
|
||||
{
|
||||
name: "HTTPS или localhost",
|
||||
status:
|
||||
location.protocol === "https:" ||
|
||||
location.hostname === "localhost",
|
||||
description: `Протокол: ${location.protocol}, Хост: ${location.hostname}`,
|
||||
},
|
||||
{
|
||||
name: "Service Worker",
|
||||
status: "serviceWorker" in navigator,
|
||||
description: "Поддержка Service Worker API",
|
||||
},
|
||||
{
|
||||
name: "Manifest",
|
||||
status: document.querySelector('link[rel="manifest"]') !== null,
|
||||
description: "Манифест PWA подключен",
|
||||
},
|
||||
{
|
||||
name: "Иконки",
|
||||
status: document.querySelector('link[rel="icon"]') !== null,
|
||||
description: "Иконки приложения подключены",
|
||||
},
|
||||
{
|
||||
name: "Уже установлено",
|
||||
status:
|
||||
window.matchMedia("(display-mode: standalone)").matches ||
|
||||
window.navigator.standalone === true,
|
||||
description: "Приложение уже установлено как PWA",
|
||||
},
|
||||
];
|
||||
|
||||
let allPassed = true;
|
||||
|
||||
checks.forEach((check) => {
|
||||
const statusDiv = document.createElement("div");
|
||||
statusDiv.className = `status ${check.status ? "success" : "error"}`;
|
||||
statusDiv.innerHTML = `${check.status ? "✅" : "❌"} ${check.name}: ${
|
||||
check.description
|
||||
}`;
|
||||
statusContainer.appendChild(statusDiv);
|
||||
|
||||
if (!check.status) allPassed = false;
|
||||
});
|
||||
|
||||
// Общая оценка
|
||||
const overallDiv = document.createElement("div");
|
||||
overallDiv.className = `status ${allPassed ? "success" : "error"}`;
|
||||
overallDiv.innerHTML = `${allPassed ? "✅" : "❌"} Общий статус: ${
|
||||
allPassed ? "PWA готово к установке" : "Есть проблемы с PWA"
|
||||
}`;
|
||||
statusContainer.appendChild(overallDiv);
|
||||
|
||||
// Отладочная информация
|
||||
const debugInfo = {
|
||||
userAgent: navigator.userAgent,
|
||||
isOnline: navigator.onLine,
|
||||
hasServiceWorker: "serviceWorker" in navigator,
|
||||
isStandalone: window.matchMedia("(display-mode: standalone)").matches,
|
||||
isIOSStandalone: window.navigator.standalone === true,
|
||||
hasDeferredPrompt: deferredPrompt !== null,
|
||||
pwaInfo: window.PWAManager
|
||||
? window.PWAManager.getPWAInfo()
|
||||
: "PWA Manager не доступен",
|
||||
};
|
||||
|
||||
debugContent.innerHTML = JSON.stringify(debugInfo, null, 2);
|
||||
|
||||
// Активируем кнопку установки если доступно
|
||||
const installBtn = document.getElementById("install-btn");
|
||||
if (
|
||||
deferredPrompt &&
|
||||
!debugInfo.isStandalone &&
|
||||
!debugInfo.isIOSStandalone
|
||||
) {
|
||||
installBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Установка PWA
|
||||
function installPWA() {
|
||||
if (deferredPrompt) {
|
||||
deferredPrompt.prompt();
|
||||
deferredPrompt.userChoice.then((choiceResult) => {
|
||||
console.log("Результат установки:", choiceResult.outcome);
|
||||
if (choiceResult.outcome === "accepted") {
|
||||
alert("Приложение установлено!");
|
||||
}
|
||||
deferredPrompt = null;
|
||||
document.getElementById("install-btn").disabled = true;
|
||||
});
|
||||
} else {
|
||||
alert(
|
||||
"Установка недоступна. Возможно, приложение уже установлено или браузер не поддерживает установку PWA."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Очистка кэша
|
||||
function clearCache() {
|
||||
if ("caches" in window) {
|
||||
caches
|
||||
.keys()
|
||||
.then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => {
|
||||
console.log("Удаление кэша:", cacheName);
|
||||
return caches.delete(cacheName);
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
alert("Кэш очищен! Перезагрузите страницу.");
|
||||
});
|
||||
|
||||
// Общая оценка
|
||||
const overallDiv = document.createElement('div');
|
||||
overallDiv.className = `status ${allPassed ? 'success' : 'error'}`;
|
||||
overallDiv.innerHTML = `${allPassed ? '✅' : '❌'} Общий статус: ${allPassed ? 'PWA готово к установке' : 'Есть проблемы с PWA'}`;
|
||||
statusContainer.appendChild(overallDiv);
|
||||
|
||||
// Отладочная информация
|
||||
const debugInfo = {
|
||||
userAgent: navigator.userAgent,
|
||||
isOnline: navigator.onLine,
|
||||
hasServiceWorker: 'serviceWorker' in navigator,
|
||||
isStandalone: window.matchMedia('(display-mode: standalone)').matches,
|
||||
isIOSStandalone: window.navigator.standalone === true,
|
||||
hasDeferredPrompt: deferredPrompt !== null,
|
||||
pwaInfo: window.PWAManager ? window.PWAManager.getPWAInfo() : 'PWA Manager не доступен'
|
||||
};
|
||||
|
||||
debugContent.innerHTML = JSON.stringify(debugInfo, null, 2);
|
||||
|
||||
// Активируем кнопку установки если доступно
|
||||
const installBtn = document.getElementById('install-btn');
|
||||
if (deferredPrompt && !debugInfo.isStandalone && !debugInfo.isIOSStandalone) {
|
||||
installBtn.disabled = false;
|
||||
}
|
||||
} else {
|
||||
alert("Кэш не поддерживается в этом браузере.");
|
||||
}
|
||||
|
||||
// Установка PWA
|
||||
function installPWA() {
|
||||
if (deferredPrompt) {
|
||||
deferredPrompt.prompt();
|
||||
deferredPrompt.userChoice.then((choiceResult) => {
|
||||
console.log('Результат установки:', choiceResult.outcome);
|
||||
if (choiceResult.outcome === 'accepted') {
|
||||
alert('Приложение установлено!');
|
||||
}
|
||||
deferredPrompt = null;
|
||||
document.getElementById('install-btn').disabled = true;
|
||||
});
|
||||
} else {
|
||||
alert('Установка недоступна. Возможно, приложение уже установлено или браузер не поддерживает установку PWA.');
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка события beforeinstallprompt
|
||||
window.addEventListener("beforeinstallprompt", (e) => {
|
||||
console.log("beforeinstallprompt событие получено");
|
||||
|
||||
// На мобильных устройствах позволяем браузеру показать нативный баннер
|
||||
const isMobile =
|
||||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
navigator.userAgent
|
||||
) ||
|
||||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2) ||
|
||||
window.matchMedia("(max-width: 768px)").matches;
|
||||
|
||||
if (!isMobile) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Очистка кэша
|
||||
function clearCache() {
|
||||
if ('caches' in window) {
|
||||
caches.keys().then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames.map(cacheName => {
|
||||
console.log('Удаление кэша:', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
})
|
||||
);
|
||||
}).then(() => {
|
||||
alert('Кэш очищен! Перезагрузите страницу.');
|
||||
});
|
||||
} else {
|
||||
alert('Кэш не поддерживается в этом браузере.');
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка события beforeinstallprompt
|
||||
window.addEventListener('beforeinstallprompt', (e) => {
|
||||
console.log('beforeinstallprompt событие получено');
|
||||
e.preventDefault();
|
||||
deferredPrompt = e;
|
||||
document.getElementById('install-btn').disabled = false;
|
||||
});
|
||||
|
||||
// Обработка успешной установки
|
||||
window.addEventListener('appinstalled', () => {
|
||||
console.log('PWA установлено успешно');
|
||||
alert('Приложение установлено успешно!');
|
||||
document.getElementById('install-btn').disabled = true;
|
||||
});
|
||||
|
||||
// Автоматическая проверка при загрузке
|
||||
window.addEventListener('load', () => {
|
||||
setTimeout(checkPWAStatus, 1000);
|
||||
});
|
||||
deferredPrompt = e;
|
||||
document.getElementById("install-btn").disabled = false;
|
||||
});
|
||||
|
||||
// Обработка успешной установки
|
||||
window.addEventListener("appinstalled", () => {
|
||||
console.log("PWA установлено успешно");
|
||||
alert("Приложение установлено успешно!");
|
||||
document.getElementById("install-btn").disabled = true;
|
||||
});
|
||||
|
||||
// Автоматическая проверка при загрузке
|
||||
window.addEventListener("load", () => {
|
||||
setTimeout(checkPWAStatus, 1000);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user