Add progressive web app companion for cross-platform access

Vite + TypeScript PWA that mirrors the Android app's core features:
- Pre-processed shelter data (build-time UTM33N→WGS84 conversion)
- Leaflet map with shelter markers, user location, and offline tiles
- Canvas compass arrow (ported from DirectionArrowView.kt)
- IndexedDB shelter cache with 7-day staleness check
- Service worker with CacheFirst tiles and precached app shell
- i18n for en, nb, nn (ported from Android strings.xml)
- iOS/Android compass handling with low-pass filter
- Respects user map interaction (no auto-snap on pan/zoom)
- Build revision cache-breaker for reliable SW updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-03-08 17:41:38 +01:00
commit e8428de775
12051 changed files with 1799735 additions and 0 deletions

View file

@ -0,0 +1,132 @@
// src/client/build/preact.ts
import { useState } from "preact/hooks";
// src/client/build/register.ts
var autoUpdateMode = "__SW_AUTO_UPDATE__";
var selfDestroying = "__SW_SELF_DESTROYING__";
var auto = autoUpdateMode === "true";
var autoDestroy = selfDestroying === "true";
function registerSW(options = {}) {
const {
immediate = false,
onNeedRefresh,
onOfflineReady,
onRegistered,
onRegisteredSW,
onRegisterError
} = options;
let wb;
let registerPromise;
let sendSkipWaitingMessage;
const updateServiceWorker = async (_reloadPage = true) => {
await registerPromise;
if (!auto) {
await sendSkipWaitingMessage?.();
}
};
async function register() {
if ("serviceWorker" in navigator) {
wb = await import("workbox-window").then(({ Workbox }) => {
return new Workbox("__SW__", { scope: "__SCOPE__", type: "__TYPE__" });
}).catch((e) => {
onRegisterError?.(e);
return void 0;
});
if (!wb)
return;
sendSkipWaitingMessage = async () => {
await wb?.messageSkipWaiting();
};
if (!autoDestroy) {
if (auto) {
wb.addEventListener("activated", (event) => {
if (event.isUpdate || event.isExternal)
window.location.reload();
});
wb.addEventListener("installed", (event) => {
if (!event.isUpdate) {
onOfflineReady?.();
}
});
} else {
let onNeedRefreshCalled = false;
const showSkipWaitingPrompt = () => {
onNeedRefreshCalled = true;
wb?.addEventListener("controlling", (event) => {
if (event.isUpdate)
window.location.reload();
});
onNeedRefresh?.();
};
wb.addEventListener("installed", (event) => {
if (typeof event.isUpdate === "undefined") {
if (typeof event.isExternal !== "undefined") {
if (event.isExternal)
showSkipWaitingPrompt();
else
!onNeedRefreshCalled && onOfflineReady?.();
} else {
if (event.isExternal)
window.location.reload();
else
!onNeedRefreshCalled && onOfflineReady?.();
}
} else if (!event.isUpdate) {
onOfflineReady?.();
}
});
wb.addEventListener("waiting", showSkipWaitingPrompt);
wb.addEventListener("externalwaiting", showSkipWaitingPrompt);
}
}
wb.register({ immediate }).then((r) => {
if (onRegisteredSW)
onRegisteredSW("__SW__", r);
else
onRegistered?.(r);
}).catch((e) => {
onRegisterError?.(e);
});
}
}
registerPromise = register();
return updateServiceWorker;
}
// src/client/build/preact.ts
function useRegisterSW(options = {}) {
const {
immediate = true,
onNeedRefresh,
onOfflineReady,
onRegistered,
onRegisteredSW,
onRegisterError
} = options;
const [needRefresh, setNeedRefresh] = useState(false);
const [offlineReady, setOfflineReady] = useState(false);
const [updateServiceWorker] = useState(() => {
return registerSW({
immediate,
onOfflineReady() {
setOfflineReady(true);
onOfflineReady?.();
},
onNeedRefresh() {
setNeedRefresh(true);
onNeedRefresh?.();
},
onRegistered,
onRegisteredSW,
onRegisterError
});
});
return {
needRefresh: [needRefresh, setNeedRefresh],
offlineReady: [offlineReady, setOfflineReady],
updateServiceWorker
};
}
export {
useRegisterSW
};

View file

