Обновлены мета-теги и улучшена функциональность PWA

- Оптимизированы мета-теги в index.html и test-pwa.html для лучшей поддержки PWA.
- Улучшена структура кода с использованием многострочных атрибутов для мета-тегов.
- Обновлен сервисный работник для более эффективного кэширования и обработки запросов.
- Добавлены новые функции в pwa.js для управления установкой и обновлением PWA.
This commit is contained in:
Fovway 2025-10-20 12:29:43 +07:00
parent efc3c4c777
commit 091fc6cc1e
5 changed files with 944 additions and 502 deletions

View File

@ -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>

View 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>

View File

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

View File

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

View File

@ -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>