mirror of
https://github.com/avinal/nikki.git
synced 2026-07-03 21:40:09 +05:30
Add loading indicator, error display, archived memos API
Loading state: - CircularProgressIndicator shown during first fetch instead of "no memos yet". isInitialLoading flag in MemoListUiState cleared after first emission from observeMemos(). Error display: - Failed refresh/load errors shown as red text in memo list - Previously errors were captured but never displayed to user Archived memos: - listArchivedMemos() API endpoint added (filter: state == ARCHIVED) - "view archived memos" link in explorer page (placeholder for full UI) 102 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
This commit is contained in:
@@ -138,6 +138,13 @@ class MemosApiClient(
|
||||
}.body()
|
||||
}
|
||||
|
||||
suspend fun listArchivedMemos(): ApiResult<ListMemosResponse> = apiCall {
|
||||
httpClient.get(url("/memos")) {
|
||||
parameter("pageSize", 50)
|
||||
parameter("filter", "state == \"ARCHIVED\"")
|
||||
}.body()
|
||||
}
|
||||
|
||||
suspend fun deleteMemo(id: String): ApiResult<Unit> = apiCall {
|
||||
val response = httpClient.delete(url("/memos/$id"))
|
||||
if (!response.status.isSuccess()) {
|
||||
|
||||
@@ -247,6 +247,14 @@ private fun ExplorerPage(
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text(
|
||||
"view archived memos",
|
||||
fontSize = 14.sp,
|
||||
color = accent,
|
||||
modifier = Modifier.clickable { /* navigate to archived */ }.padding(vertical = 4.dp),
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
|
||||
@@ -349,7 +349,13 @@ fun MemoListScreen(
|
||||
)
|
||||
}
|
||||
|
||||
if (memos.isEmpty() && !uiState.isRefreshing) {
|
||||
if (uiState.isInitialLoading && memos.isEmpty()) {
|
||||
item {
|
||||
Box(Modifier.fillMaxWidth().padding(top = 48.dp), contentAlignment = Alignment.Center) {
|
||||
androidx.compose.material3.CircularProgressIndicator(color = accent, strokeWidth = 2.dp)
|
||||
}
|
||||
}
|
||||
} else if (memos.isEmpty() && !uiState.isRefreshing) {
|
||||
item {
|
||||
Box(Modifier.fillMaxWidth().padding(top = 48.dp), contentAlignment = Alignment.Center) {
|
||||
Text("no memos yet", fontSize = 15.sp, color = subtleColor)
|
||||
@@ -357,6 +363,15 @@ fun MemoListScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.error != null) {
|
||||
item {
|
||||
Text(
|
||||
uiState.error!!, fontSize = 12.sp, color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(start = 24.dp, end = 12.dp, top = 4.dp, bottom = 4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(memos, key = { it.id }) { memo ->
|
||||
MemoCard(
|
||||
memo = memo,
|
||||
|
||||
@@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -22,6 +23,7 @@ data class MemoListUiState(
|
||||
val searchQuery: String = "",
|
||||
val isSearching: Boolean = false,
|
||||
val error: String? = null,
|
||||
val isInitialLoading: Boolean = true,
|
||||
)
|
||||
|
||||
class MemoListViewModel(private val memoRepository: MemoRepository) : ViewModel() {
|
||||
@@ -32,6 +34,13 @@ class MemoListViewModel(private val memoRepository: MemoRepository) : ViewModel(
|
||||
private val _searchQuery = MutableStateFlow("")
|
||||
private var searchJob: Job? = null
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
memoRepository.observeMemos().first()
|
||||
_uiState.update { it.copy(isInitialLoading = false) }
|
||||
}
|
||||
}
|
||||
|
||||
val memos: StateFlow<List<Memo>> = combine(
|
||||
memoRepository.observeMemos(),
|
||||
_searchQuery,
|
||||
|
||||
Reference in New Issue
Block a user