@ -0,0 +1,132 @@
// src/client/build/react.ts
import { useState } from "react";
// src/client/build/register.ts
var autoUpdateMode = "__SW_AUTO_UPDATE__";
var selfDestroying = "__SW_SELF_DESTROYING__";
var auto = autoUpdateMode === "true";
var autoDestroy = selfDestroying === "true";
function registerSW(options = {}) {
const {
immediate = false,
onNeedRefresh,
onOfflineReady,
onRegistered,
onRegisteredSW,
onRegisterError
} = options;
let wb;
let registerPromise;
let sendSkipWaitingMessage;
const updateServiceWorker = async (_reloadPage = true) => {
await registerPromise;
if (!auto) {
await sendSkipWaitingMessage?.();
}
};
async function register() {
if ("serviceWorker" in navigator) {
wb = await import("workbox-window").then(({ Workbox }) => {
return new Workbox("__SW__", { scope: "__SCOPE__", type: "__TYPE__" });
}).catch((e) => {
onRegisterError?.(e);
return void 0;
});
if (!wb)
return;
sendSkipWaitingMessage = async () => {
await wb?.messageSkipWaiting();
};
if (!autoDestroy) {
if (auto) {
wb.addEventListener("activated", (event) => {
if (event.isUpdate || event.isExternal)
window.location.reload();
});
wb.addEventListener("installed", (event) => {
if (!event.isUpdate) {
onOfflineReady?.();
}
});
} else {
let onNeedRefreshCalled = false;
const showSkipWaitingPrompt = () => {
onNeedRefreshCalled = true;
wb?.addEventListener("controlling", (event) => {
if (event.isUpdate)
window.location.reload();
});
onNeedRefresh?.();
};
wb.addEventListener("installed", (event) => {
if (typeof event.isUpdate === "undefined") {
if (typeof event.isExternal !== "undefined") {
if (event.isExternal)
showSkipWaitingPrompt();
else
!onNeedRefreshCalled && onOfflineReady?.();
} else {
if (event.isExternal)
window.location.reload();
else
!onNeedRefreshCalled && onOfflineReady?.();
}
} else if (!event.isUpdate) {
onOfflineReady?.();
}
});
wb.addEventListener("waiting", showSkipWaitingPrompt);
wb.addEventListener("externalwaiting", showSkipWaitingPrompt);
}
}
wb.register({ immediate }).then((r) => {
if (onRegisteredSW)
onRegisteredSW("__SW__", r);
else
onRegistered?.(r);
}).catch((e) => {
onRegisterError?.(e);
});
}
}
registerPromise = register();
return updateServiceWorker;
}
// src/client/build/react.ts
function useRegisterSW(options = {}) {
const {
immediate = true,
onNeedRefresh,
onOfflineReady,
onRegistered,
onRegisteredSW,
onRegisterError
} = options;
const [needRefresh, setNeedRefresh] = useState(false);
const [offlineReady, setOfflineReady] = useState(false);
const [updateServiceWorker] = useState(() => {
return registerSW({
immediate,
onOfflineReady() {
setOfflineReady(true);
onOfflineReady?.();
},
onNeedRefresh() {
setNeedRefresh(true);
onNeedRefresh?.();
},
onRegistered,
onRegisteredSW,
onRegisterError
});
});
return {
needRefresh: [needRefresh, setNeedRefresh],
offlineReady: [offlineReady, setOfflineReady],
updateServiceWorker
};
}
export {
useRegisterSW
};

View file

