diff --git a/public/browserconfig.xml b/public/browserconfig.xml new file mode 100644 index 0000000..6d2f040 --- /dev/null +++ b/public/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #007bff + + + diff --git a/public/icon.svg b/public/icon.svg new file mode 100644 index 0000000..f05aada --- /dev/null +++ b/public/icon.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #tag + + + + + + diff --git a/public/index.html b/public/index.html index bfa9bd7..8c52341 100644 --- a/public/index.html +++ b/public/index.html @@ -4,7 +4,30 @@ Вход в систему заметок + + + + + + + + + + + + + + + + + + + + + + + @@ -50,5 +73,66 @@

Создатель: Fovway

+ + + diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 0000000..05d0654 --- /dev/null +++ b/public/logo.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + # + + + + + + + NoteJS + + + + + Система заметок + + + + + + + + diff --git a/public/notes.html b/public/notes.html index 7b6425b..84cc8c0 100644 --- a/public/notes.html +++ b/public/notes.html @@ -3,8 +3,30 @@ - Заметки + Заметки - NoteJS + + + + + + + + + + + + + + + + + + + + + + @@ -240,6 +262,10 @@ + + + + @@ -132,5 +154,8 @@ + + + diff --git a/public/pwa.js b/public/pwa.js new file mode 100644 index 0000000..f8976b0 --- /dev/null +++ b/public/pwa.js @@ -0,0 +1,164 @@ +// PWA Service Worker Registration и установка +class PWAManager { + constructor() { + this.deferredPrompt = null; + this.init(); + } + + init() { + this.registerServiceWorker(); + this.setupInstallPrompt(); + this.setupAppInstalled(); + } + + // Регистрация Service Worker + registerServiceWorker() { + if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/sw.js') + .then((registration) => { + console.log('SW зарегистрирован успешно:', registration.scope); + + // Проверяем обновления + registration.addEventListener('updatefound', () => { + const newWorker = registration.installing; + newWorker.addEventListener('statechange', () => { + if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { + this.showUpdateNotification(); + } + }); + }); + }) + .catch((error) => { + console.log('Ошибка регистрации SW:', error); + }); + }); + } + } + + // Показ уведомления об обновлении + showUpdateNotification() { + if (confirm('Доступна новая версия приложения. Обновить?')) { + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.postMessage({ type: 'SKIP_WAITING' }); + } + window.location.reload(); + } + } + + // Настройка промпта установки + setupInstallPrompt() { + window.addEventListener('beforeinstallprompt', (e) => { + e.preventDefault(); + this.deferredPrompt = e; + this.showInstallButton(); + }); + } + + // Показ кнопки установки + showInstallButton() { + // Проверяем, не установлено ли уже приложение + if (window.matchMedia('(display-mode: standalone)').matches || + window.navigator.standalone === true) { + return; // Приложение уже установлено + } + + const installButton = this.createInstallButton(); + this.addInstallButtonToPage(installButton); + } + + // Создание кнопки установки + createInstallButton() { + const installButton = document.createElement('button'); + installButton.textContent = '📱 Установить приложение'; + installButton.className = 'btnSave'; + installButton.style.marginTop = '10px'; + installButton.style.width = '100%'; + installButton.style.fontSize = '14px'; + installButton.style.display = 'flex'; + installButton.style.alignItems = 'center'; + installButton.style.justifyContent = 'center'; + installButton.style.gap = '8px'; + + installButton.addEventListener('click', () => { + this.installApp(); + }); + + return installButton; + } + + // Добавление кнопки на страницу + addInstallButtonToPage(installButton) { + // Ищем подходящее место для кнопки + const authLink = document.querySelector('.auth-link'); + const footer = document.querySelector('.footer'); + const container = document.querySelector('.container'); + + if (authLink) { + authLink.appendChild(installButton); + } else if (footer) { + footer.insertBefore(installButton, footer.firstChild); + } else if (container) { + container.appendChild(installButton); + } + } + + // Установка приложения + installApp() { + if (this.deferredPrompt) { + this.deferredPrompt.prompt(); + this.deferredPrompt.userChoice.then((choiceResult) => { + if (choiceResult.outcome === 'accepted') { + console.log('Пользователь установил приложение'); + this.trackInstallation(); + } + this.deferredPrompt = null; + this.removeInstallButton(); + }); + } + } + + // Удаление кнопки установки + removeInstallButton() { + const installButton = document.querySelector('button[style*="Установить приложение"]'); + if (installButton) { + installButton.remove(); + } + } + + // Отслеживание установки + trackInstallation() { + // Здесь можно добавить аналитику + console.log('PWA установлено успешно'); + } + + // Обработка успешной установки + setupAppInstalled() { + window.addEventListener('appinstalled', () => { + console.log('PWA установлено успешно'); + this.removeInstallButton(); + }); + } + + // Проверка статуса PWA + isPWAInstalled() { + return window.matchMedia('(display-mode: standalone)').matches || + window.navigator.standalone === true; + } + + // Получение информации о PWA + getPWAInfo() { + return { + isInstalled: this.isPWAInstalled(), + isOnline: navigator.onLine, + hasServiceWorker: 'serviceWorker' in navigator, + userAgent: navigator.userAgent + }; + } +} + +// Инициализация PWA Manager +const pwaManager = new PWAManager(); + +// Экспорт для использования в других скриптах +window.PWAManager = pwaManager; diff --git a/public/register.html b/public/register.html index f3ebb8c..6f95636 100644 --- a/public/register.html +++ b/public/register.html @@ -3,8 +3,30 @@ - Регистрация + Регистрация - NoteJS + + + + + + + + + + + + + + + + + + + + + + @@ -61,5 +83,8 @@

