Skip to content

Commit 36a31b1

Browse files
committed
Offline mode
1 parent 6a31d2d commit 36a31b1

File tree

3 files changed

+186
-0
lines changed

3 files changed

+186
-0
lines changed

composer.html

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
<span class="brand-subtitle">Settings HEX Composer</span>
2828
</div>
2929
</div>
30+
<div id="sw-cache-indicator" class="sw-cache-indicator hidden" aria-live="polite" aria-label="Offline cache status">
31+
<div class="sw-spinner" aria-hidden="true"></div>
32+
</div>
3033
<div class="app-menu">
3134
<button id="app-menu-toggle" class="menu-toggle" aria-expanded="false" aria-controls="app-menu-panel" aria-label="Open menu">
3235
<svg class="menu-toggle-icon" viewBox="0 0 448 512" aria-hidden="true" focusable="false">
@@ -199,6 +202,81 @@ <h3 id="import-preview-title">Import preview</h3>
199202
});
200203
}
201204

205+
function setSwCacheBusy(isBusy) {
206+
const indicator = document.getElementById('sw-cache-indicator');
207+
if (!indicator) {
208+
return;
209+
}
210+
indicator.classList.toggle('hidden', !isBusy);
211+
}
212+
213+
function getRequiredCacheUrls() {
214+
const urls = [
215+
'settings-meta.json',
216+
'device-version-notes.json'
217+
];
218+
const settingsFiles = Array.isArray(window.SETTINGS_FILES)
219+
? window.SETTINGS_FILES.map(file => `settings/${file}`)
220+
: [];
221+
return Array.from(new Set([...urls, ...settingsFiles]));
222+
}
223+
224+
async function getMissingCacheUrls(urls) {
225+
if (!('caches' in window)) {
226+
return [];
227+
}
228+
const missing = [];
229+
for (const url of urls) {
230+
try {
231+
const match = await caches.match(url);
232+
if (!match) {
233+
missing.push(url);
234+
}
235+
} catch (error) {
236+
missing.push(url);
237+
}
238+
}
239+
return missing;
240+
}
241+
242+
async function warmMissingUrls(urls) {
243+
if (!navigator.onLine || !urls.length) {
244+
return;
245+
}
246+
await Promise.all(
247+
urls.map((url) =>
248+
fetch(url, { cache: 'reload' }).catch(() => null)
249+
)
250+
);
251+
}
252+
253+
let swCacheLoopId = null;
254+
async function ensureCacheReadyLoop() {
255+
const urls = getRequiredCacheUrls();
256+
const missing = await getMissingCacheUrls(urls);
257+
if (!missing.length) {
258+
setSwCacheBusy(false);
259+
if (swCacheLoopId) {
260+
clearTimeout(swCacheLoopId);
261+
swCacheLoopId = null;
262+
}
263+
return;
264+
}
265+
setSwCacheBusy(true);
266+
await warmMissingUrls(missing);
267+
swCacheLoopId = setTimeout(ensureCacheReadyLoop, 2000);
268+
}
269+
270+
function initSwCacheIndicator() {
271+
if (!('serviceWorker' in navigator)) {
272+
return;
273+
}
274+
setSwCacheBusy(true);
275+
navigator.serviceWorker.ready
276+
.then(() => ensureCacheReadyLoop())
277+
.catch(() => null);
278+
}
279+
202280
// ---------------------------------------------
203281
// Load and Display Settings
204282
// ---------------------------------------------
@@ -224,6 +302,7 @@ <h3 id="import-preview-title">Import preview</h3>
224302
const COMPOSER_MESSAGING_MAX_LEN = 46;
225303

226304
initAppMenu();
305+
initSwCacheIndicator();
227306

228307
let composerDeviceVersionNotes = null;
229308
let composerDeviceVersionNotesLoading = null;

index.html

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@
4444
<span class="brand-subtitle">OpenCollar Edge BLE Web Connect</span>
4545
</div>
4646
</div>
47+
<div id="sw-cache-indicator" class="sw-cache-indicator hidden" aria-live="polite" aria-label="Offline cache status">
48+
<div class="sw-spinner" aria-hidden="true"></div>
49+
</div>
4750
<div class="app-menu hidden">
4851
<button id="app-menu-toggle" class="menu-toggle" aria-expanded="false" aria-controls="app-menu-panel" aria-label="Open menu">
4952
<svg class="menu-toggle-icon" viewBox="0 0 448 512" aria-hidden="true" focusable="false">
@@ -1108,6 +1111,82 @@ <h3 id="import-preview-title">Import preview</h3>
11081111
});
11091112
}
11101113

