Добавлен PWA, оффлайн режим и плашка о том что сейчас оффлайн режим

This commit is contained in:
Fovway 2025-10-09 15:27:28 +07:00
parent 2d947941b5
commit 4e697a7b04
23 changed files with 1986 additions and 2 deletions

2
.gitignore vendored
View File

@ -1,2 +1,2 @@
node_modules/ node_modules/
apikey.txt apikey.txt

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,36 @@
# Создание APK из PWA с помощью Capacitor
## Предварительные требования
- Node.js версии 20 или выше (скачайте с nodejs.org).
- Android Studio установлен (скачайте с developer.android.com/studio).
## Шаг 1: Подготовьте проект
- Убедитесь, что у вас есть package.json (npm init -y если нет).
- Установите Capacitor: npm install @capacitor/cli @capacitor/core --save-dev.
## Шаг 2: Инициализируйте Capacitor
- Выполните: npx cap init "Weather App" "com.weather.app".
- Это создаст capacitor.config.json.
## Шаг 3: Добавьте Android платформу
- Выполните: npx cap add android.
- Это создаст папку android/ с нативным проектом.
## Шаг 4: Синхронизируйте веб-ресурсы
- Выполните: npx cap sync android.
- Это скопирует веб-файлы в нативный проект.
## Шаг 5: Откройте в Android Studio
- Выполните: npx cap open android.
- Android Studio откроется с проектом.
## Шаг 6: Соберите APK
- В Android Studio подключите устройство или эмулятор.
- Нажмите "Run" > "Run 'app'" для тестирования.
- Для сборки APK: "Build" > "Build Bundle(s)/APK(s)" > "Build APK(s)".
- APK будет в android/app/build/outputs/apk/debug/.
## Примечания
- Убедитесь, что порт для dev server не конфликтует (в capacitor.config.json можно настроить).
- Для production используйте npx cap sync android перед сборкой.
- Иконки в manifest.json должны быть PNG.

View File

@ -0,0 +1,36 @@
# Создание APK из PWA с помощью PWABuilder
## Шаг 1: Разверните PWA на хостинге
- Создайте аккаунт на GitHub (если нет).
- Создайте новый репозиторий.
- Загрузите все файлы проекта (index.html, script.js, manifest.json, service-worker.js, иконки и т.д.).
- В настройках репозитория включите GitHub Pages (Settings > Pages > Source: main branch).
- Получите URL типа https://yourusername.github.io/repository-name.
## Шаг 2: Проверьте PWA
- Убедитесь, что сайт работает на https://yourusername.github.io/repository-name.
- Проверьте консоль браузера на ошибки (Service Worker должен зарегистрироваться, manifest загрузиться).
## Шаг 3: Используйте PWABuilder
- Перейдите на https://pwabuilder.com.
- В поле "Enter the URL of a website" введите URL вашего PWA.
- Нажмите "Start".
## Шаг 4: Выберите платформы
- Выберите "Android" в списке платформ.
- Нажмите "Next".
## Шаг 5: Скачайте проект
- В разделе "Android" нажмите "Download".
- Скачайте ZIP файл с Cordova проектом.
## Шаг 6: Соберите APK
- Разархивируйте ZIP.
- Установите Android Studio (если нет).
- В Android Studio откройте проект из папки android.
- Подключите Android устройство или эмулятор.
- Нажмите "Run" > "Run 'app'" для тестирования или "Build" > "Build Bundle(s)/APK(s)" > "Build APK(s)" для сборки APK.
## Примечания
- PWABuilder генерирует Cordova проект, который можно открыть в Android Studio.
- Убедитесь, что иконки в manifest.json - PNG файлы (конвертируйте SVG в PNG, если нужно).

24
convert_icons.js Normal file
View File

