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