mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2025-12-21 13:00:12 -06:00
feat(pwa): better offline page and offline request handler
This commit is contained in:
@@ -377,6 +377,7 @@ PWA_APP_SCREENSHOTS = [
|
||||
"type": "image/png",
|
||||
},
|
||||
]
|
||||
PWA_SERVICE_WORKER_PATH = BASE_DIR / "templates" / "pwa" / "serviceworker.js"
|
||||
|
||||
ENABLE_SOFT_DELETE = os.getenv("ENABLE_SOFT_DELETE", "false").lower() == "true"
|
||||
KEEP_DELETED_TRANSACTIONS_FOR = int(os.getenv("KEEP_DELETED_ENTRIES_FOR", "365"))
|
||||
|
||||
79
app/templates/offline.html
Normal file
79
app/templates/offline.html
Normal file
@@ -0,0 +1,79 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Offline</title>
|
||||
<style>
|
||||
.offline, body {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #222;
|
||||
color: #fbb700;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.wifi-icon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
@keyframes flash {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
.flashing {
|
||||
animation: flash 1s infinite;
|
||||
}
|
||||
#offline-countdown {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="offline">
|
||||
<svg class="wifi-icon flashing" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.08 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z" fill="#fbb700"/>
|
||||
<path d="M23 21L1 3" stroke="#fbb700" stroke-width="2"/>
|
||||
</svg>
|
||||
<p>Either you or your WYGIWYH instance is offline.</p>
|
||||
<div id="offline-countdown"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function attemptReload() {
|
||||
const countdownElement = document.getElementById('offline-countdown');
|
||||
let secondsLeft = 30;
|
||||
|
||||
function updateCountdown() {
|
||||
countdownElement.textContent = `Retrying in ${secondsLeft} seconds...`;
|
||||
secondsLeft--;
|
||||
|
||||
if (secondsLeft < 0) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
setTimeout(updateCountdown, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
updateCountdown();
|
||||
}
|
||||
|
||||
// Start the reload attempt process immediately
|
||||
attemptReload();
|
||||
|
||||
// Also attempt reload when coming back online
|
||||
window.addEventListener('online', () => {
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
// For HTMX compatibility
|
||||
document.body.addEventListener('htmx:load', attemptReload);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
74
app/templates/pwa/serviceworker.js
Normal file
74
app/templates/pwa/serviceworker.js
Normal file
@@ -0,0 +1,74 @@
|
||||
// Base Service Worker implementation. To use your own Service Worker, set the PWA_SERVICE_WORKER_PATH variable in settings.py
|
||||
|
||||
var staticCacheName = "django-pwa-v" + new Date().getTime();
|
||||
var filesToCache = [
|
||||
'/offline/',
|
||||
'/static/css/django-pwa-app.css',
|
||||
'/static/img/favicon/android-icon-192x192.png',
|
||||
'/static/img/favicon/apple-icon-180x180.png',
|
||||
'/static/img/pwa/splash-640x1136.png',
|
||||
'/static/img/pwa/splash-750x1334.png',
|
||||
];
|
||||
|
||||
// Cache on install
|
||||
self.addEventListener("install", event => {
|
||||
this.skipWaiting();
|
||||
event.waitUntil(
|
||||
caches.open(staticCacheName)
|
||||
.then(cache => {
|
||||
return cache.addAll(filesToCache);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Clear cache on activate
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys().then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames
|
||||
.filter(cacheName => (cacheName.startsWith("django-pwa-")))
|
||||
.filter(cacheName => (cacheName !== staticCacheName))
|
||||
.map(cacheName => caches.delete(cacheName))
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Serve from Cache
|
||||
self.addEventListener("fetch", event => {
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then(response => {
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
return fetch(event.request).catch(() => {
|
||||
const isHtmxRequest = event.request.headers.get('HX-Request') === 'true';
|
||||
const isHtmxBoosted = event.request.headers.get('HX-Boosted') === 'true';
|
||||
|
||||
if (!isHtmxRequest || isHtmxBoosted) {
|
||||
// Serve offline content without changing URL
|
||||
return caches.match('/offline/').then(offlineResponse => {
|
||||
if (offlineResponse) {
|
||||
return offlineResponse.text().then(offlineText => {
|
||||
return new Response(offlineText, {
|
||||
status: 200,
|
||||
headers: {'Content-Type': 'text/html'}
|
||||
});
|
||||
});
|
||||
}
|
||||
// If offline page is not in cache, return a simple offline message
|
||||
return new Response('<h1>Offline</h1><p>The page is not available offline.</p>', {
|
||||
status: 200,
|
||||
headers: {'Content-Type': 'text/html'}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// For non-boosted HTMX requests, let it fail normally
|
||||
throw new Error('Network request failed');
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -58,13 +58,21 @@
|
||||
|
||||
// HTMX Loading
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.show-loading.htmx-request {
|
||||
@@ -103,7 +111,7 @@
|
||||
}
|
||||
|
||||
.swing-out-top-bck {
|
||||
animation: swing-out-top-bck 0.45s cubic-bezier(0.600, -0.280, 0.735, 0.045) both;
|
||||
animation: swing-out-top-bck 0.45s cubic-bezier(0.600, -0.280, 0.735, 0.045) both;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------
|
||||
@@ -155,7 +163,7 @@
|
||||
}
|
||||
|
||||
.scale-in-center {
|
||||
animation: scale-in-center 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
|
||||
animation: scale-in-center 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------
|
||||
@@ -182,5 +190,18 @@
|
||||
}
|
||||
|
||||
.scale-out-center {
|
||||
animation: scale-out-center 0.5s cubic-bezier(0.550, 0.085, 0.680, 0.530) both;
|
||||
animation: scale-out-center 0.5s cubic-bezier(0.550, 0.085, 0.680, 0.530) both;
|
||||
}
|
||||
|
||||
@keyframes flash {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.flashing {
|
||||
animation: flash 1s infinite;
|
||||
}
|
||||
|
||||
@@ -53,3 +53,27 @@ select[multiple] {
|
||||
.transaction:has(input[type="checkbox"]:checked) > .transaction-item {
|
||||
background-color: $primary-bg-subtle-dark;
|
||||
}
|
||||
|
||||
.offline {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #222;
|
||||
color: #fbb700;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
.wifi-icon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
#offline-countdown {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user