@ -0,0 +1,24 @@
const sharp = require('sharp');
const fs = require('fs');
// Конвертация icon-192.svg в icon-192.png
sharp('icon-192.svg')
.png()
.toFile('icon-192.png')
.then(() => {
console.log('icon-192.png created');
})
.catch(err => {
console.error('Error creating icon-192.png:', err);
});
// Конвертация icon-512.svg в icon-512.png
sharp('icon-512.svg')
.png()
.toFile('icon-512.png')
.then(() => {
console.log('icon-512.png created');
})
.catch(err => {
console.error('Error creating icon-512.png:', err);
});

BIN
icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

9
icon-192.svg Normal file
View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192" width="192" height="192">
<defs>
<style>.a{fill:#3b82f6;}.b{fill:#ffffff;}</style>
</defs>
<circle class="a" cx="96" cy="96" r="72"/>
<circle class="b" cx="96" cy="96" r="48"/>
<path class="a" d="M96,24V36M96,156V168M168,96H156M36,96H24M138.5,53.5l-8.5,8.5M53.5,138.5l-8.5,8.5M138.5,138.5l-8.5-8.5M53.5,53.5l-8.5-8.5" stroke="#3b82f6" stroke-width="3" fill="none"/>
<ellipse class="a" cx="120" cy="144" rx="48" ry="24" opacity="0.7"/>
</svg>

After

Width:  |  Height:  |  Size: 519 B

BIN
icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

9
icon-512.svg Normal file
View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<defs>
<style>.a{fill:#3b82f6;}.b{fill:#ffffff;}</style>
</defs>
<circle class="a" cx="256" cy="256" r="192"/>
<circle class="b" cx="256" cy="256" r="128"/>
<path class="a" d="M256,64V96M256,416V448M448,256H416M96,256H64M368.5,143.5l-22.5,22.5M143.5,368.5l-22.5,22.5M368.5,368.5l-22.5-22.5M143.5,143.5l-22.5-22.5" stroke="#3b82f6" stroke-width="8" fill="none"/>
<ellipse class="a" cx="320" cy="384" rx="128" ry="64" opacity="0.7"/>
</svg>

After

Width:  |  Height:  |  Size: 542 B

View File

@ -4,6 +4,8 @@
<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>
<meta name="theme-color" content="#3b82f6" />
<link rel="manifest" href="/manifest.json" />
<link <link
rel="icon" rel="icon"
type="image/svg+xml" type="image/svg+xml"
@ -217,6 +219,14 @@
</head> </head>
<body class="min-h-screen"> <body class="min-h-screen">
<div class="container mx-auto max-w-7xl px-4 py-8"> <div class="container mx-auto max-w-7xl px-4 py-8">
<!-- Offline Banner -->
<div id="offline-banner" class="hidden fixed top-0 left-0 right-0 z-50 bg-gradient-to-r from-red-500 to-orange-500 dark:from-red-600 dark:to-orange-600 text-white px-6 py-3 text-center shadow-2xl border-b-4 border-red-700 dark:border-red-800 animate-pulse">
<div class="flex items-center justify-center gap-3">
<i data-lucide="wifi-off" class="w-6 h-6 animate-bounce"></i>
<span class="font-bold text-lg">🚫 Оффлайн режим. Данные могут быть не актуальными.</span>
</div>
</div>
<!-- Header --> <!-- Header -->
<header class="text-center mb-12"> <header class="text-center mb-12">
<div class="flex items-center justify-center gap-3 mb-4"> <div class="flex items-center justify-center gap-3 mb-4">

21
manifest.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "Погода",
"short_name": "Погода",
"description": "Узнайте погоду в вашем городе",
"start_url": "/",
"display": "standalone",
"background_color": "#dbeafe",
"theme_color": "#3b82f6",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

1693
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,8 +15,11 @@
"express": "^4.17.1" "express": "^4.17.1"
}, },
"devDependencies": { "devDependencies": {
"@capacitor/cli": "^7.4.3",
"@capacitor/core": "^7.4.3",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"sharp": "^0.34.4",
"tailwindcss": "^3.4.16" "tailwindcss": "^3.4.16"
} }
} }