@ -0,0 +1,94 @@
// src/client/build/register.ts
var autoUpdateMode = "__SW_AUTO_UPDATE__";
var selfDestroying = "__SW_SELF_DESTROYING__";
var auto = autoUpdateMode === "true";
var autoDestroy = selfDestroying === "true";
function registerSW(options = {}) {
const {
immediate = false,
onNeedRefresh,
onOfflineReady,
onRegistered,
onRegisteredSW,
onRegisterError
} = options;
let wb;
let registerPromise;
let sendSkipWaitingMessage;
const updateServiceWorker = async (_reloadPage = true) => {
await registerPromise;
if (!auto) {
await sendSkipWaitingMessage?.();
}
};
async function register() {
if ("serviceWorker" in navigator) {
wb = await import("workbox-window").then(({ Workbox }) => {
return new Workbox("__SW__", { scope: "__SCOPE__", type: "__TYPE__" });
}).catch((e) => {
onRegisterError?.(e);
return void 0;
});
if (!wb)
return;
sendSkipWaitingMessage = async () => {
await wb?.messageSkipWaiting();
};
if (!autoDestroy) {
if (auto) {
wb.addEventListener("activated", (event) => {
if (event.isUpdate || event.isExternal)
window.location.reload();
});
wb.addEventListener("installed", (event) => {
if (!event.isUpdate) {
onOfflineReady?.();
}
});
} else {
let onNeedRefreshCalled = false;
const showSkipWaitingPrompt = () => {
onNeedRefreshCalled = true;
wb?.addEventListener("controlling", (event) => {
if (event.isUpdate)
window.location.reload();
});
onNeedRefresh?.();
};
wb.addEventListener("installed", (event) => {
if (typeof event.isUpdate === "undefined") {
if (typeof event.isExternal !== "undefined") {
if (event.isExternal)
showSkipWaitingPrompt();
else
!onNeedRefreshCalled && onOfflineReady?.();
} else {
if (event.isExternal)
window.location.reload();
else
!onNeedRefreshCalled && onOfflineReady?.();
}
} else if (!event.isUpdate) {
onOfflineReady?.();
}
});
wb.addEventListener("waiting", showSkipWaitingPrompt);
wb.addEventListener("externalwaiting", showSkipWaitingPrompt);
}
}
wb.register({ immediate }).then((r) => {
if (onRegisteredSW)
onRegisteredSW("__SW__", r);
else
onRegistered?.(r);
}).catch((e) => {
onRegisterError?.(e);
});
}
}
registerPromise = register();
return updateServiceWorker;
}
export {
registerSW
};

View file

@ -0,0 +1,130 @@
// src/client/build/solid.ts
import { createSignal } from "solid-js";
// src/client/build/register.ts
var autoUpdateMode = "__SW_AUTO_UPDATE__";
var selfDestroying = "__SW_SELF_DESTROYING__";
var auto = autoUpdateMode === "true";
var autoDestroy = selfDestroying === "true";
function registerSW(options = {}) {
const {
immediate = false,
onNeedRefresh,
onOfflineReady,
onRegistered,
onRegisteredSW,
onRegisterError
} = options;
let wb;
let registerPromise;
let sendSkipWaitingMessage;
const updateServiceWorker = async (_reloadPage = true) => {
await registerPromise;
if (!auto) {
await sendSkipWaitingMessage?.();
}
};
async function register() {
if ("serviceWorker" in navigator) {
wb = await import("workbox-window").then(({ Workbox }) => {
return new Workbox("__SW__", { scope: "__SCOPE__", type: "__TYPE__" });
}).catch((e) => {
onRegisterError?.(e);
return void 0;
});
if (!wb)
return;
sendSkipWaitingMessage = async () => {
await wb?.messageSkipWaiting();
};
if (!autoDestroy) {
if (auto) {
wb.addEventListener("activated", (event) => {
if (event.isUpdate || event.isExternal)
window.location.reload();
});
wb.addEventListener("installed", (event) => {
if (!event.isUpdate) {
onOfflineReady?.();
}
});
} else {
let onNeedRefreshCalled = false;
const showSkipWaitingPrompt = () => {
onNeedRefreshCalled = true;
wb?.addEventListener("controlling", (event) => {
if (event.isUpdate)
window.location.reload();
});
onNeedRefresh?.();
};
wb.addEventListener("installed", (event) => {
if (typeof event.isUpdate === "undefined") {
if (typeof event.isExternal !== "undefined") {
if (event.isExternal)
showSkipWaitingPrompt();
else
!onNeedRefreshCalled && onOfflineReady?.();
} else {
if (event.isExternal)
window.location.reload();
else
!onNeedRefreshCalled && onOfflineReady?.();
}
} else if (!event.isUpdate) {
onOfflineReady?.();
}
});
wb.addEventListener("waiting", showSkipWaitingPrompt);
wb.addEventListener("externalwaiting", showSkipWaitingPrompt);
}
}
wb.register({ immediate }).then((r) => {
if (onRegisteredSW)
onRegisteredSW("__SW__", r);
else
onRegistered?.(r);
}).catch((e) => {
onRegisterError?.(e);
});
}
}
registerPromise = register();
return updateServiceWorker;
}
// src/client/build/solid.ts
function useRegisterSW(options = {}) {
const {
immediate = true,
onNeedRefresh,
onOfflineReady,
onRegistered,
onRegisteredSW,
onRegisterError
} = options;
const [needRefresh, setNeedRefresh] = createSignal(false);
const [offlineReady, setOfflineReady] = createSignal(false);
const updateServiceWorker = registerSW({
immediate,
onOfflineReady() {
setOfflineReady(true);
onOfflineReady?.();
},
onNeedRefresh() {
setNeedRefresh(true);
onNeedRefresh?.();
},
onRegistered,
onRegisteredSW,
onRegisterError
});
return {
needRefresh: [needRefresh, setNeedRefresh],
offlineReady: [offlineReady, setOfflineReady],
updateServiceWorker
};
}
export {
useRegisterSW
};

