Fix infinite spinner on guide screen and allow cleartext HTTP in debug
Two bugs combined to cause this: (1) Android blocks plain-HTTP traffic by default since API 28, so every call to our HTTP-only local backend was throwing and being silently swallowed; (2) GuideViewModel's loadNearby() returned early on that exception without ever setting isLoading = false, so the spinner never cleared and no error showed. Fix: debug-only manifest override (src/debug/AndroidManifest.xml) enables cleartext traffic for local dev against the Docker backend; GuideViewModel now always resolves isLoading and surfaces a errorMessage + retry() the UI can act on instead of hanging forever. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
3f5a857c5a
commit
188bf01d6e
5 changed files with 60 additions and 12 deletions
10
android/app/src/debug/AndroidManifest.xml
Normal file
10
android/app/src/debug/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!--
|
||||||
|
Debug builds only: allow plain-HTTP traffic so the app can talk to the
|
||||||
|
local Docker backend (http://<lan-ip>:8000) during development.
|
||||||
|
Android blocks cleartext traffic by default since API 28 — release
|
||||||
|
builds intentionally do NOT get this override.
|
||||||
|
-->
|
||||||
|
<application android:usesCleartextTraffic="true" />
|
||||||
|
</manifest>
|
||||||
|
|
@ -1,13 +1,18 @@
|
||||||
package com.guidecity.app.ui.guide
|
package com.guidecity.app.ui.guide
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Favorite
|
import androidx.compose.material.icons.filled.Favorite
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material.icons.filled.VolumeOff
|
import androidx.compose.material.icons.filled.VolumeOff
|
||||||
import androidx.compose.material.icons.filled.VolumeUp
|
import androidx.compose.material.icons.filled.VolumeUp
|
||||||
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
|
@ -20,6 +25,7 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import com.guidecity.app.R
|
import com.guidecity.app.R
|
||||||
|
|
||||||
|
|
@ -61,6 +67,22 @@ fun GuideScreen(
|
||||||
.padding(padding),
|
.padding(padding),
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
|
state.errorMessage != null -> {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(24.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Text(text = state.errorMessage.orEmpty())
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Button(onClick = { viewModel.retry() }) {
|
||||||
|
Text(text = stringResource(R.string.guide_retry))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
state.isLoading -> {
|
state.isLoading -> {
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ data class GuideUiState(
|
||||||
val activeIndex: Int = 0,
|
val activeIndex: Int = 0,
|
||||||
val doneIndices: Set<Int> = emptySet(),
|
val doneIndices: Set<Int> = emptySet(),
|
||||||
val isMuted: Boolean = false,
|
val isMuted: Boolean = false,
|
||||||
|
val errorMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -73,11 +74,11 @@ class GuideViewModel @Inject constructor(
|
||||||
private suspend fun loadNearby(startNarration: Boolean) {
|
private suspend fun loadNearby(startNarration: Boolean) {
|
||||||
val location = selectedLocationHolder.location.value ?: locationProvider.getCurrentLocation()
|
val location = selectedLocationHolder.location.value ?: locationProvider.getCurrentLocation()
|
||||||
if (location == null) {
|
if (location == null) {
|
||||||
_uiState.value = _uiState.value.copy(isLoading = false)
|
_uiState.value = _uiState.value.copy(isLoading = false, errorMessage = "Location unavailable")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = runCatching {
|
runCatching {
|
||||||
placesRepository.getNearby(
|
placesRepository.getNearby(
|
||||||
lat = location.lat,
|
lat = location.lat,
|
||||||
lon = location.lon,
|
lon = location.lon,
|
||||||
|
|
@ -85,17 +86,30 @@ class GuideViewModel @Inject constructor(
|
||||||
lang = "ru",
|
lang = "ru",
|
||||||
length = contentLength.toApiValue(),
|
length = contentLength.toApiValue(),
|
||||||
)
|
)
|
||||||
}.getOrNull() ?: return
|
}.onSuccess { response ->
|
||||||
|
_uiState.value = _uiState.value.copy(
|
||||||
|
isLoading = false,
|
||||||
|
places = response.places,
|
||||||
|
activeIndex = 0,
|
||||||
|
doneIndices = emptySet(),
|
||||||
|
errorMessage = null,
|
||||||
|
)
|
||||||
|
if (startNarration) {
|
||||||
|
narrateActive()
|
||||||
|
}
|
||||||
|
}.onFailure { error ->
|
||||||
|
_uiState.value = _uiState.value.copy(
|
||||||
|
isLoading = false,
|
||||||
|
errorMessage = error.message ?: "Network error",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_uiState.value = _uiState.value.copy(
|
/** Retries after a failed load (e.g. tap a "retry" button shown on error). */
|
||||||
isLoading = false,
|
fun retry() {
|
||||||
places = response.places,
|
viewModelScope.launch {
|
||||||
activeIndex = 0,
|
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
|
||||||
doneIndices = emptySet(),
|
loadNearby(startNarration = true)
|
||||||
)
|
|
||||||
|
|
||||||
if (startNarration) {
|
|
||||||
narrateActive()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
<string name="map_search_placeholder">Введите место для поиска…</string>
|
<string name="map_search_placeholder">Введите место для поиска…</string>
|
||||||
|
|
||||||
<string name="guide_no_places_nearby">Поблизости пока не найдено интересных мест.</string>
|
<string name="guide_no_places_nearby">Поблизости пока не найдено интересных мест.</string>
|
||||||
|
<string name="guide_retry">Повторить</string>
|
||||||
<string name="guide_favorite_add">Добавить в избранное</string>
|
<string name="guide_favorite_add">Добавить в избранное</string>
|
||||||
<string name="guide_favorite_remove">Убрать из избранного</string>
|
<string name="guide_favorite_remove">Убрать из избранного</string>
|
||||||
<string name="guide_mute">Выключить озвучку</string>
|
<string name="guide_mute">Выключить озвучку</string>
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
<string name="map_search_placeholder">Search for a place instead…</string>
|
<string name="map_search_placeholder">Search for a place instead…</string>
|
||||||
|
|
||||||
<string name="guide_no_places_nearby">No places found nearby yet.</string>
|
<string name="guide_no_places_nearby">No places found nearby yet.</string>
|
||||||
|
<string name="guide_retry">Retry</string>
|
||||||
<string name="guide_favorite_add">Add to favorites</string>
|
<string name="guide_favorite_add">Add to favorites</string>
|
||||||
<string name="guide_favorite_remove">Remove from favorites</string>
|
<string name="guide_favorite_remove">Remove from favorites</string>
|
||||||
<string name="guide_mute">Mute narration</string>
|
<string name="guide_mute">Mute narration</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue