Compare commits
2 commits
f0c4a1f5b4
...
262fafe9e0
| Author | SHA1 | Date | |
|---|---|---|---|
| 262fafe9e0 | |||
| 42c28df102 |
10 changed files with 127 additions and 5 deletions
|
|
@ -14,8 +14,8 @@ android {
|
||||||
applicationId = "no.naiv.tilfluktsrom"
|
applicationId = "no.naiv.tilfluktsrom"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 14
|
versionCode = 15
|
||||||
versionName = "1.9.1"
|
versionName = "1.10.0"
|
||||||
|
|
||||||
// Deep link domain — single source of truth for manifest + Kotlin code
|
// Deep link domain — single source of truth for manifest + Kotlin code
|
||||||
val deepLinkDomain = "tilfluktsrom.naiv.no"
|
val deepLinkDomain = "tilfluktsrom.naiv.no"
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ class ShelterRepository(private val context: Context) {
|
||||||
.readTimeout(60, TimeUnit.SECONDS)
|
.readTimeout(60, TimeUnit.SECONDS)
|
||||||
.addInterceptor(Interceptor { chain ->
|
.addInterceptor(Interceptor { chain ->
|
||||||
chain.proceed(chain.request().newBuilder()
|
chain.proceed(chain.request().newBuilder()
|
||||||
.header("User-Agent", "Tilfluktsrom/1.9.1")
|
.header("User-Agent", "Tilfluktsrom/1.10.0")
|
||||||
.build())
|
.build())
|
||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,16 @@
|
||||||
<html lang="en" dir="ltr">
|
<html lang="en" dir="ltr">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
<!-- WCAG 1.4.4 Resize Text: do not pin maximum-scale or disable user-scalable.
|
||||||
|
Leaflet handles its own touch gestures on the map; the rest of the page
|
||||||
|
(status bar, bottom sheet, dialogs) must remain zoomable for low-vision users. -->
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="theme-color" content="#1A1A2E" />
|
<meta name="theme-color" content="#1A1A2E" />
|
||||||
<meta name="description" content="Find the nearest public shelter in Norway" />
|
<meta name="description" content="Find the nearest public shelter in Norway" />
|
||||||
|
<!-- iOS home-screen/PWA integration: run in standalone mode, dark status bar, correct label. -->
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Tilfluktsrom" />
|
||||||
<meta http-equiv="Content-Security-Policy"
|
<meta http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://*.tile.openstreetmap.org; connect-src 'self' https://*.tile.openstreetmap.org; font-src 'self'; worker-src 'self'" />
|
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://*.tile.openstreetmap.org; connect-src 'self' https://*.tile.openstreetmap.org; font-src 'self'; worker-src 'self'" />
|
||||||
<title>Tilfluktsrom</title>
|
<title>Tilfluktsrom</title>
|
||||||
|
|
|
||||||
|
|
@ -101,10 +101,14 @@ function setupButtons(): void {
|
||||||
const compassContainer = document.getElementById('compass-container')!;
|
const compassContainer = document.getElementById('compass-container')!;
|
||||||
|
|
||||||
if (isCompassMode) {
|
if (isCompassMode) {
|
||||||
// Request compass permission on first toggle (iOS requirement)
|
// Request compass permission on first toggle (iOS requirement).
|
||||||
|
// On denial we stay in map mode and surface a status message so the
|
||||||
|
// user understands why nothing happened — silent reverting is the
|
||||||
|
// failure mode the Android GPS-denied banner was designed to avoid.
|
||||||
const granted = await compassProvider.requestPermission();
|
const granted = await compassProvider.requestPermission();
|
||||||
if (!granted) {
|
if (!granted) {
|
||||||
isCompassMode = false;
|
isCompassMode = false;
|
||||||
|
statusBar.setStatus(t('compass_permission_denied'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mapContainer.style.display = 'none';
|
mapContainer.style.display = 'none';
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,10 @@ export const en: Record<string, string> = {
|
||||||
update_success: 'Shelter data updated',
|
update_success: 'Shelter data updated',
|
||||||
update_failed: 'Update failed \u2014 using cached data',
|
update_failed: 'Update failed \u2014 using cached data',
|
||||||
error_shelter_not_found: 'Shelter not found',
|
error_shelter_not_found: 'Shelter not found',
|
||||||
|
compass_permission_denied:
|
||||||
|
'Compass access denied. You can still use the map to find shelters.',
|
||||||
|
ios_install_hint:
|
||||||
|
'Add Tilfluktsrom to your home screen for offline access: tap Share, then Add to Home Screen.',
|
||||||
|
|
||||||
// Accessibility
|
// Accessibility
|
||||||
direction_arrow_description: 'Direction to shelter, %s away',
|
direction_arrow_description: 'Direction to shelter, %s away',
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,10 @@ export const nb: Record<string, string> = {
|
||||||
update_success: 'Tilfluktsromdata oppdatert',
|
update_success: 'Tilfluktsromdata oppdatert',
|
||||||
update_failed: 'Oppdatering mislyktes — bruker lagrede data',
|
update_failed: 'Oppdatering mislyktes — bruker lagrede data',
|
||||||
error_shelter_not_found: 'Fant ikke tilfluktsrommet',
|
error_shelter_not_found: 'Fant ikke tilfluktsrommet',
|
||||||
|
compass_permission_denied:
|
||||||
|
'Kompasstilgang avslått. Du kan fortsatt bruke kartet til å finne tilfluktsrom.',
|
||||||
|
ios_install_hint:
|
||||||
|
'Legg Tilfluktsrom til hjemskjermen for frakoblet bruk: trykk Del, deretter Legg til på hjem-skjerm.',
|
||||||
|
|
||||||
// Tilgjengelighet
|
// Tilgjengelighet
|
||||||
direction_arrow_description: 'Retning til tilfluktsrom, %s unna',
|
direction_arrow_description: 'Retning til tilfluktsrom, %s unna',
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,10 @@ export const nn: Record<string, string> = {
|
||||||
update_success: 'Tilfluktsromdata oppdatert',
|
update_success: 'Tilfluktsromdata oppdatert',
|
||||||
update_failed: 'Oppdatering mislukkast — brukar lagra data',
|
update_failed: 'Oppdatering mislukkast — brukar lagra data',
|
||||||
error_shelter_not_found: 'Fann ikkje tilfluktsrommet',
|
error_shelter_not_found: 'Fann ikkje tilfluktsrommet',
|
||||||
|
compass_permission_denied:
|
||||||
|
'Kompasstilgang avslått. Du kan framleis bruke kartet til å finne tilfluktsrom.',
|
||||||
|
ios_install_hint:
|
||||||
|
'Legg Tilfluktsrom til heimeskjermen for fråkopla bruk: trykk Del, deretter Legg til på heimeskjerm.',
|
||||||
|
|
||||||
// Tilgjenge
|
// Tilgjenge
|
||||||
direction_arrow_description: 'Retning til tilfluktsrom, %s unna',
|
direction_arrow_description: 'Retning til tilfluktsrom, %s unna',
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { initLocale } from './i18n/i18n';
|
||||||
import { init } from './app';
|
import { init } from './app';
|
||||||
import { setStatus } from './ui/status-bar';
|
import { setStatus } from './ui/status-bar';
|
||||||
import { t } from './i18n/i18n';
|
import { t } from './i18n/i18n';
|
||||||
|
import { maybeShow as maybeShowIosInstallHint } from './ui/install-hint';
|
||||||
|
|
||||||
console.info(`[tilfluktsrom] build ${__BUILD_REVISION__}`);
|
console.info(`[tilfluktsrom] build ${__BUILD_REVISION__}`);
|
||||||
|
|
||||||
|
|
@ -33,4 +34,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
await init();
|
await init();
|
||||||
|
|
||||||
|
// Shown only on first iOS Safari visit, once per device. Placed after init()
|
||||||
|
// so the banner doesn't compete with the loading overlay.
|
||||||
|
maybeShowIosInstallHint();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,41 @@ html, body {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- iOS "Add to Home Screen" hint (shown once, dismissable) --- */
|
||||||
|
#ios-install-hint {
|
||||||
|
position: fixed;
|
||||||
|
left: 12px;
|
||||||
|
right: 12px;
|
||||||
|
bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #16213E;
|
||||||
|
color: #ECEFF1;
|
||||||
|
border: 1px solid #FF6B35;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.3;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ios-install-hint span {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ios-install-hint button {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid #ECEFF1;
|
||||||
|
color: #ECEFF1;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
#cache-retry-btn {
|
#cache-retry-btn {
|
||||||
background: none;
|
background: none;
|
||||||
border: 1px solid #FFFFFF;
|
border: 1px solid #FFFFFF;
|
||||||
|
|
|
||||||
59
pwa/src/ui/install-hint.ts
Normal file
59
pwa/src/ui/install-hint.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
/**
|
||||||
|
* iOS-only "Add to Home Screen" hint.
|
||||||
|
*
|
||||||
|
* Chrome-based browsers fire `beforeinstallprompt` and we could show a native
|
||||||
|
* install UI there; iOS Safari does not. For iOS users the only way to install
|
||||||
|
* is Share → Add to Home Screen, so we show a dismissable textual hint the
|
||||||
|
* first time they visit in a non-standalone context.
|
||||||
|
*
|
||||||
|
* Shown once per device (localStorage). Harmless if the heuristic mis-fires
|
||||||
|
* on a new iOS version — the hint is dismissable.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { t } from '../i18n/i18n';
|
||||||
|
|
||||||
|
const DISMISSED_KEY = 'tilfluktsrom:ios-install-hint:dismissed';
|
||||||
|
|
||||||
|
function isIOS(): boolean {
|
||||||
|
// Safari on iPadOS 13+ reports as MacIntel, so also check for touch + Safari.
|
||||||
|
const ua = navigator.userAgent;
|
||||||
|
if (/iPad|iPhone|iPod/.test(ua)) return true;
|
||||||
|
return (
|
||||||
|
navigator.maxTouchPoints > 1 &&
|
||||||
|
/Macintosh/.test(ua) &&
|
||||||
|
/Safari/.test(ua) &&
|
||||||
|
!/Chrome|CriOS|FxiOS/.test(ua)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStandalone(): boolean {
|
||||||
|
// iOS-specific property
|
||||||
|
const nav = navigator as Navigator & { standalone?: boolean };
|
||||||
|
if (nav.standalone) return true;
|
||||||
|
// Standards-based check used by Chrome/Edge
|
||||||
|
return window.matchMedia?.('(display-mode: standalone)').matches ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function maybeShow(): void {
|
||||||
|
if (!isIOS() || isStandalone()) return;
|
||||||
|
if (localStorage.getItem(DISMISSED_KEY) === '1') return;
|
||||||
|
|
||||||
|
const banner = document.createElement('div');
|
||||||
|
banner.id = 'ios-install-hint';
|
||||||
|
banner.setAttribute('role', 'status');
|
||||||
|
|
||||||
|
const text = document.createElement('span');
|
||||||
|
text.textContent = t('ios_install_hint');
|
||||||
|
|
||||||
|
const dismiss = document.createElement('button');
|
||||||
|
dismiss.type = 'button';
|
||||||
|
dismiss.textContent = t('action_close');
|
||||||
|
dismiss.setAttribute('aria-label', t('action_close'));
|
||||||
|
dismiss.addEventListener('click', () => {
|
||||||
|
localStorage.setItem(DISMISSED_KEY, '1');
|
||||||
|
banner.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
banner.append(text, dismiss);
|
||||||
|
document.body.appendChild(banner);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue