Compare commits
15 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5365c99bf5 | |||
| 890ea96ed8 | |||
| 337e9b96c2 | |||
| 3f0c958b98 | |||
| 40da1dfd6c | |||
| 1fb9f14ad4 | |||
| b31537df2c | |||
| d2291a2d35 | |||
| 91dd89c03d | |||
| fbe8e53141 | |||
| 83d406a6aa | |||
| c603e81b2a | |||
| 948625b777 | |||
| 3d6f8f362e | |||
| 87ac698d55 |
46 changed files with 3800 additions and 664 deletions
73
.beads/.gitignore
vendored
Normal file
73
.beads/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
# Dolt database (managed by Dolt, not git)
|
||||||
|
dolt/
|
||||||
|
embeddeddolt/
|
||||||
|
|
||||||
|
# Runtime files
|
||||||
|
bd.sock
|
||||||
|
bd.sock.startlock
|
||||||
|
sync-state.json
|
||||||
|
last-touched
|
||||||
|
.exclusive-lock
|
||||||
|
|
||||||
|
# Daemon runtime (lock, log, pid)
|
||||||
|
daemon.*
|
||||||
|
|
||||||
|
# Interactions log (runtime, not versioned)
|
||||||
|
interactions.jsonl
|
||||||
|
|
||||||
|
# Push state (runtime, per-machine)
|
||||||
|
push-state.json
|
||||||
|
|
||||||
|
# Lock files (various runtime locks)
|
||||||
|
*.lock
|
||||||
|
|
||||||
|
# Credential key (encryption key for federation peer auth — never commit)
|
||||||
|
.beads-credential-key
|
||||||
|
|
||||||
|
# Local version tracking (prevents upgrade notification spam after git ops)
|
||||||
|
.local_version
|
||||||
|
|
||||||
|
# Worktree redirect file (contains relative path to main repo's .beads/)
|
||||||
|
# Must not be committed as paths would be wrong in other clones
|
||||||
|
redirect
|
||||||
|
|
||||||
|
# Sync state (local-only, per-machine)
|
||||||
|
# These files are machine-specific and should not be shared across clones
|
||||||
|
.sync.lock
|
||||||
|
export-state/
|
||||||
|
export-state.json
|
||||||
|
|
||||||
|
# Ephemeral store (SQLite - wisps/molecules, intentionally not versioned)
|
||||||
|
ephemeral.sqlite3
|
||||||
|
ephemeral.sqlite3-journal
|
||||||
|
ephemeral.sqlite3-wal
|
||||||
|
ephemeral.sqlite3-shm
|
||||||
|
|
||||||
|
# Dolt server management (auto-started by bd)
|
||||||
|
dolt-server.pid
|
||||||
|
dolt-server.log
|
||||||
|
dolt-server.lock
|
||||||
|
dolt-server.port
|
||||||
|
dolt-server.activity
|
||||||
|
|
||||||
|
# Corrupt backup directories (created by bd doctor --fix recovery)
|
||||||
|
*.corrupt.backup/
|
||||||
|
|
||||||
|
# Backup data (auto-exported JSONL, local-only)
|
||||||
|
backup/
|
||||||
|
|
||||||
|
# Per-project environment file (Dolt connection config, GH#2520)
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Legacy files (from pre-Dolt versions)
|
||||||
|
*.db
|
||||||
|
*.db?*
|
||||||
|
*.db-journal
|
||||||
|
*.db-wal
|
||||||
|
*.db-shm
|
||||||
|
db.sqlite
|
||||||
|
bd.db
|
||||||
|
# NOTE: Do NOT add negation patterns here.
|
||||||
|
# They would override fork protection in .git/info/exclude.
|
||||||
|
# Config files (metadata.json, config.yaml) are tracked by git by default
|
||||||
|
# since no pattern above ignores them.
|
||||||
81
.beads/README.md
Normal file
81
.beads/README.md
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
# Beads - AI-Native Issue Tracking
|
||||||
|
|
||||||
|
Welcome to Beads! This repository uses **Beads** for issue tracking - a modern, AI-native tool designed to live directly in your codebase alongside your code.
|
||||||
|
|
||||||
|
## What is Beads?
|
||||||
|
|
||||||
|
Beads is issue tracking that lives in your repo, making it perfect for AI coding agents and developers who want their issues close to their code. No web UI required - everything works through the CLI and integrates seamlessly with git.
|
||||||
|
|
||||||
|
**Learn more:** [github.com/steveyegge/beads](https://github.com/steveyegge/beads)
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Essential Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create new issues
|
||||||
|
bd create "Add user authentication"
|
||||||
|
|
||||||
|
# View all issues
|
||||||
|
bd list
|
||||||
|
|
||||||
|
# View issue details
|
||||||
|
bd show <issue-id>
|
||||||
|
|
||||||
|
# Update issue status
|
||||||
|
bd update <issue-id> --claim
|
||||||
|
bd update <issue-id> --status done
|
||||||
|
|
||||||
|
# Sync with Dolt remote
|
||||||
|
bd dolt push
|
||||||
|
```
|
||||||
|
|
||||||
|
### Working with Issues
|
||||||
|
|
||||||
|
Issues in Beads are:
|
||||||
|
- **Git-native**: Stored in Dolt database with version control and branching
|
||||||
|
- **AI-friendly**: CLI-first design works perfectly with AI coding agents
|
||||||
|
- **Branch-aware**: Issues can follow your branch workflow
|
||||||
|
- **Always in sync**: Auto-syncs with your commits
|
||||||
|
|
||||||
|
## Why Beads?
|
||||||
|
|
||||||
|
✨ **AI-Native Design**
|
||||||
|
- Built specifically for AI-assisted development workflows
|
||||||
|
- CLI-first interface works seamlessly with AI coding agents
|
||||||
|
- No context switching to web UIs
|
||||||
|
|
||||||
|
🚀 **Developer Focused**
|
||||||
|
- Issues live in your repo, right next to your code
|
||||||
|
- Works offline, syncs when you push
|
||||||
|
- Fast, lightweight, and stays out of your way
|
||||||
|
|
||||||
|
🔧 **Git Integration**
|
||||||
|
- Automatic sync with git commits
|
||||||
|
- Branch-aware issue tracking
|
||||||
|
- Dolt-native three-way merge resolution
|
||||||
|
|
||||||
|
## Get Started with Beads
|
||||||
|
|
||||||
|
Try Beads in your own projects:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Beads
|
||||||
|
curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash
|
||||||
|
|
||||||
|
# Initialize in your repo
|
||||||
|
bd init
|
||||||
|
|
||||||
|
# Create your first issue
|
||||||
|
bd create "Try out Beads"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
- **Documentation**: [github.com/steveyegge/beads/docs](https://github.com/steveyegge/beads/tree/main/docs)
|
||||||
|
- **Quick Start Guide**: Run `bd quickstart`
|
||||||
|
- **Examples**: [github.com/steveyegge/beads/examples](https://github.com/steveyegge/beads/tree/main/examples)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Beads: Issue tracking that moves at the speed of thought* ⚡
|
||||||
56
.beads/config.yaml
Normal file
56
.beads/config.yaml
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# Beads Configuration File
|
||||||
|
# This file configures default behavior for all bd commands in this repository
|
||||||
|
# All settings can also be set via environment variables (BD_* prefix)
|
||||||
|
# or overridden with command-line flags
|
||||||
|
|
||||||
|
# Issue prefix for this repository (used by bd init)
|
||||||
|
# If not set, bd init will auto-detect from directory name
|
||||||
|
# Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc.
|
||||||
|
# issue-prefix: ""
|
||||||
|
|
||||||
|
# Use no-db mode: JSONL-only, no Dolt database
|
||||||
|
# When true, bd will use .beads/issues.jsonl as the source of truth
|
||||||
|
# no-db: false
|
||||||
|
|
||||||
|
# Enable JSON output by default
|
||||||
|
# json: false
|
||||||
|
|
||||||
|
# Feedback title formatting for mutating commands (create/update/close/dep/edit)
|
||||||
|
# 0 = hide titles, N > 0 = truncate to N characters
|
||||||
|
# output:
|
||||||
|
# title-length: 255
|
||||||
|
|
||||||
|
# Default actor for audit trails (overridden by BEADS_ACTOR or --actor)
|
||||||
|
# actor: ""
|
||||||
|
|
||||||
|
# Export events (audit trail) to .beads/events.jsonl on each flush/sync
|
||||||
|
# When enabled, new events are appended incrementally using a high-water mark.
|
||||||
|
# Use 'bd export --events' to trigger manually regardless of this setting.
|
||||||
|
# events-export: false
|
||||||
|
|
||||||
|
# Multi-repo configuration (experimental - bd-307)
|
||||||
|
# Allows hydrating from multiple repositories and routing writes to the correct database
|
||||||
|
# repos:
|
||||||
|
# primary: "." # Primary repo (where this database lives)
|
||||||
|
# additional: # Additional repos to hydrate from (read-only)
|
||||||
|
# - ~/beads-planning # Personal planning repo
|
||||||
|
# - ~/work-planning # Work planning repo
|
||||||
|
|
||||||
|
# JSONL backup (periodic export for off-machine recovery)
|
||||||
|
# Auto-enabled when a git remote exists. Override explicitly:
|
||||||
|
# backup:
|
||||||
|
# enabled: false # Disable auto-backup entirely
|
||||||
|
# interval: 15m # Minimum time between auto-exports
|
||||||
|
# git-push: false # Disable git push (export locally only)
|
||||||
|
# git-repo: "" # Separate git repo for backups (default: project repo)
|
||||||
|
|
||||||
|
# Integration settings (access with 'bd config get/set')
|
||||||
|
# These are stored in the database, not in this file:
|
||||||
|
# - jira.url
|
||||||
|
# - jira.project
|
||||||
|
# - linear.url
|
||||||
|
# - linear.api-key
|
||||||
|
# - github.org
|
||||||
|
# - github.repo
|
||||||
|
|
||||||
|
sync.remote: "git+ssh://git@kode.naiv.no:2222/olemd/tilfluktsrom.git"
|
||||||
24
.beads/hooks/post-checkout
Executable file
24
.beads/hooks/post-checkout
Executable file
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
# --- BEGIN BEADS INTEGRATION v1.0.2 ---
|
||||||
|
# This section is managed by beads. Do not remove these markers.
|
||||||
|
if command -v bd >/dev/null 2>&1; then
|
||||||
|
export BD_GIT_HOOK=1
|
||||||
|
_bd_timeout=${BEADS_HOOK_TIMEOUT:-300}
|
||||||
|
if command -v timeout >/dev/null 2>&1; then
|
||||||
|
timeout "$_bd_timeout" bd hooks run post-checkout "$@"
|
||||||
|
_bd_exit=$?
|
||||||
|
if [ $_bd_exit -eq 124 ]; then
|
||||||
|
echo >&2 "beads: hook 'post-checkout' timed out after ${_bd_timeout}s — continuing without beads"
|
||||||
|
_bd_exit=0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
bd hooks run post-checkout "$@"
|
||||||
|
_bd_exit=$?
|
||||||
|
fi
|
||||||
|
if [ $_bd_exit -eq 3 ]; then
|
||||||
|
echo >&2 "beads: database not initialized — skipping hook 'post-checkout'"
|
||||||
|
_bd_exit=0
|
||||||
|
fi
|
||||||
|
if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
|
||||||
|
fi
|
||||||
|
# --- END BEADS INTEGRATION v1.0.2 ---
|
||||||
24
.beads/hooks/post-merge
Executable file
24
.beads/hooks/post-merge
Executable file
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
# --- BEGIN BEADS INTEGRATION v1.0.2 ---
|
||||||
|
# This section is managed by beads. Do not remove these markers.
|
||||||
|
if command -v bd >/dev/null 2>&1; then
|
||||||
|
export BD_GIT_HOOK=1
|
||||||
|
_bd_timeout=${BEADS_HOOK_TIMEOUT:-300}
|
||||||
|
if command -v timeout >/dev/null 2>&1; then
|
||||||
|
timeout "$_bd_timeout" bd hooks run post-merge "$@"
|
||||||
|
_bd_exit=$?
|
||||||
|
if [ $_bd_exit -eq 124 ]; then
|
||||||
|
echo >&2 "beads: hook 'post-merge' timed out after ${_bd_timeout}s — continuing without beads"
|
||||||
|
_bd_exit=0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
bd hooks run post-merge "$@"
|
||||||
|
_bd_exit=$?
|
||||||
|
fi
|
||||||
|
if [ $_bd_exit -eq 3 ]; then
|
||||||
|
echo >&2 "beads: database not initialized — skipping hook 'post-merge'"
|
||||||
|
_bd_exit=0
|
||||||
|
fi
|
||||||
|
if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
|
||||||
|
fi
|
||||||
|
# --- END BEADS INTEGRATION v1.0.2 ---
|
||||||
24
.beads/hooks/pre-commit
Executable file
24
.beads/hooks/pre-commit
Executable file
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
# --- BEGIN BEADS INTEGRATION v1.0.2 ---
|
||||||
|
# This section is managed by beads. Do not remove these markers.
|
||||||
|
if command -v bd >/dev/null 2>&1; then
|
||||||
|
export BD_GIT_HOOK=1
|
||||||
|
_bd_timeout=${BEADS_HOOK_TIMEOUT:-300}
|
||||||
|
if command -v timeout >/dev/null 2>&1; then
|
||||||
|
timeout "$_bd_timeout" bd hooks run pre-commit "$@"
|
||||||
|
_bd_exit=$?
|
||||||
|
if [ $_bd_exit -eq 124 ]; then
|
||||||
|
echo >&2 "beads: hook 'pre-commit' timed out after ${_bd_timeout}s — continuing without beads"
|
||||||
|
_bd_exit=0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
bd hooks run pre-commit "$@"
|
||||||
|
_bd_exit=$?
|
||||||
|
fi
|
||||||
|
if [ $_bd_exit -eq 3 ]; then
|
||||||
|
echo >&2 "beads: database not initialized — skipping hook 'pre-commit'"
|
||||||
|
_bd_exit=0
|
||||||
|
fi
|
||||||
|
if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
|
||||||
|
fi
|
||||||
|
# --- END BEADS INTEGRATION v1.0.2 ---
|
||||||
24
.beads/hooks/pre-push
Executable file
24
.beads/hooks/pre-push
Executable file
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
# --- BEGIN BEADS INTEGRATION v1.0.2 ---
|
||||||
|
# This section is managed by beads. Do not remove these markers.
|
||||||
|
if command -v bd >/dev/null 2>&1; then
|
||||||
|
export BD_GIT_HOOK=1
|
||||||
|
_bd_timeout=${BEADS_HOOK_TIMEOUT:-300}
|
||||||
|
if command -v timeout >/dev/null 2>&1; then
|
||||||
|
timeout "$_bd_timeout" bd hooks run pre-push "$@"
|
||||||
|
_bd_exit=$?
|
||||||
|
if [ $_bd_exit -eq 124 ]; then
|
||||||
|
echo >&2 "beads: hook 'pre-push' timed out after ${_bd_timeout}s — continuing without beads"
|
||||||
|
_bd_exit=0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
bd hooks run pre-push "$@"
|
||||||
|
_bd_exit=$?
|
||||||
|
fi
|
||||||
|
if [ $_bd_exit -eq 3 ]; then
|
||||||
|
echo >&2 "beads: database not initialized — skipping hook 'pre-push'"
|
||||||
|
_bd_exit=0
|
||||||
|
fi
|
||||||
|
if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
|
||||||
|
fi
|
||||||
|
# --- END BEADS INTEGRATION v1.0.2 ---
|
||||||
24
.beads/hooks/prepare-commit-msg
Executable file
24
.beads/hooks/prepare-commit-msg
Executable file
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
# --- BEGIN BEADS INTEGRATION v1.0.2 ---
|
||||||
|
# This section is managed by beads. Do not remove these markers.
|
||||||
|
if command -v bd >/dev/null 2>&1; then
|
||||||
|
export BD_GIT_HOOK=1
|
||||||
|
_bd_timeout=${BEADS_HOOK_TIMEOUT:-300}
|
||||||
|
if command -v timeout >/dev/null 2>&1; then
|
||||||
|
timeout "$_bd_timeout" bd hooks run prepare-commit-msg "$@"
|
||||||
|
_bd_exit=$?
|
||||||
|
if [ $_bd_exit -eq 124 ]; then
|
||||||
|
echo >&2 "beads: hook 'prepare-commit-msg' timed out after ${_bd_timeout}s — continuing without beads"
|
||||||
|
_bd_exit=0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
bd hooks run prepare-commit-msg "$@"
|
||||||
|
_bd_exit=$?
|
||||||
|
fi
|
||||||
|
if [ $_bd_exit -eq 3 ]; then
|
||||||
|
echo >&2 "beads: database not initialized — skipping hook 'prepare-commit-msg'"
|
||||||
|
_bd_exit=0
|
||||||
|
fi
|
||||||
|
if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
|
||||||
|
fi
|
||||||
|
# --- END BEADS INTEGRATION v1.0.2 ---
|
||||||
14
.beads/issues.jsonl
Normal file
14
.beads/issues.jsonl
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{"id":"tilfluktsrom-7zc","title":"Badge-kontrast under WCAG 2.2 AA — bytt shelter_primary til warning_bg","description":"Mirror av Forgejo-issue #18.\n\nBadge introdusert i 1fb9f14 (#13) bruker hvit fet 11sp tekst på shelter_primary (#FF6B35). Målt kontrast: ~2.84:1.\n\nWCAG 2.2 AA SC 1.4.3:\n- Normaltekst: ≥4.5:1\n- Large text (≥14pt fet eller ≥18pt regular): ≥3.0:1\n\n11sp fet teller som normaltekst → feiler AA-terskelen, og marginalt også 3:1 for large text.\n\nFiks: én linje i app/src/main/res/layout/item_shelter.xml — bytt @color/shelter_primary til @color/warning_bg (#BF360C, dokumentert ~5.5:1 vs hvit i colors.xml-kommentar).\n\nAlternativer som ikke fungerer:\n- shelter_primary_dark (#E55A2B) ~3.6:1 — fortsatt under AA normaltekst\n- Svart tekst på orange — ~7:1, men bryter visuell konsistens\n\nRelevant for offentlig-sektor-godkjenning (Uutilsynet/WAD/EN 301 549).\n\nForgejo: https://kode.naiv.no/olemd/tilfluktsrom/issues/18","acceptance_criteria":"Badge-bakgrunnen på item_shelter.xml outsideNearestBadge er endret til warning_bg. Visuelt verifisert at hvit fet 11sp tekst på den nye bakgrunnen gir ≥4.5:1 målt kontrast (med en kontrast-sjekker eller WCAG-verktøy).","status":"closed","priority":2,"issue_type":"bug","assignee":"Ole-Morten Duesund","owner":"olemd@glemt.net","created_at":"2026-04-29T14:54:00Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-29T14:55:09Z","started_at":"2026-04-29T14:54:56Z","closed_at":"2026-04-29T14:55:09Z","close_reason":"Bytta @color/shelter_primary til @color/warning_bg på outsideNearestBadge i item_shelter.xml. Kontrast: ~2.84:1 → ~5.6:1 (over WCAG 2.2 AA SC 1.4.3 4.5:1-terskel). Visuell verifisering på enhet/emulator gjenstår.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"id":"tilfluktsrom-efo","title":"PWA: dyplenket tilfluktsrom utenfor topp-N vises som om det er nærmest","description":"Mirror av Forgejo-issue #17.\n\nParallell til Android-fiksen i 1fb9f14 (#13).\n\nNåværende oppførsel: når dyplenken peker på et tilfluktsrom utenfor de N nærmeste, gjør pwa/src/app.ts:266-285 unshift() inn på indeks 0 og setter selectedShelterIndex=0. Resultat: det dyplenkede vises *som om* det er det nærmeste — uten badge, uten separator, uten forklaring. Lista bryter sin egen sort-på-avstand-invariant.\n\nHybrid-fiks (parallell til Android):\n1. Append (push) i stedet for unshift på app.ts:274\n2. Badge i shelter-list.ts på den appendede raden — bruk samme nøkkel shelter_outside_nearest_badge i pwa/src/i18n/{en,nb,nn}.ts\n3. updateList() får outsideNearestRomnr-parameter (eller wrap-objekt parallelt med Android sin ShelterListItem)\n4. Badge-teksten suffikses i aria-label (skjermleser-paritet med Android)\n5. selectedItem.scrollIntoView({block:'nearest', behavior:'smooth'}) etter updateList\n6. Tilgjengelighet: badge ≥4.5:1 kontrast (WCAG 2.2 AA SC 1.4.3). #FF6B35+hvit tekst er ~3.5:1 — *under terskel*. Bruk shelter_primary_dark #E55A2B som badge-bakgrunn, eller svart tekst på orange (~7:1)\n\nUt av skopet: endring av selve dyplenke-formatet (fortsatt /shelter/{romnr}), klyngevisning (#10).\n\nForgejo: https://kode.naiv.no/olemd/tilfluktsrom/issues/17","acceptance_criteria":"Dyplenket tilfluktsrom utenfor topp-N appendes på siste rad i lista (ikke unshift). Den appendede raden har et synlig 'Valgt – utenfor nærområdet'-badge med ≥4.5:1 kontrast. Lista scroller automatisk til valgt rad. Skjermleser leser badge-teksten som del av aria-label. Verifisert manuelt med en deep-link til et tilfluktsrom langt fra brukerens posisjon.","status":"open","priority":2,"issue_type":"bug","owner":"olemd@glemt.net","created_at":"2026-04-29T14:52:08Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-29T14:52:08Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"id":"tilfluktsrom-5vc","title":"Brukerens retningspil i kartet er hvit og forsvinner på lyse underlag","description":"Mirror av Forgejo-issue #16.\n\nNår brukeren beveger seg raskt nok til at OSMDroid bytter fra person- til retningspil-ikonet i MyLocationNewOverlay, blir pilen tilnærmet usynlig: heltrukken hvit uten kontur eller skygge. På lyse tiles (snø, sand, brede veifyll, lyse OSM-temaer) forsvinner den helt.\n\nÅrsak: MainActivity.kt:190 instansierer MyLocationNewOverlay uten å overstyre setDirectionIcon()/setPersonIcon() — OSMDroid bruker da stock hvite bitmaps.\n\nForslag:\n- Egen vector i res/drawable/ic_user_direction.xml med kontrastfarget fyll + motsatt-farge kontur\n- Kall myLocationOverlay.setDirectionIcon(...) + setPersonIcon(...) eksplisitt\n- Vurder halo-ring (blå pil i hvit ring, Google Maps-stil) for variert underlag\n- Sjekk PWA-en (Leaflet) for samme problem\n\nTilgjengelighet:\n- ≥3:1 kontrast mot både lyse og mørke tiles (WCAG 2.2 AA, ikke-tekstlig innhold)\n- Ikke avhengig av farge alene; konturen sikrer at formen faktisk synes\n\nForgejo: https://kode.naiv.no/olemd/tilfluktsrom/issues/16","acceptance_criteria":"Pilen og person-ikonet i MyLocationNewOverlay er tydelig synlig mot både lyse og mørke kartunderlag (≥3:1 kontrast). Verifisert manuelt på minst ett lyst og ett mørkt kartområde, både stillestående (person) og i bevegelse (pil).","status":"closed","priority":2,"issue_type":"bug","assignee":"Ole-Morten Duesund","owner":"olemd@glemt.net","created_at":"2026-04-29T14:06:57Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-29T14:15:13Z","started_at":"2026-04-29T14:09:43Z","closed_at":"2026-04-29T14:15:13Z","close_reason":"Fikset i d2291a2 — egne ikoner i res/drawable/ic_user_dot.xml og ic_user_arrow.xml, satt på MyLocationNewOverlay med setPersonIcon/setDirectionIcon + (0.5, 0.5)-anker. Visuell verifisering på enhet/emulator gjenstår.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"id":"tilfluktsrom-5s7","title":"PWA: manuell testing mangler","description":"Mirror av Forgejo-issue #1.\n\nPWA-versjonen (pwa/) er skrevet, men ikke manuelt testet i nettleser. Enhetstestene passerer, men appen må verifiseres i praksis:\n\n- Start utviklingsserver og test i Chrome/Firefox\n- Test offline-modus (service worker)\n- Test kompass (iOS Safari + Android Chrome)\n- Test installasjon via «Legg til på startskjerm»\n- Test kartbufring og offline kartvisning\n- Test på fysisk iPhone (iOS-spesifikk kompasshåndtering)\n- Test i18n (norsk bokmål, nynorsk, engelsk)\n\nForgejo: https://kode.naiv.no/olemd/tilfluktsrom/issues/1","status":"closed","priority":2,"issue_type":"task","owner":"olemd@glemt.net","created_at":"2026-04-29T13:57:04Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-29T14:02:57Z","closed_at":"2026-04-29T14:02:57Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"id":"tilfluktsrom-nt8","title":"Åpne i kartapp for gangvei til tilfluktsrom","description":"Mirror av Forgejo-issue #2.\n\nLegg til knapp (på bunnark og/eller kompassvisning) som åpner gangveibeskrivelse til valgt tilfluktsrom i ekstern kartapp.\n\nImplementasjon:\n- ACTION_VIEW intent med geo: URI: geo:lat,lon?q=lat,lon(Tilfluktsrom - adresse)\n- geo: håndteres av tilgjengelig kartapp (OsmAnd, Organic Maps, Google Maps, ...)\n- OsmAnd og Organic Maps støtter offline-navigasjon med geo: — ideelt for degradert nett\n- IKKE hardkode Google Maps-URL-er — bruk geo:\n- Faller pent tilbake hvis ingen kartapp er installert (Toast med koordinater å kopiere)\n- Knapp ved siden av tilfluktsrom-adresse i bunnarket\n\nI en akuttsituasjon er det å finne tilfluktsrommet på kartet bare halve problemet — du må vite gangveien dit. geo:-intent fungerer med offline-kapable kartapper, kritisk når nettet er nede.\n\nForgejo: https://kode.naiv.no/olemd/tilfluktsrom/issues/2","status":"open","priority":2,"issue_type":"feature","owner":"olemd@glemt.net","created_at":"2026-04-29T13:57:03Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-29T13:57:03Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"id":"tilfluktsrom-gmu","title":"Test og ferdigstill PWA-versjonen","description":"Mirror av Forgejo-issue #7.\n\nFå den eksisterende PWA-en i pwa/-katalogen til å fungere og testet som webfallback.\n\nStatus:\n- Vite + TypeScript + Leaflet + idb + vite-plugin-pwa\n- Shelter-data forhåndsprosesseres ved bygg (scripts/fetch-shelters.ts)\n- Markert som ikke-testet i README (issue #1)\n\nOppgaver:\n- bun install + bun run dev — fikse byggefeil\n- Verifiser at bun run fetch-shelters genererer public/data/shelters.json\n- Test offline (service worker)\n- Test på mobilnettleser (iOS Safari, Android Chrome)\n- Deploy til statisk hosting\n- Lenke til PWA fra Android-appens om-side eller README\n\nWebfallback for iOS-brukere og folk uten Android-app. Også raskest tilgang i en akuttsituasjon.\n\nForgejo: https://kode.naiv.no/olemd/tilfluktsrom/issues/7","status":"closed","priority":2,"issue_type":"task","owner":"olemd@glemt.net","created_at":"2026-04-29T13:56:46Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-29T14:02:57Z","closed_at":"2026-04-29T14:02:57Z","close_reason":"Closed","dependency_count":0,"dependent_count":1,"comment_count":0}
|
||||||
|
{"id":"tilfluktsrom-9sf","title":"Dyplenket tilfluktsrom utenfor lista vises ikke","description":"Mirror av Forgejo-issue #13.\n\nNår en dyplenke åpner et tilfluktsrom som ikke er blant de 3 nærmeste, blir det valgt i kartet, men vises ikke i lista i bunnpanelet. Brukeren ser ikke hva som er valgt.\n\nForslag:\n1. Legg til det dyplenkede tilfluktsrommet som ekstra element i lista (med markering om at det ikke er blant de 3 nærmeste), eller\n2. Rull lista slik at det valgte elementet er synlig.\n\nIdentifisert i bruksanalyse. Moderat prioritet.\n\nForgejo: https://kode.naiv.no/olemd/tilfluktsrom/issues/13","status":"closed","priority":2,"issue_type":"bug","assignee":"Ole-Morten Duesund","owner":"olemd@glemt.net","created_at":"2026-04-29T13:56:15Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-29T14:49:15Z","started_at":"2026-04-29T14:46:18Z","closed_at":"2026-04-29T14:49:15Z","close_reason":"Hybrid implementert: ShelterListItem-wrapper med isOutsideNearest-flagg, badge i item_shelter.xml, smoothScrollToPosition på rebuildShelterList. Visuell verifisering på enhet/emulator gjenstår.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"id":"tilfluktsrom-jmv","title":"Geonorge: lokalId regenereres på hver eksport — bytt til romnr som ekstern nøkkel","description":"Mirror av Forgejo-issue #15.\n\nTilfluktsromdata fra Geonorge regenererer lokalId-feltet ved hver eksport (verifisert: alle 556 lokalId-er endres mellom snapshots, mens romnr/plasser/adresse/koordinater er stabile).\n\nKonsekvens: delingslenker basert på lokalId ble brutt mellom datasett-oppdateringer. Vi har allerede byttet ekstern delingsidentifikator til romnr og beholdt lokalId som intern Room-PK.\n\nGjenstår:\n- Spørre Geonorge/DSB hvorfor lokalId regenereres (tilsiktet gml:id-stil eller FME/SOSI-feil?)\n- Hvis feil: be om at lokalId persisteres mellom eksporter\n- Hvis tilsiktet: be om dokumentasjon\n- Sjekke om WFS-endepunktet returnerer stabile ID-er\n\nForgejo: https://kode.naiv.no/olemd/tilfluktsrom/issues/15","status":"deferred","priority":2,"issue_type":"task","owner":"olemd@glemt.net","created_at":"2026-04-29T13:55:59Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-30T11:16:26Z","defer_until":"2026-10-30T00:00:00Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"id":"tilfluktsrom-jvn","title":"Filtrere tilfluktsrom etter minimumskapasitet","description":"Mirror av Forgejo-issue #5.\n\nLegg til filter for minimumskapasitet slik at brukere kan finne tilfluktsrom store nok for gruppen sin.\n\nImplementasjon:\n- Filterchip eller dropdown over tilfluktsromlista (f.eks. \"Min. plasser: 50 / 100 / 200 / Alle\")\n- Filteret gjelder både nærmeste-lista og kartmarkørene\n- Lagre valg i SharedPreferences\n- Default: vis alle (intet filter)\n\nSkoler, arbeidsplasser og familier trenger tilfluktsrom med nok kapasitet. Et lite tilfluktsrom med 20 plasser er ubrukelig for en gruppe på 50. Enkel UX-forbedring med reell praktisk verdi.\n\nForgejo: https://kode.naiv.no/olemd/tilfluktsrom/issues/5","status":"open","priority":3,"issue_type":"feature","owner":"olemd@glemt.net","created_at":"2026-04-29T13:56:51Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-29T13:56:51Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"id":"tilfluktsrom-nyz","title":"Støtte for internasjonale tilfluktsromdata","description":"Mirror av Forgejo-issue #9.\n\nI dag støtter Tilfluktsrom kun norske data fra Geonorge (GeoJSON, EPSG:25833). Mål: identifisere og integrere data fra andre land.\n\nMål:\n1. Identifisere internasjonale datakilder (NO, SE/MSB, FI/Pelastustoimi, CH/FOCP, SG/SCDF, US/FEMA)\n2. Støtte flere dataformater uten å bryte eksisterende funksjonalitet\n3. Auto-nedlasting basert på brukerens posisjon\n\nTekniske vurderinger:\n- ShelterDataSource-grensesnitt med per-land-implementasjoner\n- Parsefeil i én kilde må aldri ødelegge andre kilder (isolert per kilde, valider per record)\n- Generalisere Shelter-modellen (kjernefelt: koordinater WGS84, kapasitet, adresse, kildeland)\n- Bbox-basert dataset-registry, last bare ned relevante datasett\n- Offline-first beholdes — alle nedlastede datasett caches i Room\n\nOut of scope: brukerbidratte lokasjoner, sanntidsstatus, ruting.\n\nForgejo: https://kode.naiv.no/olemd/tilfluktsrom/issues/9 (3 kommentarer)","status":"open","priority":3,"issue_type":"feature","owner":"olemd@glemt.net","created_at":"2026-04-29T13:56:34Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-29T13:56:34Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"id":"tilfluktsrom-bnw","title":"Klyngevisning for kartmarkører","description":"Mirror av Forgejo-issue #10.\n\nNår mange tilfluktsrom ligger tett i kartet, overlapper markørene og det er vanskelig å trykke på riktig en.\n\nForslag: legg til klyngevisning (marker clustering) som grupperer nærliggende markører og viser et tall. Når brukeren zoomer inn, splittes klyngene.\n\nAlternativer:\n- Android: OSMBonusPack MarkerClusterer, eller egen logikk\n- PWA: Leaflet.markercluster-plugin\n\nLavere prioritet enn tilgjengelighetsforbedringer.\n\nForgejo: https://kode.naiv.no/olemd/tilfluktsrom/issues/10","status":"open","priority":3,"issue_type":"feature","owner":"olemd@glemt.net","created_at":"2026-04-29T13:56:23Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-29T13:56:23Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"id":"tilfluktsrom-k5i","title":"PWA: legg til sivilforsvarsinformasjons-dialog","description":"Mirror av Forgejo-issue #12.\n\nPWA-versjonen mangler sivilforsvarsinformasjonsdialogen som finnes i Android-appen (CivilDefenseInfoDialog).\n\nPort dialogen til PWA-en med samme innhold (5 steg + DSB-kilde). Vis som modal/overlay.\n\nAvhengighet: bør gjøres etter #7 (test og ferdigstill PWA).\n\nForgejo: https://kode.naiv.no/olemd/tilfluktsrom/issues/12","status":"open","priority":3,"issue_type":"feature","owner":"olemd@glemt.net","created_at":"2026-04-29T13:56:19Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-29T13:56:19Z","dependencies":[{"issue_id":"tilfluktsrom-k5i","depends_on_id":"tilfluktsrom-gmu","type":"blocks","created_at":"2026-04-29T16:01:22Z","created_by":"Ole-Morten Duesund","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
||||||
|
{"id":"tilfluktsrom-52s","title":"Migrere UI fra Views/ViewBinding til Jetpack Compose","description":"Mirror av Forgejo-issue #14.\n\nMigrere fra tradisjonelle Android Views med ViewBinding til Jetpack Compose.\n\nOmfang:\n- activity_main.xml → rot-Composable med tilstandsheving\n- RecyclerView + ShelterListAdapter → LazyColumn\n- DirectionArrowView (Canvas) → Compose Canvas\n- Bunnark → Card / BottomSheetScaffold\n- dialog_civil_defense.xml → AlertDialog composable\n- Lasteoverlegg → AnimatedVisibility\n- Innfør MainViewModel\n\nForblir Views: OSMDroid MapView (pakkes i AndroidView), Widget (RemoteViews, eventuell Glance-migrering er separat).\n\nVurderinger:\n- APK +2-3 MB (Compose runtime)\n- @Preview gir bedre dev-loop for ikke-kart-komponenter\n- Stort tiltak — ikke kritisk\n\nForgejo: https://kode.naiv.no/olemd/tilfluktsrom/issues/14","status":"open","priority":3,"issue_type":"feature","owner":"olemd@glemt.net","created_at":"2026-04-29T13:56:10Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-29T13:56:10Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"id":"tilfluktsrom-bok","title":"Forberede F-Droid-metadata for innsending","description":"Mirror av Forgejo-issue #8.\n\nNB: F-Droid-distribusjon er pauset (jf. memory feedback_fdroid_paused.md). Skal IKKE jobbes på før brukeren gjenopptar F-Droid-spor.\n\nForberede repoet for F-Droid-innsending med fastlane metadata-struktur:\n- fastlane/metadata/android/-katalog\n- en-US/ og nb-NO/ med full_description, short_description, title, changelogs/\n- images/ med skjermbilder og feature graphic\n- Dokumenter at appen bruker play-services-location men faller tilbake (anti-features)\n- Vurdere .fdroid.yml hvis spesielle byggesteg trengs\n\nForgejo: https://kode.naiv.no/olemd/tilfluktsrom/issues/8 (1 kommentar)","status":"open","priority":4,"issue_type":"task","owner":"olemd@glemt.net","created_at":"2026-04-29T13:56:40Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-29T13:56:40Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
7
.beads/metadata.json
Normal file
7
.beads/metadata.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"database": "dolt",
|
||||||
|
"backend": "dolt",
|
||||||
|
"dolt_mode": "embedded",
|
||||||
|
"dolt_database": "tilfluktsrom",
|
||||||
|
"project_id": "49020f4c-0353-4536-a242-39a3dc116f11"
|
||||||
|
}
|
||||||
26
.claude/settings.json
Normal file
26
.claude/settings.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"PreCompact": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"command": "bd prime",
|
||||||
|
"type": "command"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"matcher": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"SessionStart": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"command": "bd prime",
|
||||||
|
"type": "command"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"matcher": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -10,3 +10,8 @@
|
||||||
local.properties
|
local.properties
|
||||||
/app/build
|
/app/build
|
||||||
keystore.properties
|
keystore.properties
|
||||||
|
|
||||||
|
# Beads / Dolt files (added by bd init)
|
||||||
|
.dolt/
|
||||||
|
*.db
|
||||||
|
.beads-credential-key
|
||||||
|
|
|
||||||
84
AGENTS.md
Normal file
84
AGENTS.md
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
# Agent Instructions
|
||||||
|
|
||||||
|
This project uses **bd** (beads) for issue tracking. Run `bd prime` for full workflow context.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bd ready # Find available work
|
||||||
|
bd show <id> # View issue details
|
||||||
|
bd update <id> --claim # Claim work atomically
|
||||||
|
bd close <id> # Complete work
|
||||||
|
bd dolt push # Push beads data to remote
|
||||||
|
```
|
||||||
|
|
||||||
|
## Non-Interactive Shell Commands
|
||||||
|
|
||||||
|
**ALWAYS use non-interactive flags** with file operations to avoid hanging on confirmation prompts.
|
||||||
|
|
||||||
|
Shell commands like `cp`, `mv`, and `rm` may be aliased to include `-i` (interactive) mode on some systems, causing the agent to hang indefinitely waiting for y/n input.
|
||||||
|
|
||||||
|
**Use these forms instead:**
|
||||||
|
```bash
|
||||||
|
# Force overwrite without prompting
|
||||||
|
cp -f source dest # NOT: cp source dest
|
||||||
|
mv -f source dest # NOT: mv source dest
|
||||||
|
rm -f file # NOT: rm file
|
||||||
|
|
||||||
|
# For recursive operations
|
||||||
|
rm -rf directory # NOT: rm -r directory
|
||||||
|
cp -rf source dest # NOT: cp -r source dest
|
||||||
|
```
|
||||||
|
|
||||||
|
**Other commands that may prompt:**
|
||||||
|
- `scp` - use `-o BatchMode=yes` for non-interactive
|
||||||
|
- `ssh` - use `-o BatchMode=yes` to fail instead of prompting
|
||||||
|
- `apt-get` - use `-y` flag
|
||||||
|
- `brew` - use `HOMEBREW_NO_AUTO_UPDATE=1` env var
|
||||||
|
|
||||||
|
<!-- BEGIN BEADS INTEGRATION v:1 profile:minimal hash:ca08a54f -->
|
||||||
|
## Beads Issue Tracker
|
||||||
|
|
||||||
|
This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands.
|
||||||
|
|
||||||
|
### Quick Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bd ready # Find available work
|
||||||
|
bd show <id> # View issue details
|
||||||
|
bd update <id> --claim # Claim work
|
||||||
|
bd close <id> # Complete work
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
- Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists
|
||||||
|
- Run `bd prime` for detailed command reference and session close protocol
|
||||||
|
- Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files
|
||||||
|
|
||||||
|
## Session Completion
|
||||||
|
|
||||||
|
**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds.
|
||||||
|
|
||||||
|
**MANDATORY WORKFLOW:**
|
||||||
|
|
||||||
|
1. **File issues for remaining work** - Create issues for anything that needs follow-up
|
||||||
|
2. **Run quality gates** (if code changed) - Tests, linters, builds
|
||||||
|
3. **Update issue status** - Close finished work, update in-progress items
|
||||||
|
4. **PUSH TO REMOTE** - This is MANDATORY:
|
||||||
|
```bash
|
||||||
|
git pull --rebase
|
||||||
|
bd dolt push
|
||||||
|
git push
|
||||||
|
git status # MUST show "up to date with origin"
|
||||||
|
```
|
||||||
|
5. **Clean up** - Clear stashes, prune remote branches
|
||||||
|
6. **Verify** - All changes committed AND pushed
|
||||||
|
7. **Hand off** - Provide context for next session
|
||||||
|
|
||||||
|
**CRITICAL RULES:**
|
||||||
|
- Work is NOT complete until `git push` succeeds
|
||||||
|
- NEVER stop before pushing - that leaves work stranded locally
|
||||||
|
- NEVER say "ready to push when you are" - YOU must push
|
||||||
|
- If push fails, resolve and retry until it succeeds
|
||||||
|
<!-- END BEADS INTEGRATION -->
|
||||||
|
|
@ -196,7 +196,7 @@ Both flavors produce identical user experiences — `standard` achieves faster G
|
||||||
|
|
||||||
### Deep Linking
|
### Deep Linking
|
||||||
|
|
||||||
**HTTPS App Links:** `https://tilfluktsrom.naiv.no/shelter/{lokalId}`
|
**HTTPS App Links:** `https://tilfluktsrom.naiv.no/shelter/{romnr}`
|
||||||
|
|
||||||
The domain is configured in one place: `DEEP_LINK_DOMAIN` in `build.gradle.kts` (exposed as `BuildConfig.DEEP_LINK_DOMAIN` and manifest placeholder `${deepLinkHost}`).
|
The domain is configured in one place: `DEEP_LINK_DOMAIN` in `build.gradle.kts` (exposed as `BuildConfig.DEEP_LINK_DOMAIN` and manifest placeholder `${deepLinkHost}`).
|
||||||
|
|
||||||
|
|
@ -207,6 +207,20 @@ The domain is configured in one place: `DEEP_LINK_DOMAIN` in `build.gradle.kts`
|
||||||
|
|
||||||
Share messages include the HTTPS URL, which SMS apps auto-link as a tappable URL.
|
Share messages include the HTTPS URL, which SMS apps auto-link as a tappable URL.
|
||||||
|
|
||||||
|
#### Deep link identifier — why `romnr`, not `lokalId`
|
||||||
|
|
||||||
|
The path component is the shelter's `romnr` (DSB room number — an integer like `776`), not the GeoJSON `lokalId` UUID, even though `lokalId` is what Room and Leaflet use as the in-memory primary key.
|
||||||
|
|
||||||
|
**Empirical reason:** the upstream GeoJSON ZIP at `nedlasting.geonorge.no/.../TilfluktsromOffentlige_GeoJSON.zip` re-rolls every `lokalId` on every export. Three snapshots of the same dataset (taken Dec 2025, Apr 20 2026, Apr 27 2026) had **556/556 different lokalIds** while every other field (`romnr`, `adresse`, `plasser`, `latitude`, `longitude`) was byte-identical and the shelter count was unchanged. The most recent two snapshots are only seven days apart, so this is regular drift, not a one-off re-issue.
|
||||||
|
|
||||||
|
That makes `lokalId` unsuitable for any cross-device or cross-build identifier:
|
||||||
|
- Sender and receiver who fetched the dataset on different days have different lokalIds for the same physical shelter, so a lokalId-keyed share link fails with "shelter not found" on the receiving side.
|
||||||
|
- Even on a single device, a user who hits "Refresh data" while a shelter is selected would lose the selection if it was tracked by lokalId.
|
||||||
|
|
||||||
|
`romnr` is the actual DSB business key — the room number physically assigned to the shelter by the civil-defence authority — and is stable across exports. Verified unique (556/556) and present (no zeros) on the current dataset.
|
||||||
|
|
||||||
|
The internal Room primary key remains `lokalId` because (a) it's already the upstream-supplied UUID and changing it would force a destructive Room schema migration, and (b) within a single fetch it's a perfectly fine in-memory key. Only the *external* deep-link identifier was switched.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Progressive Web App
|
## Progressive Web App
|
||||||
|
|
|
||||||
48
CLAUDE.md
48
CLAUDE.md
|
|
@ -110,3 +110,51 @@ Current screenshots:
|
||||||
- Default (English): `res/values/strings.xml`
|
- Default (English): `res/values/strings.xml`
|
||||||
- Norwegian Bokmål: `res/values-nb/strings.xml`
|
- Norwegian Bokmål: `res/values-nb/strings.xml`
|
||||||
- Norwegian Nynorsk: `res/values-nn/strings.xml`
|
- Norwegian Nynorsk: `res/values-nn/strings.xml`
|
||||||
|
|
||||||
|
|
||||||
|
<!-- BEGIN BEADS INTEGRATION v:1 profile:minimal hash:ca08a54f -->
|
||||||
|
## Beads Issue Tracker
|
||||||
|
|
||||||
|
This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands.
|
||||||
|
|
||||||
|
### Quick Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bd ready # Find available work
|
||||||
|
bd show <id> # View issue details
|
||||||
|
bd update <id> --claim # Claim work
|
||||||
|
bd close <id> # Complete work
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
- Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists
|
||||||
|
- Run `bd prime` for detailed command reference and session close protocol
|
||||||
|
- Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files
|
||||||
|
|
||||||
|
## Session Completion
|
||||||
|
|
||||||
|
**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds.
|
||||||
|
|
||||||
|
**MANDATORY WORKFLOW:**
|
||||||
|
|
||||||
|
1. **File issues for remaining work** - Create issues for anything that needs follow-up
|
||||||
|
2. **Run quality gates** (if code changed) - Tests, linters, builds
|
||||||
|
3. **Update issue status** - Close finished work, update in-progress items
|
||||||
|
4. **PUSH TO REMOTE** - This is MANDATORY:
|
||||||
|
```bash
|
||||||
|
git pull --rebase
|
||||||
|
bd dolt push
|
||||||
|
git push
|
||||||
|
git status # MUST show "up to date with origin"
|
||||||
|
```
|
||||||
|
5. **Clean up** - Clear stashes, prune remote branches
|
||||||
|
6. **Verify** - All changes committed AND pushed
|
||||||
|
7. **Hand off** - Provide context for next session
|
||||||
|
|
||||||
|
**CRITICAL RULES:**
|
||||||
|
- Work is NOT complete until `git push` succeeds
|
||||||
|
- NEVER stop before pushing - that leaves work stranded locally
|
||||||
|
- NEVER say "ready to push when you are" - YOU must push
|
||||||
|
- If push fails, resolve and retry until it succeeds
|
||||||
|
<!-- END BEADS INTEGRATION -->
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ android {
|
||||||
applicationId = "no.naiv.tilfluktsrom"
|
applicationId = "no.naiv.tilfluktsrom"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 15
|
versionCode = 17
|
||||||
versionName = "1.10.0"
|
versionName = "1.10.2"
|
||||||
|
|
||||||
// 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"
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -5,6 +5,8 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.view.HapticFeedbackConstants
|
import android.view.HapticFeedbackConstants
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
import android.hardware.Sensor
|
import android.hardware.Sensor
|
||||||
import android.hardware.SensorEvent
|
import android.hardware.SensorEvent
|
||||||
import android.hardware.SensorEventListener
|
import android.hardware.SensorEventListener
|
||||||
|
|
@ -42,6 +44,7 @@ import no.naiv.tilfluktsrom.location.ShelterFinder
|
||||||
import no.naiv.tilfluktsrom.location.ShelterWithDistance
|
import no.naiv.tilfluktsrom.location.ShelterWithDistance
|
||||||
import no.naiv.tilfluktsrom.ui.CivilDefenseInfoDialog
|
import no.naiv.tilfluktsrom.ui.CivilDefenseInfoDialog
|
||||||
import no.naiv.tilfluktsrom.ui.ShelterListAdapter
|
import no.naiv.tilfluktsrom.ui.ShelterListAdapter
|
||||||
|
import no.naiv.tilfluktsrom.ui.ShelterListItem
|
||||||
import no.naiv.tilfluktsrom.util.DistanceUtils
|
import no.naiv.tilfluktsrom.util.DistanceUtils
|
||||||
import org.osmdroid.util.GeoPoint
|
import org.osmdroid.util.GeoPoint
|
||||||
import org.osmdroid.views.CustomZoomButtonsController
|
import org.osmdroid.views.CustomZoomButtonsController
|
||||||
|
|
@ -71,6 +74,14 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
|
||||||
private var deviceHeading = 0f
|
private var deviceHeading = 0f
|
||||||
private var isCompassMode = false
|
private var isCompassMode = false
|
||||||
private var cachingJob: Job? = null
|
private var cachingJob: Job? = null
|
||||||
|
private var refreshJob: Job? = null
|
||||||
|
|
||||||
|
// Whether to consider showing the map-cache prompt on the next location
|
||||||
|
// update. Mirrors the PWA's firstLocationFix flag: we only prompt once per
|
||||||
|
// session, regardless of whether the user accepts or skips. Without this
|
||||||
|
// guard, every location update re-checks hasCacheForLocation and re-shows
|
||||||
|
// the prompt if the user previously chose "Skip".
|
||||||
|
private var mapCachePromptPending = true
|
||||||
// Map from shelter lokalId to its map marker, for icon swapping on selection
|
// Map from shelter lokalId to its map marker, for icon swapping on selection
|
||||||
private var shelterMarkerMap: MutableMap<String, Marker> = mutableMapOf()
|
private var shelterMarkerMap: MutableMap<String, Marker> = mutableMapOf()
|
||||||
private var highlightedMarkerId: String? = null
|
private var highlightedMarkerId: String? = null
|
||||||
|
|
@ -78,8 +89,14 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
|
||||||
// Whether a compass sensor is available on this device
|
// Whether a compass sensor is available on this device
|
||||||
private var hasCompassSensor = false
|
private var hasCompassSensor = false
|
||||||
|
|
||||||
// Deep link: shelter ID to select once data is loaded
|
// Deep link: shelter to select once data is loaded.
|
||||||
private var pendingDeepLinkShelterId: String? = null
|
// We key on `romnr` (DSB's room number) rather than `lokalId` because
|
||||||
|
// upstream Geonorge re-rolls the lokalId UUID on every export. Two
|
||||||
|
// devices that fetched at different times have different lokalIds for
|
||||||
|
// the same physical shelter, breaking cross-device share links.
|
||||||
|
// Romnr is the actual DSB business key and is stable across exports.
|
||||||
|
// See ARCHITECTURE.md → "Deep link identifier".
|
||||||
|
private var pendingDeepLinkRomnr: Int? = null
|
||||||
|
|
||||||
// The currently selected shelter — can be any shelter, not just one from nearestShelters
|
// The currently selected shelter — can be any shelter, not just one from nearestShelters
|
||||||
private var selectedShelter: ShelterWithDistance? = null
|
private var selectedShelter: ShelterWithDistance? = null
|
||||||
|
|
@ -131,7 +148,9 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle https://{domain}/shelter/{lokalId} deep link.
|
* Handle https://{domain}/shelter/{romnr} deep link.
|
||||||
|
* `romnr` is DSB's stable shelter room number — see field comment on
|
||||||
|
* pendingDeepLinkRomnr for why we don't use lokalId.
|
||||||
* If shelters are already loaded, select immediately; otherwise store as pending.
|
* If shelters are already loaded, select immediately; otherwise store as pending.
|
||||||
*/
|
*/
|
||||||
private fun handleDeepLinkIntent(intent: Intent?) {
|
private fun handleDeepLinkIntent(intent: Intent?) {
|
||||||
|
|
@ -140,15 +159,15 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
|
||||||
uri.host != BuildConfig.DEEP_LINK_DOMAIN ||
|
uri.host != BuildConfig.DEEP_LINK_DOMAIN ||
|
||||||
uri.path?.startsWith("/shelter/") != true) return
|
uri.path?.startsWith("/shelter/") != true) return
|
||||||
|
|
||||||
val lokalId = uri.lastPathSegment ?: return
|
val romnr = uri.lastPathSegment?.toIntOrNull() ?: return
|
||||||
// Clear intent data so config changes don't re-trigger
|
// Clear intent data so config changes don't re-trigger
|
||||||
intent.data = null
|
intent.data = null
|
||||||
|
|
||||||
val shelter = allShelters.find { it.lokalId == lokalId }
|
val shelter = allShelters.find { it.romnr == romnr }
|
||||||
if (shelter != null) {
|
if (shelter != null) {
|
||||||
selectShelterByData(shelter)
|
selectShelterByData(shelter)
|
||||||
} else {
|
} else {
|
||||||
pendingDeepLinkShelterId = lokalId
|
pendingDeepLinkRomnr = romnr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,14 +189,37 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
|
||||||
false // Don't consume the event
|
false // Don't consume the event
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add user location overlay
|
// Add user location overlay. OSMDroid's stock person/arrow bitmaps
|
||||||
|
// are pure white and disappear on light tiles - replace with
|
||||||
|
// app-themed icons that have a white halo + drop shadow so the
|
||||||
|
// silhouette stays visible on any tile theme (Forgejo #16).
|
||||||
myLocationOverlay = MyLocationNewOverlay(
|
myLocationOverlay = MyLocationNewOverlay(
|
||||||
GpsMyLocationProvider(this@MainActivity), this
|
GpsMyLocationProvider(this@MainActivity), this
|
||||||
)
|
).apply {
|
||||||
|
drawableToBitmap(R.drawable.ic_user_dot, sizeDp = 24)?.let {
|
||||||
|
setPersonIcon(it)
|
||||||
|
setPersonAnchor(0.5f, 0.5f)
|
||||||
|
}
|
||||||
|
drawableToBitmap(R.drawable.ic_user_arrow, sizeDp = 32)?.let {
|
||||||
|
setDirectionIcon(it)
|
||||||
|
setDirectionAnchor(0.5f, 0.5f)
|
||||||
|
}
|
||||||
|
}
|
||||||
overlays.add(myLocationOverlay)
|
overlays.add(myLocationOverlay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun drawableToBitmap(@androidx.annotation.DrawableRes resId: Int, sizeDp: Int): Bitmap? {
|
||||||
|
val drawable = ContextCompat.getDrawable(this, resId) ?: return null
|
||||||
|
val sizePx = (sizeDp * resources.displayMetrics.density).toInt().coerceAtLeast(1)
|
||||||
|
val bitmap = Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888)
|
||||||
|
Canvas(bitmap).also { canvas ->
|
||||||
|
drawable.setBounds(0, 0, sizePx, sizePx)
|
||||||
|
drawable.draw(canvas)
|
||||||
|
}
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupShelterList() {
|
private fun setupShelterList() {
|
||||||
shelterAdapter = ShelterListAdapter { swd ->
|
shelterAdapter = ShelterListAdapter { swd ->
|
||||||
userSelectedShelter = true
|
userSelectedShelter = true
|
||||||
|
|
@ -281,9 +323,9 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
|
||||||
updateShelterMarkers()
|
updateShelterMarkers()
|
||||||
|
|
||||||
// Process pending deep links now that shelter data is available
|
// Process pending deep links now that shelter data is available
|
||||||
pendingDeepLinkShelterId?.let { id ->
|
pendingDeepLinkRomnr?.let { romnr ->
|
||||||
pendingDeepLinkShelterId = null
|
pendingDeepLinkRomnr = null
|
||||||
val shelter = shelters.find { it.lokalId == id }
|
val shelter = shelters.find { it.romnr == romnr }
|
||||||
if (shelter != null) {
|
if (shelter != null) {
|
||||||
selectShelterByData(shelter)
|
selectShelterByData(shelter)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -418,13 +460,16 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache map tiles on first launch
|
// Cache map tiles on first launch — at most one prompt
|
||||||
if (!mapCacheManager.hasCacheForLocation(location.latitude, location.longitude)) {
|
// per session so a "Skip" decision sticks.
|
||||||
if (isNetworkAvailable()) {
|
if (mapCachePromptPending &&
|
||||||
|
!mapCacheManager.hasCacheForLocation(location.latitude, location.longitude) &&
|
||||||
|
isNetworkAvailable()
|
||||||
|
) {
|
||||||
|
mapCachePromptPending = false
|
||||||
cacheMapTiles(location.latitude, location.longitude)
|
cacheMapTiles(location.latitude, location.longitude)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
throw e
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
@ -442,25 +487,60 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
|
||||||
allShelters, location.latitude, location.longitude, NEAREST_COUNT
|
allShelters, location.latitude, location.longitude, NEAREST_COUNT
|
||||||
)
|
)
|
||||||
|
|
||||||
// Highlight which nearest-list item matches the current selection
|
|
||||||
val selectedIdx = if (selectedShelter != null) {
|
|
||||||
nearestShelters.indexOfFirst { it.shelter.lokalId == selectedShelter!!.shelter.lokalId }
|
|
||||||
} else -1
|
|
||||||
|
|
||||||
shelterAdapter.submitList(nearestShelters)
|
|
||||||
shelterAdapter.selectPosition(selectedIdx)
|
|
||||||
|
|
||||||
if (userSelectedShelter && selectedShelter != null) {
|
if (userSelectedShelter && selectedShelter != null) {
|
||||||
// Recalculate distance/bearing for the user's picked shelter
|
// Recalculate distance/bearing for the user's picked shelter
|
||||||
refreshSelectedShelterDistance(location)
|
refreshSelectedShelterDistance(location)
|
||||||
} else {
|
rebuildShelterList()
|
||||||
// Auto-select nearest
|
updateSelectedShelterUI()
|
||||||
if (nearestShelters.isNotEmpty()) {
|
} else if (nearestShelters.isNotEmpty()) {
|
||||||
|
// Auto-select nearest; selectShelter handles list rebuild + UI
|
||||||
selectShelter(nearestShelters[0])
|
selectShelter(nearestShelters[0])
|
||||||
|
} else {
|
||||||
|
rebuildShelterList()
|
||||||
|
updateSelectedShelterUI()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSelectedShelterUI()
|
/**
|
||||||
|
* Rebuild the bottom-sheet list from the current nearest set + selection.
|
||||||
|
*
|
||||||
|
* Hybrid behaviour for Forgejo #13: when a shelter has been explicitly
|
||||||
|
* selected (deep link, marker tap, ...) and is *not* among the N nearest,
|
||||||
|
* append it to the list with an "outside nearest" badge so the user can
|
||||||
|
* see what they selected. The list also auto-scrolls to the selected
|
||||||
|
* row, so a manually-picked nearby entry comes into view too.
|
||||||
|
*/
|
||||||
|
private fun rebuildShelterList() {
|
||||||
|
val items = nearestShelters
|
||||||
|
.map { ShelterListItem(it, isOutsideNearest = false) }
|
||||||
|
.toMutableList()
|
||||||
|
|
||||||
|
val selected = selectedShelter
|
||||||
|
val isSelectedAmongNearest = selected != null &&
|
||||||
|
nearestShelters.any { it.shelter.lokalId == selected.shelter.lokalId }
|
||||||
|
if (selected != null && !isSelectedAmongNearest) {
|
||||||
|
// Only flag as "outside nearest" when there *is* a nearest list to
|
||||||
|
// contrast with - otherwise the selection is just the only entry.
|
||||||
|
items.add(
|
||||||
|
ShelterListItem(
|
||||||
|
selected,
|
||||||
|
isOutsideNearest = nearestShelters.isNotEmpty()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
shelterAdapter.submitList(items)
|
||||||
|
|
||||||
|
val selectedIdx = if (selected != null) {
|
||||||
|
items.indexOfFirst { it.swd.shelter.lokalId == selected.shelter.lokalId }
|
||||||
|
} else -1
|
||||||
|
shelterAdapter.selectPosition(selectedIdx)
|
||||||
|
|
||||||
|
if (selectedIdx >= 0) {
|
||||||
|
binding.shelterList.post {
|
||||||
|
binding.shelterList.smoothScrollToPosition(selectedIdx)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -471,10 +551,7 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
|
||||||
selectedShelter = swd
|
selectedShelter = swd
|
||||||
currentLocation?.let { refreshSelectedShelterDistance(it) }
|
currentLocation?.let { refreshSelectedShelterDistance(it) }
|
||||||
|
|
||||||
// Update list highlight
|
rebuildShelterList()
|
||||||
val idx = nearestShelters.indexOfFirst { it.shelter.lokalId == swd.shelter.lokalId }
|
|
||||||
shelterAdapter.selectPosition(idx)
|
|
||||||
|
|
||||||
updateSelectedShelterUI()
|
updateSelectedShelterUI()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -681,8 +758,17 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
lifecycleScope.launch {
|
// Guard against double-tap / overlapping refreshes. Without this, the
|
||||||
|
// user can fire several refreshData() calls that serialize on the
|
||||||
|
// single Room write-lock and stack 30–90 s of OkHttp timeouts on top
|
||||||
|
// of each other — perceived as "hang" with no feedback.
|
||||||
|
if (refreshJob?.isActive == true) return
|
||||||
|
|
||||||
binding.statusText.text = getString(R.string.status_updating)
|
binding.statusText.text = getString(R.string.status_updating)
|
||||||
|
showLoading(getString(R.string.loading_shelters))
|
||||||
|
|
||||||
|
refreshJob = lifecycleScope.launch {
|
||||||
|
try {
|
||||||
val success = repository.refreshData()
|
val success = repository.refreshData()
|
||||||
if (success) {
|
if (success) {
|
||||||
updateFreshnessIndicator()
|
updateFreshnessIndicator()
|
||||||
|
|
@ -690,6 +776,9 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(this@MainActivity, R.string.update_failed, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this@MainActivity, R.string.update_failed, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
hideLoading()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -706,7 +795,10 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
val shelter = selected.shelter
|
val shelter = selected.shelter
|
||||||
val deepLink = "https://${BuildConfig.DEEP_LINK_DOMAIN}/shelter/${shelter.lokalId}"
|
// Path component is romnr (stable DSB business key), not lokalId —
|
||||||
|
// upstream re-rolls lokalId on every Geonorge export, which would
|
||||||
|
// break cross-device links. See pendingDeepLinkRomnr comment.
|
||||||
|
val deepLink = "https://${BuildConfig.DEEP_LINK_DOMAIN}/shelter/${shelter.romnr}"
|
||||||
val body = getString(
|
val body = getString(
|
||||||
R.string.share_body,
|
R.string.share_body,
|
||||||
shelter.adresse,
|
shelter.adresse,
|
||||||
|
|
|
||||||
|
|
@ -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.10.0")
|
.header("User-Agent", "Tilfluktsrom/1.10.2")
|
||||||
.build())
|
.build())
|
||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
package no.naiv.tilfluktsrom.ui
|
package no.naiv.tilfluktsrom.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.Html
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import no.naiv.tilfluktsrom.R
|
import no.naiv.tilfluktsrom.R
|
||||||
|
|
||||||
|
|
@ -28,7 +31,12 @@ class AboutDialog : DialogFragment() {
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
return inflater.inflate(R.layout.dialog_about, container, false)
|
val view = inflater.inflate(R.layout.dialog_about, container, false)
|
||||||
|
view.findViewById<TextView>(R.id.about_data_links).apply {
|
||||||
|
text = Html.fromHtml(getString(R.string.about_data_links), Html.FROM_HTML_MODE_COMPACT)
|
||||||
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
}
|
||||||
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package no.naiv.tilfluktsrom.ui
|
||||||
|
|
||||||
import android.view.HapticFeedbackConstants
|
import android.view.HapticFeedbackConstants
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
|
@ -11,12 +12,23 @@ import no.naiv.tilfluktsrom.databinding.ItemShelterBinding
|
||||||
import no.naiv.tilfluktsrom.location.ShelterWithDistance
|
import no.naiv.tilfluktsrom.location.ShelterWithDistance
|
||||||
import no.naiv.tilfluktsrom.util.DistanceUtils
|
import no.naiv.tilfluktsrom.util.DistanceUtils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One row in the bottom-sheet list. The list normally holds the N nearest
|
||||||
|
* shelters to the user, but a deep-linked / explicitly-selected shelter that
|
||||||
|
* is *not* among them is appended with isOutsideNearest=true so the user can
|
||||||
|
* see what they picked. See Forgejo #13 / beads tilfluktsrom-9sf.
|
||||||
|
*/
|
||||||
|
data class ShelterListItem(
|
||||||
|
val swd: ShelterWithDistance,
|
||||||
|
val isOutsideNearest: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapter for the list of nearest shelters shown in the bottom sheet.
|
* Adapter for the list of nearest shelters shown in the bottom sheet.
|
||||||
*/
|
*/
|
||||||
class ShelterListAdapter(
|
class ShelterListAdapter(
|
||||||
private val onShelterSelected: (ShelterWithDistance) -> Unit
|
private val onShelterSelected: (ShelterWithDistance) -> Unit
|
||||||
) : ListAdapter<ShelterWithDistance, ShelterListAdapter.ViewHolder>(DIFF_CALLBACK) {
|
) : ListAdapter<ShelterListItem, ShelterListAdapter.ViewHolder>(DIFF_CALLBACK) {
|
||||||
|
|
||||||
private var selectedPosition = 0
|
private var selectedPosition = 0
|
||||||
|
|
||||||
|
|
@ -42,23 +54,34 @@ class ShelterListAdapter(
|
||||||
private val binding: ItemShelterBinding
|
private val binding: ItemShelterBinding
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
fun bind(item: ShelterWithDistance, isSelected: Boolean) {
|
fun bind(item: ShelterListItem, isSelected: Boolean) {
|
||||||
val ctx = binding.root.context
|
val ctx = binding.root.context
|
||||||
binding.shelterAddress.text = item.shelter.adresse
|
val swd = item.swd
|
||||||
binding.shelterDistance.text = DistanceUtils.formatDistance(item.distanceMeters)
|
binding.shelterAddress.text = swd.shelter.adresse
|
||||||
|
binding.shelterDistance.text = DistanceUtils.formatDistance(swd.distanceMeters)
|
||||||
binding.shelterCapacity.text = ctx.getString(
|
binding.shelterCapacity.text = ctx.getString(
|
||||||
R.string.shelter_capacity, item.shelter.plasser
|
R.string.shelter_capacity, swd.shelter.plasser
|
||||||
)
|
)
|
||||||
binding.shelterRoomNr.text = ctx.getString(
|
binding.shelterRoomNr.text = ctx.getString(
|
||||||
R.string.shelter_room_nr, item.shelter.romnr
|
R.string.shelter_room_nr, swd.shelter.romnr
|
||||||
)
|
)
|
||||||
|
|
||||||
binding.root.contentDescription = ctx.getString(
|
binding.outsideNearestBadge.visibility =
|
||||||
|
if (item.isOutsideNearest) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
// Build accessible description; suffix the badge text so screen-
|
||||||
|
// reader users learn the same context that sighted users see.
|
||||||
|
val baseDesc = ctx.getString(
|
||||||
R.string.content_desc_shelter_item,
|
R.string.content_desc_shelter_item,
|
||||||
item.shelter.adresse,
|
swd.shelter.adresse,
|
||||||
DistanceUtils.formatDistance(item.distanceMeters),
|
DistanceUtils.formatDistance(swd.distanceMeters),
|
||||||
item.shelter.plasser
|
swd.shelter.plasser
|
||||||
)
|
)
|
||||||
|
binding.root.contentDescription = if (item.isOutsideNearest) {
|
||||||
|
ctx.getString(R.string.shelter_outside_nearest_badge) + ". " + baseDesc
|
||||||
|
} else {
|
||||||
|
baseDesc
|
||||||
|
}
|
||||||
|
|
||||||
binding.root.isSelected = isSelected
|
binding.root.isSelected = isSelected
|
||||||
binding.root.alpha = if (isSelected) 1.0f else 0.7f
|
binding.root.alpha = if (isSelected) 1.0f else 0.7f
|
||||||
|
|
@ -68,18 +91,18 @@ class ShelterListAdapter(
|
||||||
val pos = adapterPosition
|
val pos = adapterPosition
|
||||||
if (pos != RecyclerView.NO_POSITION) {
|
if (pos != RecyclerView.NO_POSITION) {
|
||||||
selectPosition(pos)
|
selectPosition(pos)
|
||||||
onShelterSelected(getItem(pos))
|
onShelterSelected(getItem(pos).swd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<ShelterWithDistance>() {
|
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<ShelterListItem>() {
|
||||||
override fun areItemsTheSame(a: ShelterWithDistance, b: ShelterWithDistance) =
|
override fun areItemsTheSame(a: ShelterListItem, b: ShelterListItem) =
|
||||||
a.shelter.lokalId == b.shelter.lokalId
|
a.swd.shelter.lokalId == b.swd.shelter.lokalId
|
||||||
|
|
||||||
override fun areContentsTheSame(a: ShelterWithDistance, b: ShelterWithDistance) =
|
override fun areContentsTheSame(a: ShelterListItem, b: ShelterListItem) =
|
||||||
a == b
|
a == b
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
20
app/src/main/res/drawable/ic_user_arrow.xml
Normal file
20
app/src/main/res/drawable/ic_user_arrow.xml
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- User direction marker (moving): orange chevron with white halo + shadow.
|
||||||
|
Points up at 0deg; OSMDroid rotates by bearing. The triple stack (shadow,
|
||||||
|
white outline, orange fill) ensures the silhouette is visible against
|
||||||
|
any tile theme - addresses Forgejo #16. -->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="32dp"
|
||||||
|
android:height="32dp"
|
||||||
|
android:viewportWidth="32"
|
||||||
|
android:viewportHeight="32">
|
||||||
|
<path
|
||||||
|
android:fillColor="#55000000"
|
||||||
|
android:pathData="M16,2.5 L29.5,29 L16,22.5 L2.5,29 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M16,3 L29,28.5 L16,22 L3,28.5 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFF6B35"
|
||||||
|
android:pathData="M16,7 L25.5,26 L16,21.5 L6.5,26 Z" />
|
||||||
|
</vector>
|
||||||
20
app/src/main/res/drawable/ic_user_dot.xml
Normal file
20
app/src/main/res/drawable/ic_user_dot.xml
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- User location marker (stationary): orange dot, white halo, soft shadow.
|
||||||
|
White ring guarantees >=3:1 contrast against dark tiles; orange core
|
||||||
|
stays visible against light tiles; shadow keeps the silhouette readable
|
||||||
|
when both layers happen to land on near-white snow/sand. -->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#33000000"
|
||||||
|
android:pathData="M12,12 m-11,0 a11,11 0 1,0 22,0 a11,11 0 1,0 -22,0 z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M12,12 m-9.5,0 a9.5,9.5 0 1,0 19,0 a9.5,9.5 0 1,0 -19,0 z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFF6B35"
|
||||||
|
android:pathData="M12,12 m-6,0 a6,6 0 1,0 12,0 a6,6 0 1,0 -12,0 z" />
|
||||||
|
</vector>
|
||||||
|
|
@ -60,11 +60,20 @@
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="8dp"
|
||||||
android:text="@string/about_data_body"
|
android:text="@string/about_data_body"
|
||||||
android:textColor="@color/text_secondary"
|
android:textColor="@color/text_secondary"
|
||||||
android:textSize="14sp" />
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/about_data_links"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textColorLink="@color/shelter_primary"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
<!-- Stored on device section -->
|
<!-- Stored on device section -->
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,21 @@
|
||||||
android:paddingHorizontal="12dp"
|
android:paddingHorizontal="12dp"
|
||||||
android:paddingVertical="8dp">
|
android:paddingVertical="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/outsideNearestBadge"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:background="@color/warning_bg"
|
||||||
|
android:paddingHorizontal="8dp"
|
||||||
|
android:paddingVertical="2dp"
|
||||||
|
android:text="@string/shelter_outside_nearest_badge"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/shelterAddress"
|
android:id="@+id/shelterAddress"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@
|
||||||
<!-- Tilgjengelighet -->
|
<!-- Tilgjengelighet -->
|
||||||
<string name="direction_arrow_description">Retning til tilfluktsrom, %s unna</string>
|
<string name="direction_arrow_description">Retning til tilfluktsrom, %s unna</string>
|
||||||
<string name="content_desc_shelter_item">%1$s, %2$s, %3$d plasser</string>
|
<string name="content_desc_shelter_item">%1$s, %2$s, %3$d plasser</string>
|
||||||
|
<string name="shelter_outside_nearest_badge">Valgt – utenfor nærområdet</string>
|
||||||
<string name="compass_accuracy_warning">Upresist kompass - %s</string>
|
<string name="compass_accuracy_warning">Upresist kompass - %s</string>
|
||||||
<string name="a11y_map">Tilfluktsromkart</string>
|
<string name="a11y_map">Tilfluktsromkart</string>
|
||||||
<string name="a11y_compass">Kompassnavigasjon</string>
|
<string name="a11y_compass">Kompassnavigasjon</string>
|
||||||
|
|
@ -97,6 +98,7 @@
|
||||||
<string name="about_privacy_body">Denne appen samler ikke inn, sender eller deler noen personopplysninger. Det finnes ingen analyse, sporing eller tredjepartstjenester.\n\nGPS-posisjonen din brukes bare lokalt på enheten din for å finne tilfluktsrom i nærheten, og sendes aldri til noen server.</string>
|
<string name="about_privacy_body">Denne appen samler ikke inn, sender eller deler noen personopplysninger. Det finnes ingen analyse, sporing eller tredjepartstjenester.\n\nGPS-posisjonen din brukes bare lokalt på enheten din for å finne tilfluktsrom i nærheten, og sendes aldri til noen server.</string>
|
||||||
<string name="about_data_title">Datakilder</string>
|
<string name="about_data_title">Datakilder</string>
|
||||||
<string name="about_data_body">Tilfluktsromdata er offentlig informasjon fra DSB (Direktoratet for samfunnssikkerhet og beredskap), distribuert via Geonorge. Kartfliser lastes fra OpenStreetMap. Begge lagres lokalt for frakoblet bruk.</string>
|
<string name="about_data_body">Tilfluktsromdata er offentlig informasjon fra DSB (Direktoratet for samfunnssikkerhet og beredskap), distribuert via Geonorge. Kartfliser lastes fra OpenStreetMap. Begge lagres lokalt for frakoblet bruk.</string>
|
||||||
|
<string name="about_data_links"><![CDATA[Mer om datasettet:<br/>• <a href="https://kartkatalog.geonorge.no/metadata/tilfluktsrom-offentlige/dbae9aae-10e7-4b75-8d67-7f0e8828f3d8">Geonorge kartkatalog</a><br/>• <a href="https://register.geonorge.no/register/versjoner/produktark/direktoratet-for-samfunnssikkerhet-og-beredskap/tilfluktsrom-offentlige">Produktark (DSB)</a>]]></string>
|
||||||
<string name="about_stored_title">Lagret på enheten din</string>
|
<string name="about_stored_title">Lagret på enheten din</string>
|
||||||
<string name="about_stored_body">• Tilfluktsromdatabase (offentlige data fra DSB)\n• Kartfliser for frakoblet bruk\n\nIngen data forlater enheten din bortsett fra forespørsler om å laste ned tilfluktsromdata og kartfliser.</string>
|
<string name="about_stored_body">• Tilfluktsromdatabase (offentlige data fra DSB)\n• Kartfliser for frakoblet bruk\n\nIngen data forlater enheten din bortsett fra forespørsler om å laste ned tilfluktsromdata og kartfliser.</string>
|
||||||
<string name="about_open_source">Åpen kildekode — kode.naiv.no/olemd/tilfluktsrom</string>
|
<string name="about_open_source">Åpen kildekode — kode.naiv.no/olemd/tilfluktsrom</string>
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@
|
||||||
<!-- Tilgjenge -->
|
<!-- Tilgjenge -->
|
||||||
<string name="direction_arrow_description">Retning til tilfluktsrom, %s unna</string>
|
<string name="direction_arrow_description">Retning til tilfluktsrom, %s unna</string>
|
||||||
<string name="content_desc_shelter_item">%1$s, %2$s, %3$d plassar</string>
|
<string name="content_desc_shelter_item">%1$s, %2$s, %3$d plassar</string>
|
||||||
|
<string name="shelter_outside_nearest_badge">Vald – utanfor nærområdet</string>
|
||||||
<string name="compass_accuracy_warning">Upresis kompass - %s</string>
|
<string name="compass_accuracy_warning">Upresis kompass - %s</string>
|
||||||
<string name="a11y_map">Tilfluktsromkart</string>
|
<string name="a11y_map">Tilfluktsromkart</string>
|
||||||
<string name="a11y_compass">Kompassnavigasjon</string>
|
<string name="a11y_compass">Kompassnavigasjon</string>
|
||||||
|
|
@ -97,6 +98,7 @@
|
||||||
<string name="about_privacy_body">Denne appen samlar ikkje inn, sender eller deler nokon personopplysingar. Det finst ingen analyse, sporing eller tredjepartstenester.\n\nGPS-posisjonen din vert berre brukt lokalt på eininga di for å finne tilfluktsrom i nærleiken, og vert aldri sendt til nokon tenar.</string>
|
<string name="about_privacy_body">Denne appen samlar ikkje inn, sender eller deler nokon personopplysingar. Det finst ingen analyse, sporing eller tredjepartstenester.\n\nGPS-posisjonen din vert berre brukt lokalt på eininga di for å finne tilfluktsrom i nærleiken, og vert aldri sendt til nokon tenar.</string>
|
||||||
<string name="about_data_title">Datakjelder</string>
|
<string name="about_data_title">Datakjelder</string>
|
||||||
<string name="about_data_body">Tilfluktsromdata er offentleg informasjon frå DSB (Direktoratet for samfunnstryggleik og beredskap), distribuert via Geonorge. Kartfliser vert lasta frå OpenStreetMap. Begge vert lagra lokalt for fråkopla bruk.</string>
|
<string name="about_data_body">Tilfluktsromdata er offentleg informasjon frå DSB (Direktoratet for samfunnstryggleik og beredskap), distribuert via Geonorge. Kartfliser vert lasta frå OpenStreetMap. Begge vert lagra lokalt for fråkopla bruk.</string>
|
||||||
|
<string name="about_data_links"><![CDATA[Meir om datasettet:<br/>• <a href="https://kartkatalog.geonorge.no/metadata/tilfluktsrom-offentlige/dbae9aae-10e7-4b75-8d67-7f0e8828f3d8">Geonorge kartkatalog</a><br/>• <a href="https://register.geonorge.no/register/versjoner/produktark/direktoratet-for-samfunnssikkerhet-og-beredskap/tilfluktsrom-offentlige">Produktark (DSB)</a>]]></string>
|
||||||
<string name="about_stored_title">Lagra på eininga di</string>
|
<string name="about_stored_title">Lagra på eininga di</string>
|
||||||
<string name="about_stored_body">• Tilfluktsromdatabase (offentlege data frå DSB)\n• Kartfliser for fråkopla bruk\n\nIngen data forlèt eininga di bortsett frå førespurnader om å laste ned tilfluktsromdata og kartfliser.</string>
|
<string name="about_stored_body">• Tilfluktsromdatabase (offentlege data frå DSB)\n• Kartfliser for fråkopla bruk\n\nIngen data forlèt eininga di bortsett frå førespurnader om å laste ned tilfluktsromdata og kartfliser.</string>
|
||||||
<string name="about_open_source">Open kjeldekode — kode.naiv.no/olemd/tilfluktsrom</string>
|
<string name="about_open_source">Open kjeldekode — kode.naiv.no/olemd/tilfluktsrom</string>
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,9 @@
|
||||||
<!-- Accessibility -->
|
<!-- Accessibility -->
|
||||||
<string name="direction_arrow_description">Direction to shelter, %s away</string>
|
<string name="direction_arrow_description">Direction to shelter, %s away</string>
|
||||||
<string name="content_desc_shelter_item">%1$s, %2$s, %3$d places</string>
|
<string name="content_desc_shelter_item">%1$s, %2$s, %3$d places</string>
|
||||||
|
<!-- Badge shown on a list row that is not among the nearest shelters but
|
||||||
|
was explicitly selected (e.g. via a deep link). Forgejo #13. -->
|
||||||
|
<string name="shelter_outside_nearest_badge">Selected (outside nearest)</string>
|
||||||
<string name="compass_accuracy_warning">Low accuracy - %s</string>
|
<string name="compass_accuracy_warning">Low accuracy - %s</string>
|
||||||
<string name="a11y_map">Shelter map</string>
|
<string name="a11y_map">Shelter map</string>
|
||||||
<string name="a11y_compass">Compass navigation</string>
|
<string name="a11y_compass">Compass navigation</string>
|
||||||
|
|
@ -98,6 +101,7 @@
|
||||||
<string name="about_privacy_body">This app does not collect, transmit, or share any personal data. There are no analytics, tracking, or third-party services.\n\nYour GPS location is used only on your device to find nearby shelters and is never sent to any server.</string>
|
<string name="about_privacy_body">This app does not collect, transmit, or share any personal data. There are no analytics, tracking, or third-party services.\n\nYour GPS location is used only on your device to find nearby shelters and is never sent to any server.</string>
|
||||||
<string name="about_data_title">Data sources</string>
|
<string name="about_data_title">Data sources</string>
|
||||||
<string name="about_data_body">Shelter data is public information from DSB (Norwegian Directorate for Civil Protection), distributed via Geonorge. Map tiles are loaded from OpenStreetMap. Both are cached locally for offline use.</string>
|
<string name="about_data_body">Shelter data is public information from DSB (Norwegian Directorate for Civil Protection), distributed via Geonorge. Map tiles are loaded from OpenStreetMap. Both are cached locally for offline use.</string>
|
||||||
|
<string name="about_data_links"><![CDATA[Learn more about the dataset:<br/>• <a href="https://kartkatalog.geonorge.no/metadata/tilfluktsrom-offentlige/dbae9aae-10e7-4b75-8d67-7f0e8828f3d8">Geonorge metadata catalogue</a><br/>• <a href="https://register.geonorge.no/register/versjoner/produktark/direktoratet-for-samfunnssikkerhet-og-beredskap/tilfluktsrom-offentlige">DSB product sheet</a>]]></string>
|
||||||
<string name="about_stored_title">Stored on your device</string>
|
<string name="about_stored_title">Stored on your device</string>
|
||||||
<string name="about_stored_body">• Shelter database (public data from DSB)\n• Map tiles for offline use\n\nNo data leaves your device except requests to download shelter data and map tiles.</string>
|
<string name="about_stored_body">• Shelter database (public data from DSB)\n• Map tiles for offline use\n\nNo data leaves your device except requests to download shelter data and map tiles.</string>
|
||||||
<string name="about_open_source">Open source — kode.naiv.no/olemd/tilfluktsrom</string>
|
<string name="about_open_source">Open source — kode.naiv.no/olemd/tilfluktsrom</string>
|
||||||
|
|
|
||||||
2
fastlane/metadata/android/en-US/changelogs/15.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/15.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
- Actionable banner when location is unavailable (permissions, disabled services, or no GPS fix)
|
||||||
|
- Home screen widget removed
|
||||||
4
fastlane/metadata/android/en-US/changelogs/16.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/16.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
- Deep links now use the shelter room number, which is stable across data refreshes — links shared between devices work reliably
|
||||||
|
- Refresh button shows a loading indicator and no longer appears to hang
|
||||||
|
- "Cache map" prompt no longer reappears after you tap Skip
|
||||||
|
- Refreshed bundled shelter data
|
||||||
2
fastlane/metadata/android/en-US/changelogs/17.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/17.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
- PWA-only release: share button is now visually consistent with the other status-bar icons, and updates apply automatically the next time you bring the app to the foreground (no manual reload needed)
|
||||||
|
- No Android changes
|
||||||
2
fastlane/metadata/android/nb-NO/changelogs/15.txt
Normal file
2
fastlane/metadata/android/nb-NO/changelogs/15.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
- Handlingsbanner når posisjon ikke er tilgjengelig (tillatelser, avslåtte stedstjenester eller manglende GPS-fix)
|
||||||
|
- Hjemmeskjerm-widget fjernet
|
||||||
4
fastlane/metadata/android/nb-NO/changelogs/16.txt
Normal file
4
fastlane/metadata/android/nb-NO/changelogs/16.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
- Delingslenker bruker nå romnummeret, som er stabilt på tvers av dataoppdateringer — lenker som deles mellom enheter virker pålitelig
|
||||||
|
- Oppdater-knappen viser en lasteindikator og ser ikke lenger ut til å henge
|
||||||
|
- "Lagre kart"-spørsmålet dukker ikke lenger opp igjen etter at du har trykket Hopp over
|
||||||
|
- Oppdatert pakket tilfluktsromdata
|
||||||
2
fastlane/metadata/android/nb-NO/changelogs/17.txt
Normal file
2
fastlane/metadata/android/nb-NO/changelogs/17.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
- PWA-utgivelse: del-knappen er visuelt konsistent med de andre ikonene i statuslinjen, og oppdateringer trer i kraft automatisk neste gang du henter appen i forgrunnen (ingen manuell omlasting)
|
||||||
|
- Ingen Android-endringer
|
||||||
2
fastlane/metadata/android/nn-NO/changelogs/15.txt
Normal file
2
fastlane/metadata/android/nn-NO/changelogs/15.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
- Handlingsbanner når posisjonen ikkje er tilgjengeleg (løyve, avslegne stadtenester eller manglande GPS-fix)
|
||||||
|
- Heimeskjerm-widgeten er fjerna
|
||||||
4
fastlane/metadata/android/nn-NO/changelogs/16.txt
Normal file
4
fastlane/metadata/android/nn-NO/changelogs/16.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
- Delingslenker brukar no romnummeret, som er stabilt på tvers av dataoppdateringar — lenker som blir delte mellom einingar verkar pålitelig
|
||||||
|
- Oppdater-knappen viser ein lasteindikator og ser ikkje lenger ut til å henge
|
||||||
|
- "Lagre kart"-spørsmålet dukkar ikkje opp att etter at du har trykt Hopp over
|
||||||
|
- Oppdatert pakka tilfluktsromdata
|
||||||
2
fastlane/metadata/android/nn-NO/changelogs/17.txt
Normal file
2
fastlane/metadata/android/nn-NO/changelogs/17.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
- PWA-utgjeving: del-knappen er visuelt jamn med dei andre ikona i statuslinja, og oppdateringar tek effekt automatisk neste gong du hentar appen i framgrunnen (inga manuell omlasting)
|
||||||
|
- Ingen Android-endringar
|
||||||
|
|
@ -25,6 +25,11 @@
|
||||||
<header id="status-bar" role="banner">
|
<header id="status-bar" role="banner">
|
||||||
<span id="status-text" aria-live="polite"></span>
|
<span id="status-text" aria-live="polite"></span>
|
||||||
<button id="about-btn" aria-label="About">ℹ</button>
|
<button id="about-btn" aria-label="About">ℹ</button>
|
||||||
|
<button id="share-btn" aria-label="Share shelter">
|
||||||
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden="true" focusable="false">
|
||||||
|
<path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92-1.31-2.92-2.92-2.92z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button id="refresh-btn" aria-label="Refresh data">↻</button>
|
<button id="refresh-btn" aria-label="Refresh data">↻</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
|
||||||
159
pwa/src/app.ts
159
pwa/src/app.ts
|
|
@ -11,6 +11,7 @@ import type { Shelter, ShelterWithDistance, LatLon } from './types';
|
||||||
import { t } from './i18n/i18n';
|
import { t } from './i18n/i18n';
|
||||||
import { formatDistance, distanceMeters, bearingDegrees } from './util/distance-utils';
|
import { formatDistance, distanceMeters, bearingDegrees } from './util/distance-utils';
|
||||||
import { findNearest } from './location/shelter-finder';
|
import { findNearest } from './location/shelter-finder';
|
||||||
|
import { DEEP_LINK_DOMAIN } from './config';
|
||||||
import * as repo from './data/shelter-repository';
|
import * as repo from './data/shelter-repository';
|
||||||
import * as locationProvider from './location/location-provider';
|
import * as locationProvider from './location/location-provider';
|
||||||
import * as compassProvider from './location/compass-provider';
|
import * as compassProvider from './location/compass-provider';
|
||||||
|
|
@ -35,6 +36,12 @@ let firstLocationFix = true;
|
||||||
// Track whether user manually selected a shelter (prevents auto-reselection
|
// Track whether user manually selected a shelter (prevents auto-reselection
|
||||||
// on location updates)
|
// on location updates)
|
||||||
let userSelectedShelter = false;
|
let userSelectedShelter = false;
|
||||||
|
// Romnr (DSB room number) of the user-selected shelter — survives location
|
||||||
|
// updates that recompute nearestShelters (deep links, marker taps to a
|
||||||
|
// far-away shelter), and *also* survives a forceRefresh() that replaces
|
||||||
|
// every lokalId in the dataset. Romnr is the stable upstream business key;
|
||||||
|
// see ARCHITECTURE.md → "Deep link identifier" for rationale.
|
||||||
|
let selectedRomnr: number | null = null;
|
||||||
|
|
||||||
export async function init(): Promise<void> {
|
export async function init(): Promise<void> {
|
||||||
applyA11yLabels();
|
applyA11yLabels();
|
||||||
|
|
@ -58,21 +65,16 @@ function applyA11yLabels(): void {
|
||||||
document.getElementById('bottom-sheet')?.setAttribute('aria-label', t('a11y_shelter_info'));
|
document.getElementById('bottom-sheet')?.setAttribute('aria-label', t('a11y_shelter_info'));
|
||||||
document.getElementById('shelter-list')?.setAttribute('aria-label', t('a11y_nearest_shelters'));
|
document.getElementById('shelter-list')?.setAttribute('aria-label', t('a11y_nearest_shelters'));
|
||||||
document.getElementById('refresh-btn')?.setAttribute('aria-label', t('action_refresh'));
|
document.getElementById('refresh-btn')?.setAttribute('aria-label', t('action_refresh'));
|
||||||
|
document.getElementById('share-btn')?.setAttribute('aria-label', t('action_share'));
|
||||||
document.getElementById('toggle-fab')?.setAttribute('aria-label', t('action_toggle_view'));
|
document.getElementById('toggle-fab')?.setAttribute('aria-label', t('action_toggle_view'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupMap(): void {
|
function setupMap(): void {
|
||||||
const container = document.getElementById('map-container')!;
|
const container = document.getElementById('map-container')!;
|
||||||
mapView.initMap(container, (shelter: Shelter) => {
|
mapView.initMap(container, (shelter: Shelter) => {
|
||||||
// Marker click — select this shelter
|
// Marker click — select this shelter (route through selectShelterByData
|
||||||
const idx = nearestShelters.findIndex(
|
// so a tap on a far-away marker also survives location updates).
|
||||||
(s) => s.shelter.lokalId === shelter.lokalId,
|
selectShelterByData(shelter);
|
||||||
);
|
|
||||||
if (idx >= 0) {
|
|
||||||
userSelectedShelter = true;
|
|
||||||
selectedShelterIndex = idx;
|
|
||||||
updateSelectedShelter(true);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,6 +88,7 @@ function setupShelterList(): void {
|
||||||
shelterList.initShelterList(container, (index: number) => {
|
shelterList.initShelterList(container, (index: number) => {
|
||||||
userSelectedShelter = true;
|
userSelectedShelter = true;
|
||||||
selectedShelterIndex = index;
|
selectedShelterIndex = index;
|
||||||
|
selectedRomnr = nearestShelters[index]?.shelter.romnr ?? null;
|
||||||
updateSelectedShelter(true);
|
updateSelectedShelter(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -132,6 +135,14 @@ function setupButtons(): void {
|
||||||
// Refresh button
|
// Refresh button
|
||||||
statusBar.onRefreshClick(forceRefresh);
|
statusBar.onRefreshClick(forceRefresh);
|
||||||
|
|
||||||
|
// Share button — emits the same HTTPS deep link the Android app uses,
|
||||||
|
// so a recipient with the app installed (and verified App Links) opens
|
||||||
|
// the shelter natively, otherwise it opens in the PWA.
|
||||||
|
document.getElementById('share-btn')?.addEventListener('click', () => {
|
||||||
|
navigator.vibrate?.(10);
|
||||||
|
shareSelectedShelter();
|
||||||
|
});
|
||||||
|
|
||||||
// Cache retry button
|
// Cache retry button
|
||||||
const cacheRetryBtn = document.getElementById('cache-retry-btn')!;
|
const cacheRetryBtn = document.getElementById('cache-retry-btn')!;
|
||||||
cacheRetryBtn.textContent = t('action_cache_now');
|
cacheRetryBtn.textContent = t('action_cache_now');
|
||||||
|
|
@ -247,8 +258,40 @@ function updateNearestShelters(location: LatLon): void {
|
||||||
NEAREST_COUNT,
|
NEAREST_COUNT,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Only auto-select the nearest shelter if the user hasn't manually selected one
|
if (userSelectedShelter && selectedRomnr !== null) {
|
||||||
if (!userSelectedShelter) {
|
// Preserve the user's chosen shelter (deep link, marker click, list tap)
|
||||||
|
// even when it isn't in the geographic top-N. We re-add it with a
|
||||||
|
// freshly computed distance/bearing so the arrow stays correct.
|
||||||
|
// Match by romnr — survives a forceRefresh() that replaces lokalIds.
|
||||||
|
const inList = nearestShelters.findIndex(
|
||||||
|
(s) => s.shelter.romnr === selectedRomnr,
|
||||||
|
);
|
||||||
|
if (inList >= 0) {
|
||||||
|
selectedShelterIndex = inList;
|
||||||
|
} else {
|
||||||
|
const shelter = allShelters.find((s) => s.romnr === selectedRomnr);
|
||||||
|
if (shelter) {
|
||||||
|
nearestShelters.unshift({
|
||||||
|
shelter,
|
||||||
|
distanceMeters: distanceMeters(
|
||||||
|
location.latitude, location.longitude,
|
||||||
|
shelter.latitude, shelter.longitude,
|
||||||
|
),
|
||||||
|
bearingDegrees: bearingDegrees(
|
||||||
|
location.latitude, location.longitude,
|
||||||
|
shelter.latitude, shelter.longitude,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
selectedShelterIndex = 0;
|
||||||
|
} else {
|
||||||
|
// Selected shelter no longer exists in the dataset (e.g. DSB
|
||||||
|
// decommissioned it). Fall back to nearest.
|
||||||
|
selectedRomnr = null;
|
||||||
|
userSelectedShelter = false;
|
||||||
|
selectedShelterIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
selectedShelterIndex = 0;
|
selectedShelterIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -405,19 +448,22 @@ async function forceRefresh(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle /shelter/{lokalId} deep links.
|
* Handle /shelter/{romnr} deep links.
|
||||||
* Called after loadData() so allShelters is populated.
|
* Called after loadData() so allShelters is populated.
|
||||||
|
*
|
||||||
|
* Path component is romnr (DSB room number), not lokalId — see
|
||||||
|
* selectedRomnr comment above for the upstream-stability rationale.
|
||||||
*/
|
*/
|
||||||
function handleDeepLink(): void {
|
function handleDeepLink(): void {
|
||||||
const match = window.location.pathname.match(/^\/shelter\/(.+)$/);
|
const match = window.location.pathname.match(/^\/shelter\/(\d+)$/);
|
||||||
if (!match) return;
|
if (!match) return;
|
||||||
|
|
||||||
const lokalId = decodeURIComponent(match[1]);
|
const romnr = parseInt(match[1], 10);
|
||||||
|
|
||||||
// Clean the URL so refresh doesn't re-trigger
|
// Clean the URL so refresh doesn't re-trigger
|
||||||
window.history.replaceState({}, '', '/');
|
window.history.replaceState({}, '', '/');
|
||||||
|
|
||||||
const shelter = allShelters.find((s) => s.lokalId === lokalId);
|
const shelter = allShelters.find((s) => s.romnr === romnr);
|
||||||
if (!shelter) {
|
if (!shelter) {
|
||||||
statusBar.setStatus(t('error_shelter_not_found'));
|
statusBar.setStatus(t('error_shelter_not_found'));
|
||||||
return;
|
return;
|
||||||
|
|
@ -428,42 +474,95 @@ function handleDeepLink(): void {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select a specific shelter, even if it's not in the current nearest-3 list.
|
* Select a specific shelter, even if it's not in the current nearest-3 list.
|
||||||
* Used for deep link targets.
|
* Used for deep link targets, marker taps, and list taps. The selection is
|
||||||
|
* remembered via selectedLokalId so subsequent location updates preserve it.
|
||||||
*/
|
*/
|
||||||
function selectShelterByData(shelter: Shelter): void {
|
function selectShelterByData(shelter: Shelter): void {
|
||||||
// Check if it's already in nearestShelters
|
userSelectedShelter = true;
|
||||||
|
selectedRomnr = shelter.romnr;
|
||||||
|
|
||||||
const existingIdx = nearestShelters.findIndex(
|
const existingIdx = nearestShelters.findIndex(
|
||||||
(s) => s.shelter.lokalId === shelter.lokalId,
|
(s) => s.shelter.romnr === shelter.romnr,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingIdx >= 0) {
|
if (existingIdx >= 0) {
|
||||||
userSelectedShelter = true;
|
|
||||||
selectedShelterIndex = existingIdx;
|
selectedShelterIndex = existingIdx;
|
||||||
} else {
|
} else {
|
||||||
// Compute distance/bearing if we have a location, otherwise use placeholder
|
// Compute distance/bearing if we have a location, otherwise NaN signals
|
||||||
let dist = NaN;
|
// "unknown distance" — same convention as MainActivity.kt.
|
||||||
let bearing = 0;
|
const dist = currentLocation
|
||||||
if (currentLocation) {
|
? distanceMeters(
|
||||||
dist = distanceMeters(
|
|
||||||
currentLocation.latitude, currentLocation.longitude,
|
currentLocation.latitude, currentLocation.longitude,
|
||||||
shelter.latitude, shelter.longitude,
|
shelter.latitude, shelter.longitude,
|
||||||
);
|
)
|
||||||
bearing = bearingDegrees(
|
: NaN;
|
||||||
|
const bearing = currentLocation
|
||||||
|
? bearingDegrees(
|
||||||
currentLocation.latitude, currentLocation.longitude,
|
currentLocation.latitude, currentLocation.longitude,
|
||||||
shelter.latitude, shelter.longitude,
|
shelter.latitude, shelter.longitude,
|
||||||
);
|
)
|
||||||
}
|
: NaN;
|
||||||
|
|
||||||
// Prepend to the list so it becomes the selected shelter
|
|
||||||
nearestShelters.unshift({
|
nearestShelters.unshift({
|
||||||
shelter,
|
shelter,
|
||||||
distanceMeters: dist,
|
distanceMeters: dist,
|
||||||
bearingDegrees: bearing,
|
bearingDegrees: bearing,
|
||||||
});
|
});
|
||||||
userSelectedShelter = true;
|
|
||||||
selectedShelterIndex = 0;
|
selectedShelterIndex = 0;
|
||||||
shelterList.updateList(nearestShelters, selectedShelterIndex);
|
shelterList.updateList(nearestShelters, selectedShelterIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSelectedShelter(true);
|
updateSelectedShelter(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Share the currently selected shelter. Uses the Web Share API when
|
||||||
|
* available (mobile browsers, installed PWAs) and falls back to copying
|
||||||
|
* the same body to the clipboard. Body format mirrors share_body in the
|
||||||
|
* Android strings.xml so the two clients produce equivalent messages.
|
||||||
|
*/
|
||||||
|
async function shareSelectedShelter(): Promise<void> {
|
||||||
|
const selected = nearestShelters[selectedShelterIndex];
|
||||||
|
if (!selected || !selected.shelter) {
|
||||||
|
statusBar.setStatus(t('share_no_shelter'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shelter = selected.shelter;
|
||||||
|
const lat = shelter.latitude.toFixed(6);
|
||||||
|
const lon = shelter.longitude.toFixed(6);
|
||||||
|
// Path component is romnr (stable DSB business key), not lokalId. The
|
||||||
|
// upstream Geonorge GeoJSON re-rolls lokalId on every export, so a
|
||||||
|
// lokalId-keyed link breaks the moment either party refreshes their
|
||||||
|
// dataset. Romnr is unique (verified 556/556) and stable across exports.
|
||||||
|
const deepLink = `https://${DEEP_LINK_DOMAIN}/shelter/${shelter.romnr}`;
|
||||||
|
|
||||||
|
const subject = t('share_subject');
|
||||||
|
const body = [
|
||||||
|
`${subject}: ${shelter.adresse}`,
|
||||||
|
t('shelter_capacity', shelter.plasser),
|
||||||
|
`${lat}, ${lon}`,
|
||||||
|
`geo:${lat},${lon}`,
|
||||||
|
deepLink,
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
if (navigator.share) {
|
||||||
|
try {
|
||||||
|
await navigator.share({ title: subject, text: body, url: deepLink });
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
// AbortError means the user cancelled — silent. Anything else falls
|
||||||
|
// through to the clipboard fallback below.
|
||||||
|
if ((err as DOMException)?.name === 'AbortError') return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navigator.clipboard?.writeText) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(body);
|
||||||
|
statusBar.setStatus(t('share_copied'));
|
||||||
|
} catch {
|
||||||
|
statusBar.setStatus(t('share_no_shelter'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,10 @@ export const en: Record<string, string> = {
|
||||||
action_skip: 'Skip',
|
action_skip: 'Skip',
|
||||||
action_cache_ok: 'Cache map',
|
action_cache_ok: 'Cache map',
|
||||||
action_cache_now: 'Cache now',
|
action_cache_now: 'Cache now',
|
||||||
|
action_share: 'Share shelter',
|
||||||
|
share_subject: 'Emergency shelter',
|
||||||
|
share_no_shelter: 'No shelter selected',
|
||||||
|
share_copied: 'Shelter details copied to clipboard',
|
||||||
warning_no_map_cache: 'No offline map cached. Map requires internet.',
|
warning_no_map_cache: 'No offline map cached. Map requires internet.',
|
||||||
|
|
||||||
// Permissions
|
// Permissions
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,11 @@ export function initLocale(): void {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Guard the DOM write so this module is usable from Node (vitest runs
|
||||||
|
// without jsdom). In a browser, document is always defined.
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
document.documentElement.lang = currentLocale;
|
document.documentElement.lang = currentLocale;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get current locale code. */
|
/** Get current locale code. */
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ export const nb: Record<string, string> = {
|
||||||
action_skip: 'Hopp over',
|
action_skip: 'Hopp over',
|
||||||
action_cache_ok: 'Lagre kart',
|
action_cache_ok: 'Lagre kart',
|
||||||
action_cache_now: 'Lagre nå',
|
action_cache_now: 'Lagre nå',
|
||||||
|
action_share: 'Del tilfluktsrom',
|
||||||
|
share_subject: 'Tilfluktsrom',
|
||||||
|
share_no_shelter: 'Ingen tilfluktsrom valgt',
|
||||||
|
share_copied: 'Tilfluktsrominfo kopiert til utklippstavlen',
|
||||||
warning_no_map_cache:
|
warning_no_map_cache:
|
||||||
'Ingen frakoblet kart lagret. Kartet krever internett.',
|
'Ingen frakoblet kart lagret. Kartet krever internett.',
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ export const nn: Record<string, string> = {
|
||||||
action_skip: 'Hopp over',
|
action_skip: 'Hopp over',
|
||||||
action_cache_ok: 'Lagre kart',
|
action_cache_ok: 'Lagre kart',
|
||||||
action_cache_now: 'Lagre no',
|
action_cache_now: 'Lagre no',
|
||||||
|
action_share: 'Del tilfluktsrom',
|
||||||
|
share_subject: 'Tilfluktsrom',
|
||||||
|
share_no_shelter: 'Ingen tilfluktsrom valt',
|
||||||
|
share_copied: 'Tilfluktsrominfo kopiert til utklippstavla',
|
||||||
warning_no_map_cache:
|
warning_no_map_cache:
|
||||||
'Ingen fråkopla kart lagra. Kartet krev internett.',
|
'Ingen fråkopla kart lagra. Kartet krev internett.',
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,44 @@ import './styles/main.css';
|
||||||
import 'leaflet/dist/leaflet.css';
|
import 'leaflet/dist/leaflet.css';
|
||||||
import { initLocale } from './i18n/i18n';
|
import { initLocale } from './i18n/i18n';
|
||||||
import { init } from './app';
|
import { init } from './app';
|
||||||
import { setStatus } from './ui/status-bar';
|
|
||||||
import { t } from './i18n/i18n';
|
|
||||||
import { maybeShow as maybeShowIosInstallHint } from './ui/install-hint';
|
import { maybeShow as maybeShowIosInstallHint } from './ui/install-hint';
|
||||||
|
|
||||||
console.info(`[tilfluktsrom] build ${__BUILD_REVISION__}`);
|
console.info(`[tilfluktsrom] build ${__BUILD_REVISION__}`);
|
||||||
|
|
||||||
|
// Make `registerType: 'autoUpdate'` actually auto-update the running tab.
|
||||||
|
// vite-plugin-pwa's autoUpdate strategy makes the new service worker
|
||||||
|
// skipWaiting + clientsClaim, but the JS already loaded in the open tab is
|
||||||
|
// the *old* build until something triggers a navigation. Without this
|
||||||
|
// listener, a deploy is invisible until the user manually refreshes.
|
||||||
|
//
|
||||||
|
// We *defer* the reload until the user next backgrounds the app
|
||||||
|
// (visibilityState === 'hidden') instead of reloading immediately. This is
|
||||||
|
// an emergency app: a mid-task reload would lose the selected shelter,
|
||||||
|
// compass mode, and any in-flight UI state right when the user can least
|
||||||
|
// afford to be surprised. Deferring keeps the "auto" promise (they're on
|
||||||
|
// the new version next time they look at the screen) without interrupting
|
||||||
|
// active use.
|
||||||
|
//
|
||||||
|
// The `wasAlreadyControlled` guard avoids reloading on the very first SW
|
||||||
|
// install (when there was no previous controller — that's a fresh visit,
|
||||||
|
// not an update).
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
const wasAlreadyControlled = !!navigator.serviceWorker.controller;
|
||||||
|
let pendingReload = false;
|
||||||
|
|
||||||
|
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||||
|
if (!wasAlreadyControlled) return;
|
||||||
|
pendingReload = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (pendingReload && document.visibilityState === 'hidden') {
|
||||||
|
pendingReload = false;
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
initLocale();
|
initLocale();
|
||||||
|
|
||||||
|
|
@ -25,14 +57,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
await navigator.storage.persist();
|
await navigator.storage.persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for service worker updates — flash a status message when a new
|
|
||||||
// version activates so the user knows they have fresh code/data.
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
|
||||||
setStatus(t('update_success'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await init();
|
await init();
|
||||||
|
|
||||||
// Shown only on first iOS Safari visit, once per device. Placed after init()
|
// Shown only on first iOS Safari visit, once per device. Placed after init()
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ html, body {
|
||||||
}
|
}
|
||||||
|
|
||||||
#about-btn,
|
#about-btn,
|
||||||
|
#share-btn,
|
||||||
#refresh-btn {
|
#refresh-btn {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
@ -75,10 +76,23 @@ html, body {
|
||||||
}
|
}
|
||||||
|
|
||||||
#about-btn:hover,
|
#about-btn:hover,
|
||||||
|
#share-btn:hover,
|
||||||
#refresh-btn:hover {
|
#refresh-btn:hover {
|
||||||
color: #ECEFF1;
|
color: #ECEFF1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#share-btn[disabled] {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#share-btn svg {
|
||||||
|
display: block;
|
||||||
|
/* `↻` and `ℹ` glyphs sit on a baseline; an SVG inside an inline-flex
|
||||||
|
container can otherwise pick up extra descender space and look
|
||||||
|
misaligned next to them. `display:block` removes that. */
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Main content area (map or compass) --- */
|
/* --- Main content area (map or compass) --- */
|
||||||
#main-content {
|
#main-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
|
||||||
2231
scripts/romnr-baseline.json
Normal file
2231
scripts/romnr-baseline.json
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue