feat: add Progressive Web App (PWA) support with service worker registration, manifest, update notifications, and install prompts
@@ -5271,5 +5271,89 @@
|
||||
safeCreateIcons();
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
// ---------------------------------------------------------------------------
|
||||
// PWA Service Worker Registration
|
||||
// ---------------------------------------------------------------------------
|
||||
function registerServiceWorker() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
.then((registration) => {
|
||||
console.log('[PWA] Service Worker registered successfully:', registration.scope);
|
||||
|
||||
// Check for updates periodically
|
||||
setInterval(() => {
|
||||
registration.update();
|
||||
}, 60000); // Check every minute
|
||||
|
||||
// Handle service worker updates
|
||||
registration.addEventListener('updatefound', () => {
|
||||
const newWorker = registration.installing;
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||
// New service worker available
|
||||
showUpdateNotification();
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('[PWA] Service Worker registration failed:', error);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showUpdateNotification() {
|
||||
const message = document.createElement('div');
|
||||
message.className = 'pwa-update-notification';
|
||||
message.innerHTML = `
|
||||
<div class="pwa-update-content">
|
||||
<span>Une nouvelle version d'ObsiGate est disponible !</span>
|
||||
<button class="pwa-update-btn" onclick="window.location.reload()">Mettre à jour</button>
|
||||
<button class="pwa-update-dismiss" onclick="this.parentElement.parentElement.remove()">×</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(message);
|
||||
|
||||
// Auto-dismiss after 30 seconds
|
||||
setTimeout(() => {
|
||||
if (message.parentElement) {
|
||||
message.remove();
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
// Handle install prompt
|
||||
let deferredPrompt;
|
||||
window.addEventListener('beforeinstallprompt', (e) => {
|
||||
e.preventDefault();
|
||||
deferredPrompt = e;
|
||||
|
||||
// Show install button if desired
|
||||
const installBtn = document.getElementById('pwa-install-btn');
|
||||
if (installBtn) {
|
||||
installBtn.style.display = 'block';
|
||||
installBtn.addEventListener('click', async () => {
|
||||
if (deferredPrompt) {
|
||||
deferredPrompt.prompt();
|
||||
const { outcome } = await deferredPrompt.userChoice;
|
||||
console.log(`[PWA] User response to install prompt: ${outcome}`);
|
||||
deferredPrompt = null;
|
||||
installBtn.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Log when app is installed
|
||||
window.addEventListener('appinstalled', () => {
|
||||
console.log('[PWA] ObsiGate has been installed');
|
||||
showToast('ObsiGate installé avec succès !');
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
init();
|
||||
registerServiceWorker();
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# ObsiGate PWA Icons
|
||||
|
||||
## Generated Icons
|
||||
|
||||
This directory contains PWA icons in SVG format.
|
||||
|
||||
### Converting to PNG
|
||||
|
||||
For production, convert these SVG files to PNG:
|
||||
|
||||
**Using ImageMagick:**
|
||||
```bash
|
||||
for file in *.svg; do
|
||||
size=$(echo $file | grep -oP '\d+x\d+' | head -1 | cut -d'x' -f1)
|
||||
convert -background none -resize ${size}x${size} "$file" "${file%.svg}.png"
|
||||
done
|
||||
```
|
||||
|
||||
**Using Inkscape:**
|
||||
```bash
|
||||
for file in *.svg; do
|
||||
size=$(echo $file | grep -oP '\d+x\d+' | head -1 | cut -d'x' -f1)
|
||||
inkscape "$file" --export-filename="${file%.svg}.png" --export-width=$size
|
||||
done
|
||||
```
|
||||
|
||||
**Online tools:**
|
||||
- https://cloudconvert.com/svg-to-png
|
||||
- https://convertio.co/svg-png/
|
||||
|
||||
### Icon Types
|
||||
|
||||
- **Regular icons**: Standard app icons with rounded corners
|
||||
- **Maskable icons**: Icons with safe zone padding for adaptive icons
|
||||
- **Search icon**: Icon for the search shortcut
|
||||
|
||||
### Sizes
|
||||
|
||||
- 72x72, 96x96, 128x128, 144x144, 152x152: Mobile devices
|
||||
- 192x192: Android home screen
|
||||
- 384x384: High-res displays
|
||||
- 512x512: Splash screens and high-DPI displays
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="128" height="128" rx="19.2" fill="url(#grad1)"/>
|
||||
|
||||
<!-- Book icon -->
|
||||
<g transform="translate(32.0, 25.6)">
|
||||
<!-- Book cover -->
|
||||
<rect x="0" y="0" width="64.0" height="76.8" rx="2.56" fill="white" opacity="0.95"/>
|
||||
|
||||
<!-- Book spine -->
|
||||
<rect x="6.4" y="0" width="2.56" height="76.8" fill="#1a1a1a" opacity="0.2"/>
|
||||
|
||||
<!-- Book pages lines -->
|
||||
<line x1="12.8" y1="19.2" x2="57.6" y2="19.2" stroke="#1a1a1a" stroke-width="1.28" opacity="0.3"/>
|
||||
<line x1="12.8" y1="32.0" x2="57.6" y2="32.0" stroke="#1a1a1a" stroke-width="1.28" opacity="0.3"/>
|
||||
<line x1="12.8" y1="44.8" x2="57.6" y2="44.8" stroke="#1a1a1a" stroke-width="1.28" opacity="0.3"/>
|
||||
<line x1="12.8" y1="57.6" x2="44.8" y2="57.6" stroke="#1a1a1a" stroke-width="1.28" opacity="0.3"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="144" height="144" rx="21.599999999999998" fill="url(#grad1)"/>
|
||||
|
||||
<!-- Book icon -->
|
||||
<g transform="translate(36.0, 28.8)">
|
||||
<!-- Book cover -->
|
||||
<rect x="0" y="0" width="72.0" height="86.39999999999999" rx="2.88" fill="white" opacity="0.95"/>
|
||||
|
||||
<!-- Book spine -->
|
||||
<rect x="7.2" y="0" width="2.88" height="86.39999999999999" fill="#1a1a1a" opacity="0.2"/>
|
||||
|
||||
<!-- Book pages lines -->
|
||||
<line x1="14.4" y1="21.599999999999998" x2="64.8" y2="21.599999999999998" stroke="#1a1a1a" stroke-width="1.44" opacity="0.3"/>
|
||||
<line x1="14.4" y1="36.0" x2="64.8" y2="36.0" stroke="#1a1a1a" stroke-width="1.44" opacity="0.3"/>
|
||||
<line x1="14.4" y1="50.4" x2="64.8" y2="50.4" stroke="#1a1a1a" stroke-width="1.44" opacity="0.3"/>
|
||||
<line x1="14.4" y1="64.8" x2="50.4" y2="64.8" stroke="#1a1a1a" stroke-width="1.44" opacity="0.3"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="152" height="152" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="152" height="152" rx="22.8" fill="url(#grad1)"/>
|
||||
|
||||
<!-- Book icon -->
|
||||
<g transform="translate(38.0, 30.400000000000002)">
|
||||
<!-- Book cover -->
|
||||
<rect x="0" y="0" width="76.0" height="91.2" rx="3.04" fill="white" opacity="0.95"/>
|
||||
|
||||
<!-- Book spine -->
|
||||
<rect x="7.6000000000000005" y="0" width="3.04" height="91.2" fill="#1a1a1a" opacity="0.2"/>
|
||||
|
||||
<!-- Book pages lines -->
|
||||
<line x1="15.200000000000001" y1="22.8" x2="68.4" y2="22.8" stroke="#1a1a1a" stroke-width="1.52" opacity="0.3"/>
|
||||
<line x1="15.200000000000001" y1="38.0" x2="68.4" y2="38.0" stroke="#1a1a1a" stroke-width="1.52" opacity="0.3"/>
|
||||
<line x1="15.200000000000001" y1="53.199999999999996" x2="68.4" y2="53.199999999999996" stroke="#1a1a1a" stroke-width="1.52" opacity="0.3"/>
|
||||
<line x1="15.200000000000001" y1="68.4" x2="53.199999999999996" y2="68.4" stroke="#1a1a1a" stroke-width="1.52" opacity="0.3"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="192" height="192" viewBox="0 0 192 192" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Full background for maskable -->
|
||||
<rect width="192" height="192" fill="url(#grad1)"/>
|
||||
|
||||
<!-- Book icon (centered with padding) -->
|
||||
<g transform="translate(57.25, 49.6)">
|
||||
<!-- Book cover -->
|
||||
<rect x="0" y="0" width="76.5" height="91.8" rx="3.06" fill="white" opacity="0.95"/>
|
||||
|
||||
<!-- Book spine -->
|
||||
<rect x="7.65" y="0" width="3.06" height="91.8" fill="#1a1a1a" opacity="0.2"/>
|
||||
|
||||
<!-- Book pages lines -->
|
||||
<line x1="15.3" y1="22.95" x2="68.85000000000001" y2="22.95" stroke="#1a1a1a" stroke-width="1.53" opacity="0.3"/>
|
||||
<line x1="15.3" y1="38.25" x2="68.85000000000001" y2="38.25" stroke="#1a1a1a" stroke-width="1.53" opacity="0.3"/>
|
||||
<line x1="15.3" y1="53.55" x2="68.85000000000001" y2="53.55" stroke="#1a1a1a" stroke-width="1.53" opacity="0.3"/>
|
||||
<line x1="15.3" y1="68.85000000000001" x2="53.55" y2="68.85000000000001" stroke="#1a1a1a" stroke-width="1.53" opacity="0.3"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="192" height="192" viewBox="0 0 192 192" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="192" height="192" rx="28.799999999999997" fill="url(#grad1)"/>
|
||||
|
||||
<!-- Book icon -->
|
||||
<g transform="translate(48.0, 38.400000000000006)">
|
||||
<!-- Book cover -->
|
||||
<rect x="0" y="0" width="96.0" height="115.19999999999999" rx="3.84" fill="white" opacity="0.95"/>
|
||||
|
||||
<!-- Book spine -->
|
||||
<rect x="9.600000000000001" y="0" width="3.84" height="115.19999999999999" fill="#1a1a1a" opacity="0.2"/>
|
||||
|
||||
<!-- Book pages lines -->
|
||||
<line x1="19.200000000000003" y1="28.799999999999997" x2="86.4" y2="28.799999999999997" stroke="#1a1a1a" stroke-width="1.92" opacity="0.3"/>
|
||||
<line x1="19.200000000000003" y1="48.0" x2="86.4" y2="48.0" stroke="#1a1a1a" stroke-width="1.92" opacity="0.3"/>
|
||||
<line x1="19.200000000000003" y1="67.19999999999999" x2="86.4" y2="67.19999999999999" stroke="#1a1a1a" stroke-width="1.92" opacity="0.3"/>
|
||||
<line x1="19.200000000000003" y1="86.4" x2="67.19999999999999" y2="86.4" stroke="#1a1a1a" stroke-width="1.92" opacity="0.3"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="384" height="384" viewBox="0 0 384 384" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="384" height="384" rx="57.599999999999994" fill="url(#grad1)"/>
|
||||
|
||||
<!-- Book icon -->
|
||||
<g transform="translate(96.0, 76.80000000000001)">
|
||||
<!-- Book cover -->
|
||||
<rect x="0" y="0" width="192.0" height="230.39999999999998" rx="7.68" fill="white" opacity="0.95"/>
|
||||
|
||||
<!-- Book spine -->
|
||||
<rect x="19.200000000000003" y="0" width="7.68" height="230.39999999999998" fill="#1a1a1a" opacity="0.2"/>
|
||||
|
||||
<!-- Book pages lines -->
|
||||
<line x1="38.400000000000006" y1="57.599999999999994" x2="172.8" y2="57.599999999999994" stroke="#1a1a1a" stroke-width="3.84" opacity="0.3"/>
|
||||
<line x1="38.400000000000006" y1="96.0" x2="172.8" y2="96.0" stroke="#1a1a1a" stroke-width="3.84" opacity="0.3"/>
|
||||
<line x1="38.400000000000006" y1="134.39999999999998" x2="172.8" y2="134.39999999999998" stroke="#1a1a1a" stroke-width="3.84" opacity="0.3"/>
|
||||
<line x1="38.400000000000006" y1="172.8" x2="134.39999999999998" y2="172.8" stroke="#1a1a1a" stroke-width="3.84" opacity="0.3"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Full background for maskable -->
|
||||
<rect width="512" height="512" fill="url(#grad1)"/>
|
||||
|
||||
<!-- Book icon (centered with padding) -->
|
||||
<g transform="translate(153.25, 132.8)">
|
||||
<!-- Book cover -->
|
||||
<rect x="0" y="0" width="204.5" height="245.39999999999998" rx="8.18" fill="white" opacity="0.95"/>
|
||||
|
||||
<!-- Book spine -->
|
||||
<rect x="20.450000000000003" y="0" width="8.18" height="245.39999999999998" fill="#1a1a1a" opacity="0.2"/>
|
||||
|
||||
<!-- Book pages lines -->
|
||||
<line x1="40.900000000000006" y1="61.349999999999994" x2="184.05" y2="61.349999999999994" stroke="#1a1a1a" stroke-width="4.09" opacity="0.3"/>
|
||||
<line x1="40.900000000000006" y1="102.25" x2="184.05" y2="102.25" stroke="#1a1a1a" stroke-width="4.09" opacity="0.3"/>
|
||||
<line x1="40.900000000000006" y1="143.14999999999998" x2="184.05" y2="143.14999999999998" stroke="#1a1a1a" stroke-width="4.09" opacity="0.3"/>
|
||||
<line x1="40.900000000000006" y1="184.05" x2="143.14999999999998" y2="184.05" stroke="#1a1a1a" stroke-width="4.09" opacity="0.3"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="512" height="512" rx="76.8" fill="url(#grad1)"/>
|
||||
|
||||
<!-- Book icon -->
|
||||
<g transform="translate(128.0, 102.4)">
|
||||
<!-- Book cover -->
|
||||
<rect x="0" y="0" width="256.0" height="307.2" rx="10.24" fill="white" opacity="0.95"/>
|
||||
|
||||
<!-- Book spine -->
|
||||
<rect x="25.6" y="0" width="10.24" height="307.2" fill="#1a1a1a" opacity="0.2"/>
|
||||
|
||||
<!-- Book pages lines -->
|
||||
<line x1="51.2" y1="76.8" x2="230.4" y2="76.8" stroke="#1a1a1a" stroke-width="5.12" opacity="0.3"/>
|
||||
<line x1="51.2" y1="128.0" x2="230.4" y2="128.0" stroke="#1a1a1a" stroke-width="5.12" opacity="0.3"/>
|
||||
<line x1="51.2" y1="179.2" x2="230.4" y2="179.2" stroke="#1a1a1a" stroke-width="5.12" opacity="0.3"/>
|
||||
<line x1="51.2" y1="230.4" x2="179.2" y2="230.4" stroke="#1a1a1a" stroke-width="5.12" opacity="0.3"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="72" height="72" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="72" height="72" rx="10.799999999999999" fill="url(#grad1)"/>
|
||||
|
||||
<!-- Book icon -->
|
||||
<g transform="translate(18.0, 14.4)">
|
||||
<!-- Book cover -->
|
||||
<rect x="0" y="0" width="36.0" height="43.199999999999996" rx="1.44" fill="white" opacity="0.95"/>
|
||||
|
||||
<!-- Book spine -->
|
||||
<rect x="3.6" y="0" width="1.44" height="43.199999999999996" fill="#1a1a1a" opacity="0.2"/>
|
||||
|
||||
<!-- Book pages lines -->
|
||||
<line x1="7.2" y1="10.799999999999999" x2="32.4" y2="10.799999999999999" stroke="#1a1a1a" stroke-width="0.72" opacity="0.3"/>
|
||||
<line x1="7.2" y1="18.0" x2="32.4" y2="18.0" stroke="#1a1a1a" stroke-width="0.72" opacity="0.3"/>
|
||||
<line x1="7.2" y1="25.2" x2="32.4" y2="25.2" stroke="#1a1a1a" stroke-width="0.72" opacity="0.3"/>
|
||||
<line x1="7.2" y1="32.4" x2="25.2" y2="32.4" stroke="#1a1a1a" stroke-width="0.72" opacity="0.3"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="96" height="96" rx="14.399999999999999" fill="url(#grad1)"/>
|
||||
|
||||
<!-- Book icon -->
|
||||
<g transform="translate(24.0, 19.200000000000003)">
|
||||
<!-- Book cover -->
|
||||
<rect x="0" y="0" width="48.0" height="57.599999999999994" rx="1.92" fill="white" opacity="0.95"/>
|
||||
|
||||
<!-- Book spine -->
|
||||
<rect x="4.800000000000001" y="0" width="1.92" height="57.599999999999994" fill="#1a1a1a" opacity="0.2"/>
|
||||
|
||||
<!-- Book pages lines -->
|
||||
<line x1="9.600000000000001" y1="14.399999999999999" x2="43.2" y2="14.399999999999999" stroke="#1a1a1a" stroke-width="0.96" opacity="0.3"/>
|
||||
<line x1="9.600000000000001" y1="24.0" x2="43.2" y2="24.0" stroke="#1a1a1a" stroke-width="0.96" opacity="0.3"/>
|
||||
<line x1="9.600000000000001" y1="33.599999999999994" x2="43.2" y2="33.599999999999994" stroke="#1a1a1a" stroke-width="0.96" opacity="0.3"/>
|
||||
<line x1="9.600000000000001" y1="43.2" x2="33.599999999999994" y2="43.2" stroke="#1a1a1a" stroke-width="0.96" opacity="0.3"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="96" height="96" rx="14.399999999999999" fill="url(#grad1)"/>
|
||||
|
||||
<!-- Search icon -->
|
||||
<g transform="translate(19.200000000000003, 19.200000000000003)">
|
||||
<circle cx="24.0" cy="24.0" r="17.28" fill="none" stroke="white" stroke-width="4.800000000000001" opacity="0.95"/>
|
||||
<line x1="36.480000000000004" y1="36.480000000000004" x2="48.0" y2="48.0" stroke="white" stroke-width="4.800000000000001" stroke-linecap="round" opacity="0.95"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 865 B |
@@ -4,6 +4,26 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ObsiGate</title>
|
||||
|
||||
<!-- PWA Meta Tags -->
|
||||
<meta name="description" content="Porte d'entrée web pour vos vaults Obsidian - Accédez, naviguez et recherchez dans toutes vos notes">
|
||||
<meta name="theme-color" content="#2563eb">
|
||||
<meta name="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-title" content="ObsiGate">
|
||||
|
||||
<!-- PWA Manifest -->
|
||||
<link rel="manifest" href="/static/manifest.json">
|
||||
|
||||
<!-- Apple Touch Icons -->
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/static/icons/icon-152x152.svg">
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/static/icons/icon-192x192.svg">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/svg+xml" href="/static/icons/icon-72x72.svg">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/static/icons/icon-192x192.svg">
|
||||
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" id="hljs-theme-dark">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css" id="hljs-theme-light" disabled>
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"name": "ObsiGate",
|
||||
"short_name": "ObsiGate",
|
||||
"description": "Porte d'entrée web pour vos vaults Obsidian - Accédez, naviguez et recherchez dans toutes vos notes",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#1a1a1a",
|
||||
"theme_color": "#2563eb",
|
||||
"orientation": "any",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/icons/icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/icon-128x128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/icon-152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/icon-192x192-maskable.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/icon-512x512-maskable.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"categories": ["productivity", "utilities"],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/static/screenshots/desktop.png",
|
||||
"sizes": "1280x720",
|
||||
"type": "image/png",
|
||||
"form_factor": "wide"
|
||||
},
|
||||
{
|
||||
"src": "/static/screenshots/mobile.png",
|
||||
"sizes": "750x1334",
|
||||
"type": "image/png",
|
||||
"form_factor": "narrow"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Recherche",
|
||||
"short_name": "Recherche",
|
||||
"description": "Rechercher dans vos vaults",
|
||||
"url": "/?action=search",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/icons/search-96x96.png",
|
||||
"sizes": "96x96"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"prefer_related_applications": false
|
||||
}
|
||||
@@ -4088,3 +4088,121 @@ body.popup-mode .content-area {
|
||||
padding-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== PWA STYLES ===== */
|
||||
|
||||
/* PWA Update Notification */
|
||||
.pwa-update-notification {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: 10000;
|
||||
animation: slideInUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
.pwa-update-content {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
padding: 16px 20px;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.pwa-update-content span {
|
||||
flex: 1;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.pwa-update-btn {
|
||||
background: white;
|
||||
color: var(--accent);
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pwa-update-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.pwa-update-dismiss {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.pwa-update-dismiss:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* PWA Install Button (optional - can be added to header) */
|
||||
#pwa-install-btn {
|
||||
display: none;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
#pwa-install-btn:hover {
|
||||
background: var(--accent-green);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@media (max-width: 768px) {
|
||||
.pwa-update-notification {
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.pwa-update-content {
|
||||
padding: 12px 16px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.pwa-update-btn {
|
||||
padding: 6px 12px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
/* ObsiGate Service Worker - PWA Support */
|
||||
|
||||
const CACHE_VERSION = 'obsigate-v1.4.0';
|
||||
const STATIC_CACHE = `${CACHE_VERSION}-static`;
|
||||
const DYNAMIC_CACHE = `${CACHE_VERSION}-dynamic`;
|
||||
const MAX_DYNAMIC_CACHE_SIZE = 50;
|
||||
|
||||
// Assets to cache on install
|
||||
const STATIC_ASSETS = [
|
||||
'/',
|
||||
'/static/index.html',
|
||||
'/static/app.js',
|
||||
'/static/style.css',
|
||||
'/static/manifest.json'
|
||||
];
|
||||
|
||||
// Install event - cache static assets
|
||||
self.addEventListener('install', (event) => {
|
||||
console.log('[SW] Installing service worker...');
|
||||
event.waitUntil(
|
||||
caches.open(STATIC_CACHE)
|
||||
.then((cache) => {
|
||||
console.log('[SW] Caching static assets');
|
||||
return cache.addAll(STATIC_ASSETS.map(url => new Request(url, { cache: 'reload' })));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('[SW] Failed to cache static assets:', err);
|
||||
})
|
||||
.then(() => self.skipWaiting())
|
||||
);
|
||||
});
|
||||
|
||||
// Activate event - clean up old caches
|
||||
self.addEventListener('activate', (event) => {
|
||||
console.log('[SW] Activating service worker...');
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames
|
||||
.filter((name) => name.startsWith('obsigate-') && name !== STATIC_CACHE && name !== DYNAMIC_CACHE)
|
||||
.map((name) => {
|
||||
console.log('[SW] Deleting old cache:', name);
|
||||
return caches.delete(name);
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
// Fetch event - serve from cache, fallback to network
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Skip non-GET requests
|
||||
if (request.method !== 'GET') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip SSE connections
|
||||
if (url.pathname === '/api/events') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip authentication endpoints
|
||||
if (url.pathname.startsWith('/api/auth/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// API requests - Network first, cache fallback
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
event.respondWith(networkFirstStrategy(request));
|
||||
return;
|
||||
}
|
||||
|
||||
// Static assets - Cache first, network fallback
|
||||
event.respondWith(cacheFirstStrategy(request));
|
||||
});
|
||||
|
||||
// Cache first strategy (for static assets)
|
||||
async function cacheFirstStrategy(request) {
|
||||
try {
|
||||
const cachedResponse = await caches.match(request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
const networkResponse = await fetch(request);
|
||||
|
||||
// Cache successful responses
|
||||
if (networkResponse && networkResponse.status === 200) {
|
||||
const cache = await caches.open(STATIC_CACHE);
|
||||
cache.put(request, networkResponse.clone());
|
||||
}
|
||||
|
||||
return networkResponse;
|
||||
} catch (error) {
|
||||
console.error('[SW] Cache first strategy failed:', error);
|
||||
|
||||
// Return offline page if available
|
||||
const offlinePage = await caches.match('/static/index.html');
|
||||
if (offlinePage) {
|
||||
return offlinePage;
|
||||
}
|
||||
|
||||
return new Response('Offline - Unable to fetch resource', {
|
||||
status: 503,
|
||||
statusText: 'Service Unavailable',
|
||||
headers: new Headers({
|
||||
'Content-Type': 'text/plain'
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Network first strategy (for API calls)
|
||||
async function networkFirstStrategy(request) {
|
||||
try {
|
||||
const networkResponse = await fetch(request);
|
||||
|
||||
// Cache successful GET responses
|
||||
if (networkResponse && networkResponse.status === 200 && request.method === 'GET') {
|
||||
const cache = await caches.open(DYNAMIC_CACHE);
|
||||
cache.put(request, networkResponse.clone());
|
||||
|
||||
// Limit dynamic cache size
|
||||
limitCacheSize(DYNAMIC_CACHE, MAX_DYNAMIC_CACHE_SIZE);
|
||||
}
|
||||
|
||||
return networkResponse;
|
||||
} catch (error) {
|
||||
console.log('[SW] Network failed, trying cache:', request.url);
|
||||
|
||||
const cachedResponse = await caches.match(request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// Return offline response
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Offline',
|
||||
message: 'Unable to fetch data. Please check your connection.'
|
||||
}), {
|
||||
status: 503,
|
||||
statusText: 'Service Unavailable',
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Limit cache size
|
||||
async function limitCacheSize(cacheName, maxSize) {
|
||||
const cache = await caches.open(cacheName);
|
||||
const keys = await cache.keys();
|
||||
|
||||
if (keys.length > maxSize) {
|
||||
// Delete oldest entries
|
||||
const deleteCount = keys.length - maxSize;
|
||||
for (let i = 0; i < deleteCount; i++) {
|
||||
await cache.delete(keys[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Message event - handle messages from clients
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
|
||||
if (event.data && event.data.type === 'CLEAR_CACHE') {
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((name) => caches.delete(name))
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Sync event - background sync for offline actions
|
||||
self.addEventListener('sync', (event) => {
|
||||
console.log('[SW] Background sync:', event.tag);
|
||||
|
||||
if (event.tag === 'sync-data') {
|
||||
event.waitUntil(syncData());
|
||||
}
|
||||
});
|
||||
|
||||
async function syncData() {
|
||||
// Placeholder for background sync logic
|
||||
console.log('[SW] Syncing data...');
|
||||
}
|
||||
|
||||
// Push notification event
|
||||
self.addEventListener('push', (event) => {
|
||||
const options = {
|
||||
body: event.data ? event.data.text() : 'New update available',
|
||||
icon: '/static/icons/icon-192x192.png',
|
||||
badge: '/static/icons/icon-72x72.png',
|
||||
vibrate: [200, 100, 200],
|
||||
data: {
|
||||
dateOfArrival: Date.now(),
|
||||
primaryKey: 1
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
action: 'explore',
|
||||
title: 'Ouvrir ObsiGate'
|
||||
},
|
||||
{
|
||||
action: 'close',
|
||||
title: 'Fermer'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification('ObsiGate', options)
|
||||
);
|
||||
});
|
||||
|
||||
// Notification click event
|
||||
self.addEventListener('notificationclick', (event) => {
|
||||
event.notification.close();
|
||||
|
||||
if (event.action === 'explore') {
|
||||
event.waitUntil(
|
||||
clients.openWindow('/')
|
||||
);
|
||||
}
|
||||
});
|
||||