1114+
function setSwCacheBusy(isBusy) {
1115+
const indicator = document.getElementById('sw-cache-indicator');
1116+
if (!indicator) {
1117+
return;
1118+
}
1119+
indicator.classList.toggle('hidden', !isBusy);
1120+
}
1121+
1122+
function getRequiredCacheUrls() {
1123+
const urls = [
1124+
'settings-meta.json',
1125+
'device-version-notes.json',
1126+
'assets/dfu/manifest.json'
1127+
];
1128+
const settingsFiles = Array.isArray(window.SETTINGS_FILES)
1129+
? window.SETTINGS_FILES.map(file => `settings/${file}`)
1130+
: [];
1131+
return Array.from(new Set([...urls, ...settingsFiles]));
1132+
}
1133+
1134+
async function getMissingCacheUrls(urls) {
1135+
if (!('caches' in window)) {
1136+
return [];
1137+
}
1138+
const missing = [];
1139+
for (const url of urls) {
1140+
try {
1141+
const match = await caches.match(url);
1142+
if (!match) {
1143+
missing.push(url);
1144+
}
1145+
} catch (error) {
1146+
missing.push(url);
1147+
}
1148+
}
1149+
return missing;
1150+
}
1151+
1152+
async function warmMissingUrls(urls) {
1153+
if (!navigator.onLine || !urls.length) {
1154+
return;
1155+
}
1156+
await Promise.all(
1157+
urls.map((url) =>
1158+
fetch(url, { cache: 'reload' }).catch(() => null)
1159+
)
1160+
);
1161+
}
1162+
1163+
let swCacheLoopId = null;
1164+
async function ensureCacheReadyLoop() {
1165+
const urls = getRequiredCacheUrls();
1166+
const missing = await getMissingCacheUrls(urls);
1167+
if (!missing.length) {
1168+
setSwCacheBusy(false);
1169+
if (swCacheLoopId) {
1170+
clearTimeout(swCacheLoopId);
1171+
swCacheLoopId = null;
1172+
}
1173+
return;
1174+
}
1175+
setSwCacheBusy(true);
1176+
await warmMissingUrls(missing);
1177+
swCacheLoopId = setTimeout(ensureCacheReadyLoop, 2000);
1178+
}
1179+
1180+
function initSwCacheIndicator() {
1181+
if (!('serviceWorker' in navigator)) {
1182+
return;
1183+
}
1184+
setSwCacheBusy(true);
1185+
navigator.serviceWorker.ready
1186+
.then(() => ensureCacheReadyLoop())
1187+
.catch(() => null);
1188+
}
1189+
11111190
function setMapTracksOverlayVisible(visible) {
11121191
const overlay = document.getElementById('map-tracks-overlay');
11131192
if (!overlay) {
@@ -4089,6 +4168,7 @@ <h4>${setting.display_name} <small>${key} (${setting.id})</small></h4>
40894168
}
40904169

40914170
initAppMenu();
4171+
initSwCacheIndicator();
40924172

40934173
updateMenuDebugState();
40944174
updateDebugFabState();

style.css

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,6 +1230,33 @@ h2 {
12301230
color: #0f5a2d;
12311231
}
12321232

1233+
.sw-cache-indicator {
1234+
flex: 1;
1235+
display: flex;
1236+
align-items: center;
1237+
justify-content: center;
1238+
min-height: 54px;
1239+
}
1240+
1241+
.sw-cache-indicator.hidden {
1242+
display: none;
1243+
}
1244+
1245+
.sw-spinner {
1246+
width: 18px;
1247+
height: 18px;
1248+
border-radius: 50%;
1249+
border: 2px solid rgba(31, 42, 35, 0.2);
1250+
border-top-color: rgba(31, 42, 35, 0.65);
1251+
animation: sw-spin 0.9s linear infinite;
1252+
}
1253+
1254+
@keyframes sw-spin {
1255+
to {
1256+
transform: rotate(360deg);
1257+
}
1258+
}
1259+
12331260
.debug-toggle-fab {
12341261
position: fixed;
12351262
right: 16px;

0 commit comments

Comments
 (0)