Skip to content

Commit ac046b7

Browse files
committed
feat: add offline-first PWA support for web
Enable the web app to work without internet after the first visit by configuring Flutter's service worker with offline-first strategy. - Build web with --pwa-strategy=offline-first in deploy and Makefile - Add inline loading screen with i18n offline status banner (7 langs) - Improve manifest.json with scope, id and categories for better PWA - Gate deploy workflow on CI success via reusable workflow_call
1 parent e58c20f commit ac046b7

5 files changed

Lines changed: 112 additions & 4 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches: [main]
66
pull_request:
77
branches: [main]
8+
workflow_call:
89

910
concurrency:
1011
group: ci-${{ github.ref }}

.github/workflows/deploy.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ concurrency:
1414
cancel-in-progress: false
1515

1616
jobs:
17+
ci:
18+
uses: ./.github/workflows/ci.yml
19+
1720
build:
21+
needs: ci
1822
runs-on: ubuntu-latest
1923
steps:
2024
- uses: actions/checkout@v6
@@ -28,7 +32,7 @@ jobs:
2832
run: flutter pub get
2933

3034
- name: Build web
31-
run: flutter build web --release --base-href "/${{ github.event.repository.name }}/"
35+
run: flutter build web --release --pwa-strategy=offline-first --base-href "/${{ github.event.repository.name }}/"
3236

3337
- name: Upload artifact
3438
uses: actions/upload-pages-artifact@v4

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: help format format-check analyze pubget run-web clean test
1+
.PHONY: help format format-check analyze pubget run-web build-web clean test
22

33
help:
44
@echo "Available targets:"
@@ -8,6 +8,7 @@ help:
88
@echo " make test - Run flutter test"
99
@echo " make pubget - Run flutter pub get"
1010
@echo " make run-web - Run flutter run"
11+
@echo " make build-web - Build web with offline-first PWA"
1112
@echo " make clean - Run flutter clean"
1213

1314
format:
@@ -28,6 +29,9 @@ pubget:
2829
run-web:
2930
flutter run -d web-server --web-port=8080 --web-hostname=0.0.0.0
3031

32+
build-web:
33+
flutter build web --release --pwa-strategy=offline-first
34+
3135
clean:
3236
flutter clean
3337

web/index.html

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!DOCTYPE html>
2-
<html>
2+
<html lang="en">
33

44
<head>
55
<!--
@@ -37,10 +37,106 @@
3737

3838
<title>PictoTap</title>
3939
<link rel="manifest" href="manifest.json">
40+
41+
<style>
42+
body {
43+
margin: 0;
44+
padding: 0;
45+
background-color: #673AB7;
46+
overflow: hidden;
47+
}
48+
49+
#loading {
50+
display: flex;
51+
flex-direction: column;
52+
align-items: center;
53+
justify-content: center;
54+
height: 100vh;
55+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
56+
color: #FFFFFF;
57+
text-align: center;
58+
padding: 0 24px;
59+
box-sizing: border-box;
60+
}
61+
62+
#loading h1 {
63+
font-size: 28px;
64+
font-weight: 700;
65+
margin: 16px 0 8px;
66+
letter-spacing: 0.5px;
67+
}
68+
69+
#loading .spinner {
70+
width: 40px;
71+
height: 40px;
72+
border: 4px solid rgba(255, 255, 255, 0.3);
73+
border-top-color: #FFFFFF;
74+
border-radius: 50%;
75+
animation: spin 0.8s linear infinite;
76+
margin-top: 24px;
77+
}
78+
79+
@keyframes spin {
80+
to { transform: rotate(360deg); }
81+
}
82+
83+
#offline-banner {
84+
display: none;
85+
background-color: rgba(0, 0, 0, 0.2);
86+
color: #FFFFFF;
87+
padding: 8px 16px;
88+
border-radius: 8px;
89+
margin-top: 16px;
90+
font-size: 14px;
91+
}
92+
</style>
4093
</head>
4194

4295
<body>
96+
<div id="loading" aria-label="Loading PictoTap">
97+
<h1>PictoTap</h1>
98+
<div class="spinner" aria-hidden="true"></div>
99+
<div id="offline-banner" role="status" aria-live="polite"></div>
100+
</div>
101+
102+
<script>
103+
const offlineBanner = document.getElementById('offline-banner');
104+
105+
const offlineMessages = {
106+
en: { cached: 'Offline — loading from cache', noCached: 'Offline — a first load with internet is required' },
107+
es: { cached: 'Sin conexión — cargando desde caché', noCached: 'Sin conexión — se necesita una primera carga con internet' },
108+
ca: { cached: 'Sense connexió — carregant des de la memòria cau', noCached: 'Sense connexió — cal una primera càrrega amb internet' },
109+
eu: { cached: 'Konexiorik gabe — cachetik kargatzen', noCached: 'Konexiorik gabe — lehen aldiz internetarekin kargatu behar da' },
110+
fr: { cached: 'Hors ligne — chargement depuis le cache', noCached: 'Hors ligne — un premier chargement avec internet est nécessaire' },
111+
gl: { cached: 'Sen conexión — cargando desde a caché', noCached: 'Sen conexión — precísase unha primeira carga con internet' },
112+
pt: { cached: 'Sem conexão — carregando do cache', noCached: 'Sem conexão — é necessário um primeiro carregamento com internet' },
113+
};
114+
115+
function getMessages() {
116+
const lang = (navigator.language || 'en').split('-')[0];
117+
return offlineMessages[lang] || offlineMessages['en'];
118+
}
119+
120+
function updateOfflineStatus() {
121+
if (!navigator.onLine) {
122+
const msg = getMessages();
123+
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
124+
offlineBanner.textContent = msg.cached;
125+
} else {
126+
offlineBanner.textContent = msg.noCached;
127+
}
128+
offlineBanner.style.display = 'block';
129+
} else {
130+
offlineBanner.style.display = 'none';
131+
}
132+
}
133+
134+
window.addEventListener('online', updateOfflineStatus);
135+
window.addEventListener('offline', updateOfflineStatus);
136+
updateOfflineStatus();
137+
</script>
138+
43139
<script src="flutter_bootstrap.js" async></script>
44140
</body>
45141

46-
</html>
142+
</html>

web/manifest.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
"name": "PictoTap",
33
"short_name": "PictoTap",
44
"start_url": ".",
5+
"scope": ".",
6+
"id": "/",
57
"display": "standalone",
68
"background_color": "#673AB7",
79
"theme_color": "#673AB7",
810
"description": "A communication app for accessibility using pictograms.",
911
"orientation": "portrait-primary",
1012
"prefer_related_applications": false,
13+
"categories": ["education", "health", "accessibility"],
1114
"icons": [
1215
{
1316
"src": "icons/Icon-192.png",

0 commit comments

Comments
 (0)