feat: implémentation de la PWA avec service worker, manifest et installation
BIN
public/images/icons/icon-128x128.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
public/images/icons/icon-144x144.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
public/images/icons/icon-152x152.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
public/images/icons/icon-192x192.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
public/images/icons/icon-384x384.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
public/images/icons/icon-512x512.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/images/icons/icon-72x72.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/images/icons/icon-96x96.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
@@ -1,17 +1,103 @@
|
||||
{
|
||||
"name": "StripStream",
|
||||
"short_name": "StripStream",
|
||||
"description": "A modern web reader for Komga",
|
||||
"description": "Votre bibliothèque numérique pour lire vos BD, mangas et comics préférés",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait",
|
||||
"background_color": "#0F172A",
|
||||
"theme_color": "#4F46E5",
|
||||
"categories": ["books", "entertainment", "reading"],
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon.svg",
|
||||
"sizes": "32x32",
|
||||
"type": "image/svg+xml",
|
||||
"src": "/images/icons/icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icons/icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icons/icon-128x128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icons/icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icons/icon-152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icons/icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icons/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
]
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/images/screenshots/home.png",
|
||||
"sizes": "1280x720",
|
||||
"type": "image/png",
|
||||
"platform": "wide",
|
||||
"label": "Page d'accueil de StripStream"
|
||||
},
|
||||
{
|
||||
"src": "/images/screenshots/reader.png",
|
||||
"sizes": "1280x720",
|
||||
"type": "image/png",
|
||||
"platform": "wide",
|
||||
"label": "Lecteur de BD StripStream"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Accueil",
|
||||
"url": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/icons/home.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Bibliothèques",
|
||||
"url": "/libraries",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/icons/library.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"related_applications": [],
|
||||
"prefer_related_applications": false
|
||||
}
|
||||
|
||||
62
public/offline.html
Normal file
@@ -0,0 +1,62 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Hors ligne - StripStream</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background-color: #0f172a;
|
||||
color: #e2e8f0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #4f46e5;
|
||||
}
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.5;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
button {
|
||||
background-color: #4f46e5;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #4338ca;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Vous êtes hors ligne</h1>
|
||||
<p>
|
||||
Il semble que vous n'ayez pas de connexion internet. Certaines fonctionnalités de
|
||||
StripStream peuvent ne pas être disponibles en mode hors ligne.
|
||||
</p>
|
||||
<button onclick="window.location.reload()">Réessayer</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
58
public/sw.js
Normal file
@@ -0,0 +1,58 @@
|
||||
const CACHE_NAME = "stripstream-cache-v1";
|
||||
const OFFLINE_PAGE = "/offline.html";
|
||||
|
||||
const STATIC_ASSETS = [
|
||||
"/",
|
||||
"/offline.html",
|
||||
"/manifest.json",
|
||||
"/favicon.svg",
|
||||
"/images/icons/icon-192x192.png",
|
||||
"/images/icons/icon-512x512.png",
|
||||
];
|
||||
|
||||
// Installation du service worker
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
return cache.addAll(STATIC_ASSETS);
|
||||
})
|
||||
);
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
// Activation et nettoyage des anciens caches
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.filter((name) => name !== CACHE_NAME).map((name) => caches.delete(name))
|
||||
);
|
||||
})
|
||||
);
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
// Stratégie de cache : Network First avec fallback sur le cache
|
||||
self.addEventListener("fetch", (event) => {
|
||||
// Ne pas intercepter les requêtes vers l'API
|
||||
if (event.request.url.includes("/api/")) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
fetch(event.request)
|
||||
.then((response) => {
|
||||
// Mettre en cache la nouvelle réponse
|
||||
const responseClone = response.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
cache.put(event.request, responseClone);
|
||||
});
|
||||
return response;
|
||||
})
|
||||
.catch(async () => {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
const cachedResponse = await cache.match(event.request);
|
||||
return cachedResponse || cache.match(OFFLINE_PAGE);
|
||||
})
|
||||
);
|
||||
});
|
||||