Добавлен PWA, оффлайн режим и плашка о том что сейчас оффлайн режим
This commit is contained in:
parent
2d947941b5
commit
4e697a7b04
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,2 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
apikey.txt
|
apikey.txt
|
||||||
BIN
@eaDir/README.md@SynoEAStream
Normal file
BIN
@eaDir/README.md@SynoEAStream
Normal file
Binary file not shown.
BIN
@eaDir/index.html@SynoEAStream
Normal file
BIN
@eaDir/index.html@SynoEAStream
Normal file
Binary file not shown.
BIN
@eaDir/input.css@SynoEAStream
Normal file
BIN
@eaDir/input.css@SynoEAStream
Normal file
Binary file not shown.
BIN
@eaDir/output.css@SynoEAStream
Normal file
BIN
@eaDir/output.css@SynoEAStream
Normal file
Binary file not shown.
BIN
@eaDir/package.json@SynoEAStream
Normal file
BIN
@eaDir/package.json@SynoEAStream
Normal file
Binary file not shown.
BIN
@eaDir/postcss.config.js@SynoEAStream
Normal file
BIN
@eaDir/postcss.config.js@SynoEAStream
Normal file
Binary file not shown.
BIN
@eaDir/script.js@SynoEAStream
Normal file
BIN
@eaDir/script.js@SynoEAStream
Normal file
Binary file not shown.
BIN
@eaDir/server.js@SynoEAStream
Normal file
BIN
@eaDir/server.js@SynoEAStream
Normal file
Binary file not shown.
BIN
@eaDir/tailwind.config.js@SynoEAStream
Normal file
BIN
@eaDir/tailwind.config.js@SynoEAStream
Normal file
Binary file not shown.
36
APK_instructions_Capacitor.md
Normal file
36
APK_instructions_Capacitor.md
Normal 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.
|
||||||
36
APK_instructions_PWABuilder.md
Normal file
36
APK_instructions_PWABuilder.md
Normal 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
24
convert_icons.js
Normal 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
BIN
icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
9
icon-192.svg
Normal file
9
icon-192.svg
Normal 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
BIN
icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
9
icon-512.svg
Normal file
9
icon-512.svg
Normal 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 |
10
index.html
10
index.html
@ -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
21
manifest.json
Normal 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
1693
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
script.js
59
script.js
@ -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
86
service-worker.js
Normal 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);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user