Создатель: Fovway

+ + + diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..ffc2569 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,266 @@ +// Service Worker для NoteJS +const CACHE_NAME = 'notejs-v1.0.0'; +const STATIC_CACHE_NAME = 'notejs-static-v1.0.0'; +const DYNAMIC_CACHE_NAME = 'notejs-dynamic-v1.0.0'; + +// Файлы для кэширования при установке +const STATIC_FILES = [ + '/', + '/index.html', + '/login.html', + '/register.html', + '/notes.html', + '/profile.html', + '/style.css', + '/app.js', + '/login.js', + '/register.js', + '/profile.js', + '/icon.svg', + '/logo.svg', + '/manifest.json', + '/icons/icon-192x192.png', + '/icons/icon-512x512.png', + 'https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js' +]; + +// Файлы, которые не нужно кэшировать +const EXCLUDE_FROM_CACHE = [ + '/api/', + '/uploads/', + '/database/' +]; + +// Установка Service Worker +self.addEventListener('install', (event) => { + console.log('[SW] Установка Service Worker'); + + event.waitUntil( + caches.open(STATIC_CACHE_NAME) + .then((cache) => { + console.log('[SW] Кэширование статических файлов'); + return cache.addAll(STATIC_FILES); + }) + .then(() => { + console.log('[SW] Статические файлы закэшированы'); + return self.skipWaiting(); + }) + .catch((error) => { + console.error('[SW] Ошибка при кэшировании статических файлов:', error); + }) + ); +}); + +// Активация Service Worker +self.addEventListener('activate', (event) => { + console.log('[SW] Активация Service Worker'); + + event.waitUntil( + caches.keys() + .then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + // Удаляем старые кэши + if (cacheName !== STATIC_CACHE_NAME && cacheName !== DYNAMIC_CACHE_NAME) { + console.log('[SW] Удаление старого кэша:', cacheName); + return caches.delete(cacheName); + } + }) + ); + }) + .then(() => { + console.log('[SW] Service Worker активирован'); + return self.clients.claim(); + }) + ); +}); + +// Перехват запросов +self.addEventListener('fetch', (event) => { + const { request } = event; + const url = new URL(request.url); + + // Пропускаем запросы к API и загрузкам + if (EXCLUDE_FROM_CACHE.some(pattern => url.pathname.startsWith(pattern))) { + return; + } + + // Стратегия кэширования: Cache First для статических файлов, Network First для HTML + if (request.method === 'GET') { + event.respondWith( + handleRequest(request) + ); + } +}); + +async function handleRequest(request) { + const url = new URL(request.url); + + try { + // Для HTML файлов используем Network First стратегию + if (request.headers.get('accept')?.includes('text/html')) { + return await networkFirstStrategy(request); + } + + // Для статических ресурсов используем Cache First стратегию + return await cacheFirstStrategy(request); + + } catch (error) { + console.error('[SW] Ошибка при обработке запроса:', error); + + // Fallback для HTML страниц + if (request.headers.get('accept')?.includes('text/html')) { + return await caches.match('/index.html'); + } + + throw error; + } +} + +// Стратегия Cache First (для статических ресурсов) +async function cacheFirstStrategy(request) { + const cachedResponse = await caches.match(request); + + if (cachedResponse) { + console.log('[SW] Запрос из кэша:', request.url); + return cachedResponse; + } + + console.log('[SW] Запрос к сети:', request.url); + const networkResponse = await fetch(request); + + // Кэшируем успешные ответы + if (networkResponse.ok) { + const cache = await caches.open(DYNAMIC_CACHE_NAME); + cache.put(request, networkResponse.clone()); + } + + return networkResponse; +} + +// Стратегия Network First (для HTML страниц) +async function networkFirstStrategy(request) { + try { + console.log('[SW] Запрос к сети (Network First):', request.url); + const networkResponse = await fetch(request); + + // Кэшируем успешные ответы + if (networkResponse.ok) { + const cache = await caches.open(DYNAMIC_CACHE_NAME); + cache.put(request, networkResponse.clone()); + } + + return networkResponse; + + } catch (error) { + console.log('[SW] Сеть недоступна, поиск в кэше:', request.url); + const cachedResponse = await caches.match(request); + + if (cachedResponse) { + return cachedResponse; + } + + // Fallback на главную страницу + if (request.headers.get('accept')?.includes('text/html')) { + return await caches.match('/index.html'); + } + + throw error; + } +} + +// Обработка push уведомлений (для будущего использования) +self.addEventListener('push', (event) => { + console.log('[SW] Получено push уведомление'); + + const options = { + body: event.data ? event.data.text() : 'Новое уведомление от NoteJS', + icon: '/icons/icon-192x192.png', + badge: '/icons/icon-96x96.png', + vibrate: [100, 50, 100], + data: { + dateOfArrival: Date.now(), + primaryKey: 1 + }, + actions: [ + { + action: 'explore', + title: 'Открыть приложение', + icon: '/icons/icon-96x96.png' + }, + { + action: 'close', + title: 'Закрыть', + icon: '/icons/icon-96x96.png' + } + ] + }; + + event.waitUntil( + self.registration.showNotification('NoteJS', options) + ); +}); + +// Обработка кликов по уведомлениям +self.addEventListener('notificationclick', (event) => { + console.log('[SW] Клик по уведомлению:', event.action); + + event.notification.close(); + + if (event.action === 'explore') { + event.waitUntil( + clients.openWindow('/') + ); + } +}); + +// Синхронизация в фоне (для будущего использования) +self.addEventListener('sync', (event) => { + console.log('[SW] Фоновая синхронизация:', event.tag); + + if (event.tag === 'background-sync') { + event.waitUntil( + doBackgroundSync() + ); + } +}); + +async function doBackgroundSync() { + // Здесь можно добавить логику синхронизации данных + console.log('[SW] Выполнение фоновой синхронизации'); +} + +// Обработка сообщений от основного потока +self.addEventListener('message', (event) => { + console.log('[SW] Получено сообщение:', event.data); + + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting(); + } + + if (event.data && event.data.type === 'GET_VERSION') { + event.ports[0].postMessage({ version: CACHE_NAME }); + } +}); + +// Периодическая очистка кэша +self.addEventListener('periodicsync', (event) => { + if (event.tag === 'cache-cleanup') { + event.waitUntil(cleanupCache()); + } +}); + +async function cleanupCache() { + const cacheNames = await caches.keys(); + const oldCaches = cacheNames.filter(name => + name !== STATIC_CACHE_NAME && + name !== DYNAMIC_CACHE_NAME && + name.startsWith('notejs-') + ); + + await Promise.all( + oldCaches.map(name => caches.delete(name)) + ); + + console.log('[SW] Очистка старых кэшей завершена'); +} diff --git a/server.js b/server.js index 0367f5f..ffc41b6 100644 --- a/server.js +++ b/server.js @@ -132,6 +132,25 @@ app.use( // Статические файлы app.use(express.static(path.join(__dirname, "public"))); +// PWA файлы с правильными заголовками +app.get('/manifest.json', (req, res) => { + res.setHeader('Content-Type', 'application/manifest+json'); + res.setHeader('Cache-Control', 'public, max-age=86400'); // 24 часа + res.sendFile(path.join(__dirname, 'public', 'manifest.json')); +}); + +app.get('/sw.js', (req, res) => { + res.setHeader('Content-Type', 'application/javascript'); + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.sendFile(path.join(__dirname, 'public', 'sw.js')); +}); + +app.get('/browserconfig.xml', (req, res) => { + res.setHeader('Content-Type', 'application/xml'); + res.setHeader('Cache-Control', 'public, max-age=86400'); // 24 часа + res.sendFile(path.join(__dirname, 'public', 'browserconfig.xml')); +}); + // Парсинг тела запроса app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json());