View file

@ -0,0 +1,130 @@
// src/client/build/svelte.ts
import { writable } from "svelte/store";
// src/client/build/register.ts
var autoUpdateMode = "__SW_AUTO_UPDATE__";
var selfDestroying = "__SW_SELF_DESTROYING__";
var auto = autoUpdateMode === "true";
var autoDestroy = selfDestroying === "true";
function registerSW(options = {}) {
const {
immediate = false,
onNeedRefresh,
onOfflineReady,
onRegistered,
onRegisteredSW,
onRegisterError
} = options;
let wb;
let registerPromise;
let sendSkipWaitingMessage;
const updateServiceWorker = async (_reloadPage = true) => {
await registerPromise;
if (!auto) {
await sendSkipWaitingMessage?.();
}
};
async function register() {
if ("serviceWorker" in navigator) {
wb = await import("workbox-window").then(({ Workbox }) => {
return new Workbox("__SW__", { scope: "__SCOPE__", type: "__TYPE__" });
}).catch((e) => {
onRegisterError?.(e);
return void 0;
});
if (!wb)
return;
sendSkipWaitingMessage = async () => {
await wb?.messageSkipWaiting();
};
if (!autoDestroy) {
if (auto) {
wb.addEventListener("activated", (event) => {
if (event.isUpdate || event.isExternal)
window.location.reload();
});
wb.addEventListener("installed", (event) => {
if (!event.isUpdate) {
onOfflineReady?.();
}
});
} else {
let onNeedRefreshCalled = false;
const showSkipWaitingPrompt = () => {
onNeedRefreshCalled = true;
wb?.addEventListener("controlling", (event) => {
if (event.isUpdate)
window.location.reload();
});
onNeedRefresh?.();
};
wb.addEventListener("installed", (event) => {
if (typeof event.isUpdate === "undefined") {
if (typeof event.isExternal !== "undefined") {
if (event.isExternal)
showSkipWaitingPrompt();
else
!onNeedRefreshCalled && onOfflineReady?.();
} else {
if (event.isExternal)
window.location.reload();
else
!onNeedRefreshCalled && onOfflineReady?.();
}
} else if (!event.isUpdate) {
onOfflineReady?.();
}
});
wb.addEventListener("waiting", showSkipWaitingPrompt);
wb.addEventListener("externalwaiting", showSkipWaitingPrompt);
}
}
wb.register({ immediate }).then((r) => {
if (onRegisteredSW)
onRegisteredSW("__SW__", r);
else
onRegistered?.(r);
}).catch((e) => {
onRegisterError?.(e);
});
}
}
registerPromise = register();
return updateServiceWorker;
}
// src/client/build/svelte.ts
function useRegisterSW(options = {}) {
const {
immediate = true,
onNeedRefresh,
onOfflineReady,
onRegistered,
onRegisteredSW,
onRegisterError
} = options;
const needRefresh = writable(false);
const offlineReady = writable(false);
const updateServiceWorker = registerSW({
immediate,
onOfflineReady() {
offlineReady.set(true);
onOfflineReady?.();
},
onNeedRefresh() {
needRefresh.set(true);
onNeedRefresh?.();
},
onRegistered,
onRegisteredSW,
onRegisterError
});
return {
needRefresh,
offlineReady,
updateServiceWorker
};
}
export {
useRegisterSW
};

View file

