Compare commits
No commits in common. "029cfa45f97263544aa3954f51e01dc6b0149892" and "ae249d5d47487a92cb0409b747a3df25c7c96a54" have entirely different histories.
029cfa45f9
...
ae249d5d47
15 changed files with 18 additions and 117 deletions
|
|
@ -215,16 +215,9 @@ Both flavors produce identical user experiences — `standard` achieves faster G
|
||||||
|
|
||||||
### Deep Linking
|
### Deep Linking
|
||||||
|
|
||||||
**HTTPS App Links:** `https://tilfluktsrom.naiv.no/shelter/{lokalId}`
|
URI scheme: `tilfluktsrom://shelter/{lokalId}`
|
||||||
|
|
||||||
The domain is configured in one place: `DEEP_LINK_DOMAIN` in `build.gradle.kts` (exposed as `BuildConfig.DEEP_LINK_DOMAIN` and manifest placeholder `${deepLinkHost}`).
|
Used in share messages so recipients can open the app directly to a specific shelter.
|
||||||
|
|
||||||
- `autoVerify="true"` on the HTTPS intent filter triggers Android's App Links verification at install time
|
|
||||||
- Verification requires `/.well-known/assetlinks.json` to be served by the PWA (in `pwa/public/.well-known/`)
|
|
||||||
- If the app is installed and verified, `/shelter/*` links open the app directly (no disambiguation dialog)
|
|
||||||
- If not installed, the link opens in the browser, where the PWA handles it
|
|
||||||
|
|
||||||
Share messages include the HTTPS URL, which SMS apps auto-link as a tappable URL.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,8 @@ android {
|
||||||
applicationId = "no.naiv.tilfluktsrom"
|
applicationId = "no.naiv.tilfluktsrom"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 12
|
versionCode = 11
|
||||||
versionName = "1.8.0"
|
versionName = "1.7.0"
|
||||||
|
|
||||||
// Deep link domain — single source of truth for manifest + Kotlin code
|
|
||||||
val deepLinkDomain = "tilfluktsrom.naiv.no"
|
|
||||||
buildConfigField("String", "DEEP_LINK_DOMAIN", "\"$deepLinkDomain\"")
|
|
||||||
manifestPlaceholders["deepLinkHost"] = deepLinkDomain
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependenciesInfo {
|
dependenciesInfo {
|
||||||
|
|
|
||||||
|
|
@ -29,14 +29,13 @@
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter android:autoVerify="true">
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data
|
<data
|
||||||
android:scheme="https"
|
android:scheme="tilfluktsrom"
|
||||||
android:host="${deepLinkHost}"
|
android:host="shelter" />
|
||||||
android:pathPrefix="/shelter/" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -143,14 +143,12 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle https://{domain}/shelter/{lokalId} deep link.
|
* Handle tilfluktsrom://shelter/{lokalId} deep link.
|
||||||
* 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?) {
|
||||||
val uri = intent?.data ?: return
|
val uri = intent?.data ?: return
|
||||||
if (uri.scheme != "https" ||
|
if (uri.scheme != "tilfluktsrom" || uri.host != "shelter") return
|
||||||
uri.host != BuildConfig.DEEP_LINK_DOMAIN ||
|
|
||||||
uri.path?.startsWith("/shelter/") != true) return
|
|
||||||
|
|
||||||
val lokalId = uri.lastPathSegment ?: return
|
val lokalId = uri.lastPathSegment ?: return
|
||||||
// Clear intent data so config changes don't re-trigger
|
// Clear intent data so config changes don't re-trigger
|
||||||
|
|
@ -671,8 +669,8 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Share the currently selected shelter via ACTION_SEND.
|
* Share the currently selected shelter via ACTION_SEND.
|
||||||
* Includes address, capacity, geo: URI, and an HTTPS deep link
|
* Includes address, capacity, geo: URI (for non-app recipients),
|
||||||
* that opens the app (if installed) or the PWA (in browser).
|
* and a tilfluktsrom:// deep link (for app users).
|
||||||
*/
|
*/
|
||||||
private fun shareShelter() {
|
private fun shareShelter() {
|
||||||
val selected = selectedShelter
|
val selected = selectedShelter
|
||||||
|
|
@ -682,14 +680,12 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
val shelter = selected.shelter
|
val shelter = selected.shelter
|
||||||
val deepLink = "https://${BuildConfig.DEEP_LINK_DOMAIN}/shelter/${shelter.lokalId}"
|
|
||||||
val body = getString(
|
val body = getString(
|
||||||
R.string.share_body,
|
R.string.share_body,
|
||||||
shelter.adresse,
|
shelter.adresse,
|
||||||
shelter.plasser,
|
shelter.plasser,
|
||||||
shelter.latitude,
|
shelter.latitude,
|
||||||
shelter.longitude,
|
shelter.longitude
|
||||||
deepLink
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
|
|
||||||
|
|
@ -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.8.0")
|
.header("User-Agent", "Tilfluktsrom/1.7.0")
|
||||||
.build())
|
.build())
|
||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@
|
||||||
|
|
||||||
<!-- Deling -->
|
<!-- Deling -->
|
||||||
<string name="share_subject">Tilfluktsrom</string>
|
<string name="share_subject">Tilfluktsrom</string>
|
||||||
<string name="share_body">Tilfluktsrom: %1$s\n%2$d plasser\n%3$.6f, %4$.6f\ngeo:%3$.6f,%4$.6f\n%5$s</string>
|
<string name="share_body">Tilfluktsrom: %1$s\n%2$d plasser\n%3$.6f, %4$.6f\ngeo:%3$.6f,%4$.6f</string>
|
||||||
<string name="share_no_shelter">Ingen tilfluktsrom valgt</string>
|
<string name="share_no_shelter">Ingen tilfluktsrom valgt</string>
|
||||||
|
|
||||||
<!-- Tilgjengelighet -->
|
<!-- Tilgjengelighet -->
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@
|
||||||
|
|
||||||
<!-- Deling -->
|
<!-- Deling -->
|
||||||
<string name="share_subject">Tilfluktsrom</string>
|
<string name="share_subject">Tilfluktsrom</string>
|
||||||
<string name="share_body">Tilfluktsrom: %1$s\n%2$d plassar\n%3$.6f, %4$.6f\ngeo:%3$.6f,%4$.6f\n%5$s</string>
|
<string name="share_body">Tilfluktsrom: %1$s\n%2$d plassar\n%3$.6f, %4$.6f\ngeo:%3$.6f,%4$.6f</string>
|
||||||
<string name="share_no_shelter">Ingen tilfluktsrom valt</string>
|
<string name="share_no_shelter">Ingen tilfluktsrom valt</string>
|
||||||
|
|
||||||
<!-- Tilgjenge -->
|
<!-- Tilgjenge -->
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@
|
||||||
|
|
||||||
<!-- Sharing -->
|
<!-- Sharing -->
|
||||||
<string name="share_subject">Emergency shelter</string>
|
<string name="share_subject">Emergency shelter</string>
|
||||||
<string name="share_body">Shelter: %1$s\n%2$d places\n%3$.6f, %4$.6f\ngeo:%3$.6f,%4$.6f\n%5$s</string>
|
<string name="share_body">Shelter: %1$s\n%2$d places\n%3$.6f, %4$.6f\ngeo:%3$.6f,%4$.6f</string>
|
||||||
<string name="share_no_shelter">No shelter selected</string>
|
<string name="share_no_shelter">No shelter selected</string>
|
||||||
|
|
||||||
<!-- Accessibility -->
|
<!-- Accessibility -->
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
|
||||||
"target": {
|
|
||||||
"namespace": "android_app",
|
|
||||||
"package_name": "no.naiv.tilfluktsrom",
|
|
||||||
"sha256_cert_fingerprints": [
|
|
||||||
"43:05:79:6F:EA:3E:F4:50:45:D3:8A:EF:EA:58:B6:65:49:D2:D2:C3:4B:4C:61:11:EE:74:48:B0:C7:70:E4:5B"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
import type { Shelter, ShelterWithDistance, LatLon } from './types';
|
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 } from './util/distance-utils';
|
||||||
import { findNearest } from './location/shelter-finder';
|
import { findNearest } from './location/shelter-finder';
|
||||||
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';
|
||||||
|
|
@ -43,7 +43,6 @@ export async function init(): Promise<void> {
|
||||||
setupShelterList();
|
setupShelterList();
|
||||||
setupButtons();
|
setupButtons();
|
||||||
await loadData();
|
await loadData();
|
||||||
handleDeepLink();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Set localized aria-labels and wire the about button. */
|
/** Set localized aria-labels and wire the about button. */
|
||||||
|
|
@ -399,67 +398,3 @@ async function forceRefresh(): Promise<void> {
|
||||||
statusBar.setStatus(t('update_failed'));
|
statusBar.setStatus(t('update_failed'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle /shelter/{lokalId} deep links.
|
|
||||||
* Called after loadData() so allShelters is populated.
|
|
||||||
*/
|
|
||||||
function handleDeepLink(): void {
|
|
||||||
const match = window.location.pathname.match(/^\/shelter\/(.+)$/);
|
|
||||||
if (!match) return;
|
|
||||||
|
|
||||||
const lokalId = decodeURIComponent(match[1]);
|
|
||||||
|
|
||||||
// Clean the URL so refresh doesn't re-trigger
|
|
||||||
window.history.replaceState({}, '', '/');
|
|
||||||
|
|
||||||
const shelter = allShelters.find((s) => s.lokalId === lokalId);
|
|
||||||
if (!shelter) {
|
|
||||||
statusBar.setStatus(t('error_shelter_not_found'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectShelterByData(shelter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select a specific shelter, even if it's not in the current nearest-3 list.
|
|
||||||
* Used for deep link targets.
|
|
||||||
*/
|
|
||||||
function selectShelterByData(shelter: Shelter): void {
|
|
||||||
// Check if it's already in nearestShelters
|
|
||||||
const existingIdx = nearestShelters.findIndex(
|
|
||||||
(s) => s.shelter.lokalId === shelter.lokalId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingIdx >= 0) {
|
|
||||||
userSelectedShelter = true;
|
|
||||||
selectedShelterIndex = existingIdx;
|
|
||||||
} else {
|
|
||||||
// Compute distance/bearing if we have a location, otherwise use placeholder
|
|
||||||
let dist = NaN;
|
|
||||||
let bearing = 0;
|
|
||||||
if (currentLocation) {
|
|
||||||
dist = distanceMeters(
|
|
||||||
currentLocation.latitude, currentLocation.longitude,
|
|
||||||
shelter.latitude, shelter.longitude,
|
|
||||||
);
|
|
||||||
bearing = bearingDegrees(
|
|
||||||
currentLocation.latitude, currentLocation.longitude,
|
|
||||||
shelter.latitude, shelter.longitude,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepend to the list so it becomes the selected shelter
|
|
||||||
nearestShelters.unshift({
|
|
||||||
shelter,
|
|
||||||
distanceMeters: dist,
|
|
||||||
bearingDegrees: bearing,
|
|
||||||
});
|
|
||||||
userSelectedShelter = true;
|
|
||||||
selectedShelterIndex = 0;
|
|
||||||
shelterList.updateList(nearestShelters, selectedShelterIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSelectedShelter(true);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
/** Deep link domain — single source of truth for the PWA. */
|
|
||||||
export const DEEP_LINK_DOMAIN = 'tilfluktsrom.naiv.no';
|
|
||||||
|
|
@ -46,7 +46,6 @@ export const en: Record<string, string> = {
|
||||||
'No cached data available. Connect to the internet to download shelter data.',
|
'No cached data available. Connect to the internet to download shelter data.',
|
||||||
update_success: 'Shelter data updated',
|
update_success: 'Shelter data updated',
|
||||||
update_failed: 'Update failed \u2014 using cached data',
|
update_failed: 'Update failed \u2014 using cached data',
|
||||||
error_shelter_not_found: 'Shelter not found',
|
|
||||||
|
|
||||||
// Accessibility
|
// Accessibility
|
||||||
direction_arrow_description: 'Direction to shelter, %s away',
|
direction_arrow_description: 'Direction to shelter, %s away',
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,6 @@ export const nb: Record<string, string> = {
|
||||||
'Ingen lagrede data tilgjengelig. Koble til internett for å laste ned tilfluktsromdata.',
|
'Ingen lagrede data tilgjengelig. Koble til internett for å laste ned tilfluktsromdata.',
|
||||||
update_success: 'Tilfluktsromdata oppdatert',
|
update_success: 'Tilfluktsromdata oppdatert',
|
||||||
update_failed: 'Oppdatering mislyktes — bruker lagrede data',
|
update_failed: 'Oppdatering mislyktes — bruker lagrede data',
|
||||||
error_shelter_not_found: 'Fant ikke tilfluktsrommet',
|
|
||||||
|
|
||||||
// Tilgjengelighet
|
// Tilgjengelighet
|
||||||
direction_arrow_description: 'Retning til tilfluktsrom, %s unna',
|
direction_arrow_description: 'Retning til tilfluktsrom, %s unna',
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,6 @@ export const nn: Record<string, string> = {
|
||||||
'Ingen lagra data tilgjengeleg. Kopla til internett for å laste ned tilfluktsromdata.',
|
'Ingen lagra data tilgjengeleg. Kopla til internett for å laste ned tilfluktsromdata.',
|
||||||
update_success: 'Tilfluktsromdata oppdatert',
|
update_success: 'Tilfluktsromdata oppdatert',
|
||||||
update_failed: 'Oppdatering mislukkast — brukar lagra data',
|
update_failed: 'Oppdatering mislukkast — brukar lagra data',
|
||||||
error_shelter_not_found: 'Fann ikkje tilfluktsrommet',
|
|
||||||
|
|
||||||
// Tilgjenge
|
// Tilgjenge
|
||||||
direction_arrow_description: 'Retning til tilfluktsrom, %s unna',
|
direction_arrow_description: 'Retning til tilfluktsrom, %s unna',
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { defineConfig } from 'vite';
|
||||||
import { VitePWA } from 'vite-plugin-pwa';
|
import { VitePWA } from 'vite-plugin-pwa';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: '/',
|
base: './',
|
||||||
define: {
|
define: {
|
||||||
// Injected as a global — changes every build, breaking any stale cache
|
// Injected as a global — changes every build, breaking any stale cache
|
||||||
__BUILD_REVISION__: JSON.stringify(
|
__BUILD_REVISION__: JSON.stringify(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue