mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-04-25 10:08:36 +02: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",
|
"type": "image/png",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
PWA_SERVICE_WORKER_PATH = BASE_DIR / "templates" / "pwa" / "serviceworker.js"
|
||||||
|
|
||||||
ENABLE_SOFT_DELETE = os.getenv("ENABLE_SOFT_DELETE", "false").lower() == "true"
|
ENABLE_SOFT_DELETE = os.getenv("ENABLE_SOFT_DELETE", "false").lower() == "true"
|
||||||
KEEP_DELETED_TRANSACTIONS_FOR = int(os.getenv("KEEP_DELETED_ENTRIES_FOR", "365"))
|
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
|
// HTMX Loading
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% {
|
||||||
100% { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fade-in {
|
@keyframes fade-in {
|
||||||
0% { opacity: 0; }
|
0% {
|
||||||
100% { opacity: 1; }
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.show-loading.htmx-request {
|
.show-loading.htmx-request {
|
||||||
@@ -103,7 +111,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.swing-out-top-bck {
|
.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 {
|
.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 {
|
.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 {
|
.transaction:has(input[type="checkbox"]:checked) > .transaction-item {
|
||||||
background-color: $primary-bg-subtle-dark;
|
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