@ -0,0 +1,130 @@
// src/client/build/vue.ts
import { ref } from "vue";
// src/client/build/register.ts
var autoUpdateMode = "__SW_AUTO_UPDATE__";
var selfDestroying = "__SW_SELF_DESTROYING__";
var auto = autoUpdateMode === "true";
var autoDestroy = selfDestroying === "true";
function registerSW(options = {}) {
const {
immediate = false,
onNeedRefresh,
onOfflineReady,
onRegistered,
onRegisteredSW,
onRegisterError
} = options;
let wb;
let registerPromise;
let sendSkipWaitingMessage;
const updateServiceWorker = async (_reloadPage = true) => {
await registerPromise;
if (!auto) {
await sendSkipWaitingMessage?.();
}
};
async function register() {
if ("serviceWorker" in navigator) {
wb = await import("workbox-window").then(({ Workbox }) => {
return new Workbox("__SW__", { scope: "__SCOPE__", type: "__TYPE__" });
}).catch((e) => {
onRegisterError?.(e);
return void 0;
});
if (!wb)
return;
sendSkipWaitingMessage = async () => {
await wb?.messageSkipWaiting();
};
if (!autoDestroy) {
if (auto) {
wb.addEventListener("activated", (event) => {
if (event.isUpdate || event.isExternal)
window.location.reload();
});
wb.addEventListener("installed", (event) => {
if (!event.isUpdate) {
onOfflineReady?.();
}
});
} else {
let onNeedRefreshCalled = false;
const showSkipWaitingPrompt = () => {
onNeedRefreshCalled = true;
wb?.addEventListener("controlling", (event) => {
if (event.isUpdate)
window.location.reload();
});
onNeedRefresh?.();
};
wb.addEventListener("installed", (event) => {
if (typeof event.isUpdate === "undefined") {
if (typeof event.isExternal !== "undefined") {
if (event.isExternal)
showSkipWaitingPrompt();
else
!onNeedRefreshCalled && onOfflineReady?.();
} else {
if (event.isExternal)
window.location.reload();
else
!onNeedRefreshCalled && onOfflineReady?.();
}
} else if (!event.isUpdate) {
onOfflineReady?.();
}
});
wb.addEventListener("waiting", showSkipWaitingPrompt);
wb.addEventListener("externalwaiting", showSkipWaitingPrompt);
}
}
wb.register({ immediate }).then((r) => {
if (onRegisteredSW)
onRegisteredSW("__SW__", r);
else
onRegistered?.(r);
}).catch((e) => {
onRegisterError?.(e);
});
}
}
registerPromise = register();
return updateServiceWorker;
}
// src/client/build/vue.ts
function useRegisterSW(options = {}) {
const {
immediate = true,
onNeedRefresh,
onOfflineReady,
onRegistered,
onRegisteredSW,
onRegisterError
} = options;
const needRefresh = ref(false);
const offlineReady = ref(false);
const updateServiceWorker = registerSW({
immediate,
onNeedRefresh() {
needRefresh.value = true;
onNeedRefresh?.();
},
onOfflineReady() {
offlineReady.value = true;
onOfflineReady?.();
},
onRegistered,
onRegisteredSW,
onRegisterError
});
return {
updateServiceWorker,
offlineReady,
needRefresh
};
}
export {
useRegisterSW
};

View file

@ -0,0 +1,16 @@
// src/client/dev/preact.ts
import { useState } from "preact/hooks";
function useRegisterSW(_options = {}) {
const needRefresh = useState(false);
const offlineReady = useState(false);
const updateServiceWorker = (_reloadPage) => {
};
return {
needRefresh,
offlineReady,
updateServiceWorker
};
}
export {
useRegisterSW
};

View file

@ -0,0 +1,16 @@
// src/client/dev/react.ts
import { useState } from "react";
function useRegisterSW(_options = {}) {
const needRefresh = useState(false);
const offlineReady = useState(false);
const updateServiceWorker = (_reloadPage) => {
};
return {
needRefresh,
offlineReady,
updateServiceWorker
};
}
export {
useRegisterSW
};

View file

@ -0,0 +1,8 @@
// src/client/dev/register.ts
function registerSW(_options = {}) {
return (_reloadPage = true) => {
};
}
export {
registerSW
};

View file

@ -0,0 +1,16 @@
// src/client/dev/solid.ts
import { createSignal } from "solid-js";
function useRegisterSW(_options = {}) {
const needRefresh = createSignal(false);
const offlineReady = createSignal(false);
const updateServiceWorker = (_reloadPage) => {
};
return {
needRefresh,
offlineReady,
updateServiceWorker
};
}
export {
useRegisterSW
};

View file

@ -0,0 +1,16 @@
// src/client/dev/svelte.ts
import { writable } from "svelte/store";
function useRegisterSW(_options = {}) {
const needRefresh = writable(false);
const offlineReady = writable(false);
const updateServiceWorker = (_reloadPage) => {
};
return {
needRefresh,
offlineReady,
updateServiceWorker
};
}
export {
useRegisterSW
};

View file

@ -0,0 +1,16 @@
// src/client/dev/vue.ts
import { ref } from "vue";
function useRegisterSW(_options = {}) {
const needRefresh = ref(false);
const offlineReady = ref(false);
const updateServiceWorker = (_reloadPage) => {
};
return {
updateServiceWorker,
offlineReady,
needRefresh
};
}
export {
useRegisterSW
};