View File

@ -1,5 +1,5 @@
// API ключ от WeatherAPI // API ключ от WeatherAPI
const API_KEY = "API_KEY_HERE"; const API_KEY = "485eff906f7d473b913104046250710";
// Глобальные переменные для темы // Глобальные переменные для темы
const themeButtons = { const themeButtons = {
@ -367,6 +367,63 @@ document.addEventListener("DOMContentLoaded", () => {
initializeTheme(); initializeTheme();
watchSystemTheme(); watchSystemTheme();
// Регистрируем Service Worker для PWA
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then(function(registration) {
console.log('Service Worker registered successfully:', registration.scope);
})
.catch(function(error) {
console.log('Service Worker registration failed:', error);
});
}
// Логи для PWA установки
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
console.log('PWA install prompt available');
deferredPrompt = e;
});
window.addEventListener('appinstalled', (e) => {
console.log('PWA installed successfully');
});
// Функция для проверки реального соединения
async function checkOnlineStatus() {
try {
// Пингуем небольшой ресурс
const response = await fetch('/manifest.json', { method: 'HEAD', cache: 'no-cache' });
return response.ok;
} catch (error) {
console.log('Connection check failed:', error);
return false;
}
}
// Функция для управления плашкой оффлайн
async function updateOfflineBanner() {
const banner = document.getElementById('offline-banner');
const isOnline = await checkOnlineStatus();
if (!isOnline) {
banner.classList.remove('hidden');
console.log('Оффлайн режим активирован');
} else {
banner.classList.add('hidden');
console.log('Онлайн режим восстановлен');
}
}
// Проверяем статус при загрузке
updateOfflineBanner();
// Слушаем изменения статуса сети и проверяем реально
window.addEventListener('online', updateOfflineBanner);
window.addEventListener('offline', updateOfflineBanner);
// Периодическая проверка каждые 30 секунд
setInterval(updateOfflineBanner, 30000);
// Сохраняем данные для графиков // Сохраняем данные для графиков
window.currentHourlyData = null; window.currentHourlyData = null;

86
service-worker.js Normal file
View File

@ -0,0 +1,86 @@
const CACHE_NAME = 'weather-app-v1';
const API_CACHE_NAME = 'weather-api-v1';
const urlsToCache = [
'/',
'/index.html',
'/script.js',
'/output.css',
'/manifest.json',
'/icon-192.png',
'/icon-512.png',
// Добавьте другие ресурсы
'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap',
'https://unpkg.com/lucide@latest/dist/umd/lucide.js',
'https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css',
'https://cdn.jsdelivr.net/npm/chart.js',
'https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js'
];
// Установка сервис-воркера
self.addEventListener('install', event => {
console.log('Service Worker installing.');
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
// Активация сервис-воркера
self.addEventListener('activate', event => {
console.log('Service Worker activating.');
event.waitUntil(
caches.keys().then(cacheNames => {
console.log('Available caches:', cacheNames);
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME && cacheName !== API_CACHE_NAME) {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
// Немедленно берем контроль над страницами
return self.clients.claim();
});
// Обработка запросов
self.addEventListener('fetch', event => {
if (event.request.url.includes('api.weatherapi.com')) {
// Network-first стратегия для API погоды
event.respondWith(
fetch(event.request)
.then(response => {
// Клонируем ответ для кэширования
const responseClone = response.clone();
caches.open(API_CACHE_NAME)
.then(cache => {
cache.put(event.request, responseClone);
console.log('API response cached:', event.request.url);
});
return response;
})
.catch(() => {
console.log('Network failed, trying cache for:', event.request.url);
return caches.match(event.request);
})
);
} else {
// Cache-first стратегия для статических файлов
event.respondWith(
caches.match(event.request)
.then(response => {
if (response) {
console.log('Serving from cache:', event.request.url);
return response;
}
console.log('Fetching from network:', event.request.url);
return fetch(event.request);
})
);
}
});