feat(android): pull-to-refresh — library tabs + admin + hidden

Final wave of audit #8. Library tabs (Artists/Albums via LibraryVM
refreshing through SyncController; Liked via LikesRepository;
History/Hidden via their own VMs) and all four admin screens
(Landing/Requests/Quarantine/Users) now support swipe-down refresh.

Per-VM change is uniform: refresh() returns Job so the
PullToRefreshScaffold wrapper can await it before hiding the
indicator.

Audit #8 user-visible parity now complete across all screens that
benefit. Search/Queue/NowPlaying/Settings intentionally excluded —
Search is query-driven, Queue is local state, NowPlaying/Settings
are forms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 18:25:50 -04:00
parent 4ca10e2afa
commit cf07a2a5a8
13 changed files with 158 additions and 124 deletions
@@ -45,7 +45,9 @@ import com.fabledsword.minstrel.nav.AdminRequests
import com.fabledsword.minstrel.nav.AdminUsers import com.fabledsword.minstrel.nav.AdminUsers
import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions import com.fabledsword.minstrel.shared.widgets.MainAppBarActions
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -80,24 +82,22 @@ class AdminLandingViewModel @Inject constructor(
refresh() refresh()
} }
fun refresh() { fun refresh(): Job = viewModelScope.launch {
viewModelScope.launch { internal.value = AdminLandingUiState.Loading
internal.value = AdminLandingUiState.Loading try {
try { val counts = coroutineScope {
val counts = coroutineScope { val req = async { requestsRepo.list().size }
val req = async { requestsRepo.list().size } val qua = async { quarantineRepo.list().size }
val qua = async { quarantineRepo.list().size } val usr = async { usersRepo.list().size }
val usr = async { usersRepo.list().size } AdminCounts(req.await(), qua.await(), usr.await())
AdminCounts(req.await(), qua.await(), usr.await())
}
internal.value = AdminLandingUiState.Success(counts)
} catch (
@Suppress("TooGenericExceptionCaught") e: Throwable,
) {
internal.value = AdminLandingUiState.Error(
e.message ?: "Admin counts load failed",
)
} }
internal.value = AdminLandingUiState.Success(counts)
} catch (
@Suppress("TooGenericExceptionCaught") e: Throwable,
) {
internal.value = AdminLandingUiState.Error(
e.message ?: "Admin counts load failed",
)
} }
} }
} }
@@ -130,7 +130,10 @@ fun AdminLandingScreen(
) )
}, },
) { inner -> ) { inner ->
Box(modifier = Modifier.fillMaxSize().padding(inner)) { PullToRefreshScaffold(
onRefresh = { viewModel.refresh().join() },
modifier = Modifier.fillMaxSize().padding(inner),
) {
when (val s = state) { when (val s = state) {
AdminLandingUiState.Loading -> LoadingCentered() AdminLandingUiState.Loading -> LoadingCentered()
is AdminLandingUiState.Error -> EmptyState( is AdminLandingUiState.Error -> EmptyState(
@@ -37,6 +37,7 @@ import com.fabledsword.minstrel.models.AdminQuarantineItemRef
import com.fabledsword.minstrel.nav.AdminQuarantine import com.fabledsword.minstrel.nav.AdminQuarantine
import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions import com.fabledsword.minstrel.shared.widgets.MainAppBarActions
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -64,7 +65,10 @@ fun AdminQuarantineScreen(
) )
}, },
) { inner -> ) { inner ->
Box(modifier = Modifier.fillMaxSize().padding(inner)) { PullToRefreshScaffold(
onRefresh = { viewModel.refresh().join() },
modifier = Modifier.fillMaxSize().padding(inner),
) {
when (val s = state) { when (val s = state) {
AdminQuarantineUiState.Loading -> LoadingCentered() AdminQuarantineUiState.Loading -> LoadingCentered()
AdminQuarantineUiState.Empty -> EmptyState( AdminQuarantineUiState.Empty -> EmptyState(
@@ -6,6 +6,7 @@ import com.fabledsword.minstrel.admin.data.AdminQuarantineRepository
import com.fabledsword.minstrel.events.EventsStream import com.fabledsword.minstrel.events.EventsStream
import com.fabledsword.minstrel.models.AdminQuarantineItemRef import com.fabledsword.minstrel.models.AdminQuarantineItemRef
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -38,23 +39,21 @@ class AdminQuarantineViewModel @Inject constructor(
} }
} }
fun refresh() { fun refresh(): Job = viewModelScope.launch {
viewModelScope.launch { internal.value = AdminQuarantineUiState.Loading
internal.value = AdminQuarantineUiState.Loading try {
try { val rows = repository.list()
val rows = repository.list() internal.value = if (rows.isEmpty()) {
internal.value = if (rows.isEmpty()) { AdminQuarantineUiState.Empty
AdminQuarantineUiState.Empty } else {
} else { AdminQuarantineUiState.Success(rows)
AdminQuarantineUiState.Success(rows)
}
} catch (
@Suppress("TooGenericExceptionCaught") e: Throwable,
) {
internal.value = AdminQuarantineUiState.Error(
e.message ?: "Admin quarantine load failed",
)
} }
} catch (
@Suppress("TooGenericExceptionCaught") e: Throwable,
) {
internal.value = AdminQuarantineUiState.Error(
e.message ?: "Admin quarantine load failed",
)
} }
} }
@@ -36,6 +36,7 @@ import com.fabledsword.minstrel.models.RequestRef
import com.fabledsword.minstrel.nav.AdminRequests import com.fabledsword.minstrel.nav.AdminRequests
import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions import com.fabledsword.minstrel.shared.widgets.MainAppBarActions
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -63,7 +64,10 @@ fun AdminRequestsScreen(
) )
}, },
) { inner -> ) { inner ->
Box(modifier = Modifier.fillMaxSize().padding(inner)) { PullToRefreshScaffold(
onRefresh = { viewModel.refresh().join() },
modifier = Modifier.fillMaxSize().padding(inner),
) {
when (val s = state) { when (val s = state) {
AdminRequestsUiState.Loading -> LoadingCentered() AdminRequestsUiState.Loading -> LoadingCentered()
AdminRequestsUiState.Empty -> EmptyState( AdminRequestsUiState.Empty -> EmptyState(
@@ -6,6 +6,7 @@ import com.fabledsword.minstrel.admin.data.AdminRequestsRepository
import com.fabledsword.minstrel.events.EventsStream import com.fabledsword.minstrel.events.EventsStream
import com.fabledsword.minstrel.models.RequestRef import com.fabledsword.minstrel.models.RequestRef
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -40,23 +41,21 @@ class AdminRequestsViewModel @Inject constructor(
} }
} }
fun refresh() { fun refresh(): Job = viewModelScope.launch {
viewModelScope.launch { internal.value = AdminRequestsUiState.Loading
internal.value = AdminRequestsUiState.Loading try {
try { val rows = repository.list()
val rows = repository.list() internal.value = if (rows.isEmpty()) {
internal.value = if (rows.isEmpty()) { AdminRequestsUiState.Empty
AdminRequestsUiState.Empty } else {
} else { AdminRequestsUiState.Success(rows)
AdminRequestsUiState.Success(rows)
}
} catch (
@Suppress("TooGenericExceptionCaught") e: Throwable,
) {
internal.value = AdminRequestsUiState.Error(
e.message ?: "Admin requests load failed",
)
} }
} catch (
@Suppress("TooGenericExceptionCaught") e: Throwable,
) {
internal.value = AdminRequestsUiState.Error(
e.message ?: "Admin requests load failed",
)
} }
} }
@@ -43,6 +43,7 @@ import com.fabledsword.minstrel.models.AdminUserRef
import com.fabledsword.minstrel.nav.AdminUsers import com.fabledsword.minstrel.nav.AdminUsers
import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions import com.fabledsword.minstrel.shared.widgets.MainAppBarActions
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -73,7 +74,10 @@ fun AdminUsersScreen(
) )
}, },
) { inner -> ) { inner ->
Box(modifier = Modifier.fillMaxSize().padding(inner)) { PullToRefreshScaffold(
onRefresh = { viewModel.refresh().join() },
modifier = Modifier.fillMaxSize().padding(inner),
) {
when (val s = state) { when (val s = state) {
AdminUsersUiState.Loading -> LoadingCentered() AdminUsersUiState.Loading -> LoadingCentered()
AdminUsersUiState.Empty -> EmptyState( AdminUsersUiState.Empty -> EmptyState(
@@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
import com.fabledsword.minstrel.admin.data.AdminUsersRepository import com.fabledsword.minstrel.admin.data.AdminUsersRepository
import com.fabledsword.minstrel.models.AdminUserRef import com.fabledsword.minstrel.models.AdminUserRef
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -30,21 +31,19 @@ class AdminUsersViewModel @Inject constructor(
refresh() refresh()
} }
fun refresh() { fun refresh(): Job = viewModelScope.launch {
viewModelScope.launch { internal.value = AdminUsersUiState.Loading
internal.value = AdminUsersUiState.Loading try {
try { val users = repository.list()
val users = repository.list() internal.value = if (users.isEmpty()) {
internal.value = if (users.isEmpty()) { AdminUsersUiState.Empty
AdminUsersUiState.Empty } else {
} else { AdminUsersUiState.Success(users)
AdminUsersUiState.Success(users)
}
} catch (
@Suppress("TooGenericExceptionCaught") e: Throwable,
) {
internal.value = AdminUsersUiState.Error(e.message ?: "Users load failed")
} }
} catch (
@Suppress("TooGenericExceptionCaught") e: Throwable,
) {
internal.value = AdminUsersUiState.Error(e.message ?: "Users load failed")
} }
} }
@@ -29,9 +29,11 @@ import com.fabledsword.minstrel.history.data.HistoryRepository
import com.fabledsword.minstrel.models.TrackRef import com.fabledsword.minstrel.models.TrackRef
import com.fabledsword.minstrel.player.PlayerController import com.fabledsword.minstrel.player.PlayerController
import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
import com.fabledsword.minstrel.shared.widgets.TrackCoverThumb import com.fabledsword.minstrel.shared.widgets.TrackCoverThumb
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsButton import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsButton
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -68,21 +70,19 @@ class HistoryTabViewModel @Inject constructor(
refresh() refresh()
} }
fun refresh() { fun refresh(): Job = viewModelScope.launch {
viewModelScope.launch { internal.value = HistoryUiState.Loading
internal.value = HistoryUiState.Loading try {
try { val page = repository.fetchPage()
val page = repository.fetchPage() internal.value = if (page.entries.isEmpty()) {
internal.value = if (page.entries.isEmpty()) { HistoryUiState.Empty
HistoryUiState.Empty } else {
} else { HistoryUiState.Success(page.entries)
HistoryUiState.Success(page.entries)
}
} catch (
@Suppress("TooGenericExceptionCaught") e: Throwable,
) {
internal.value = HistoryUiState.Error(e.message ?: "History load failed")
} }
} catch (
@Suppress("TooGenericExceptionCaught") e: Throwable,
) {
internal.value = HistoryUiState.Error(e.message ?: "History load failed")
} }
} }
@@ -102,7 +102,7 @@ fun HistoryTab(
viewModel: HistoryTabViewModel = hiltViewModel(), viewModel: HistoryTabViewModel = hiltViewModel(),
) { ) {
val state by viewModel.uiState.collectAsStateWithLifecycle() val state by viewModel.uiState.collectAsStateWithLifecycle()
Box(modifier = Modifier.fillMaxSize()) { PullToRefreshScaffold(onRefresh = { viewModel.refresh().join() }) {
when (val s = state) { when (val s = state) {
HistoryUiState.Loading -> LoadingCentered() HistoryUiState.Loading -> LoadingCentered()
HistoryUiState.Empty -> EmptyState( HistoryUiState.Empty -> EmptyState(
@@ -44,6 +44,7 @@ import com.fabledsword.minstrel.nav.Library
import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.ErrorRetry import com.fabledsword.minstrel.shared.widgets.ErrorRetry
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions import com.fabledsword.minstrel.shared.widgets.MainAppBarActions
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsViewModel import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsViewModel
/** /**
@@ -133,17 +134,19 @@ private fun ArtistsTab(
navController: NavHostController, navController: NavHostController,
) { ) {
val state by viewModel.uiState.collectAsStateWithLifecycle() val state by viewModel.uiState.collectAsStateWithLifecycle()
when (val s = state) { PullToRefreshScaffold(onRefresh = { viewModel.refresh().join() }) {
LibraryUiState.Loading -> LoadingCentered() when (val s = state) {
LibraryUiState.Empty -> EmptyState( LibraryUiState.Loading -> LoadingCentered()
title = "No artists yet", LibraryUiState.Empty -> EmptyState(
body = "Scan a folder in your server settings to populate the library.", title = "No artists yet",
) body = "Scan a folder in your server settings to populate the library.",
is LibraryUiState.Error -> ErrorRetry(message = s.message, onRetry = {}) )
is LibraryUiState.Success -> ArtistsGrid( is LibraryUiState.Error -> ErrorRetry(message = s.message, onRetry = {})
artists = s.artists, is LibraryUiState.Success -> ArtistsGrid(
onArtistClick = { id -> navController.navigate(ArtistDetail(id)) }, artists = s.artists,
) onArtistClick = { id -> navController.navigate(ArtistDetail(id)) },
)
}
} }
} }
@@ -153,17 +156,19 @@ private fun AlbumsTab(
navController: NavHostController, navController: NavHostController,
) { ) {
val state by viewModel.uiState.collectAsStateWithLifecycle() val state by viewModel.uiState.collectAsStateWithLifecycle()
when (val s = state) { PullToRefreshScaffold(onRefresh = { viewModel.refresh().join() }) {
LibraryUiState.Loading -> LoadingCentered() when (val s = state) {
LibraryUiState.Empty -> EmptyState( LibraryUiState.Loading -> LoadingCentered()
title = "No albums yet", LibraryUiState.Empty -> EmptyState(
body = "Scan a folder in your server settings to populate the library.", title = "No albums yet",
) body = "Scan a folder in your server settings to populate the library.",
is LibraryUiState.Error -> ErrorRetry(message = s.message, onRetry = {}) )
is LibraryUiState.Success -> AlbumsGrid( is LibraryUiState.Error -> ErrorRetry(message = s.message, onRetry = {})
albums = s.albums, is LibraryUiState.Success -> AlbumsGrid(
onAlbumClick = { id -> navController.navigate(AlbumDetail(id)) }, albums = s.albums,
) onAlbumClick = { id -> navController.navigate(AlbumDetail(id)) },
)
}
} }
} }
@@ -2,20 +2,24 @@ package com.fabledsword.minstrel.library.ui
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.fabledsword.minstrel.cache.sync.SyncController
import com.fabledsword.minstrel.library.data.LibraryRepository import com.fabledsword.minstrel.library.data.LibraryRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
private const val SHARE_STOP_TIMEOUT_MS = 5_000L private const val SHARE_STOP_TIMEOUT_MS = 5_000L
@HiltViewModel @HiltViewModel
class LibraryViewModel @Inject constructor( class LibraryViewModel @Inject constructor(
private val repository: LibraryRepository, repository: LibraryRepository,
private val syncController: SyncController,
) : ViewModel() { ) : ViewModel() {
val uiState: StateFlow<LibraryUiState> = val uiState: StateFlow<LibraryUiState> =
@@ -37,4 +41,13 @@ class LibraryViewModel @Inject constructor(
started = SharingStarted.WhileSubscribed(SHARE_STOP_TIMEOUT_MS), started = SharingStarted.WhileSubscribed(SHARE_STOP_TIMEOUT_MS),
initialValue = LibraryUiState.Loading, initialValue = LibraryUiState.Loading,
) )
/**
* Pull-to-refresh entry point. Triggers /api/library/sync via the
* SyncController; the resulting Room writes flow through the
* observeArtists/observeAlbums flows to re-render the grids.
*/
fun refresh(): Job = viewModelScope.launch {
syncController.syncSafe()
}
} }
@@ -40,9 +40,11 @@ import com.fabledsword.minstrel.nav.ArtistDetail
import com.fabledsword.minstrel.player.PlayerController import com.fabledsword.minstrel.player.PlayerController
import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.HorizontalScrollRow import com.fabledsword.minstrel.shared.widgets.HorizontalScrollRow
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
import com.fabledsword.minstrel.shared.widgets.TrackCoverThumb import com.fabledsword.minstrel.shared.widgets.TrackCoverThumb
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsButton import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsButton
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
@@ -80,9 +82,11 @@ class LikedTabViewModel @Inject constructor(
) : ViewModel() { ) : ViewModel() {
init { init {
viewModelScope.launch { refresh()
runCatching { repository.refreshIds() } }
}
fun refresh(): Job = viewModelScope.launch {
runCatching { repository.refreshIds() }
} }
val uiState: StateFlow<LikedTabUiState> = combine( val uiState: StateFlow<LikedTabUiState> = combine(
@@ -122,7 +126,7 @@ fun LikedTab(
viewModel: LikedTabViewModel = hiltViewModel(), viewModel: LikedTabViewModel = hiltViewModel(),
) { ) {
val state by viewModel.uiState.collectAsStateWithLifecycle() val state by viewModel.uiState.collectAsStateWithLifecycle()
Box(modifier = Modifier.fillMaxSize()) { PullToRefreshScaffold(onRefresh = { viewModel.refresh().join() }) {
when (val s = state) { when (val s = state) {
LikedTabUiState.Loading -> LoadingCentered() LikedTabUiState.Loading -> LoadingCentered()
LikedTabUiState.Empty -> EmptyState( LikedTabUiState.Empty -> EmptyState(
@@ -28,11 +28,12 @@ import com.composables.icons.lucide.ArchiveRestore
import com.composables.icons.lucide.Lucide import com.composables.icons.lucide.Lucide
import com.fabledsword.minstrel.models.QuarantineRef import com.fabledsword.minstrel.models.QuarantineRef
import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
@Composable @Composable
fun HiddenTab(viewModel: HiddenTabViewModel = hiltViewModel()) { fun HiddenTab(viewModel: HiddenTabViewModel = hiltViewModel()) {
val state by viewModel.uiState.collectAsStateWithLifecycle() val state by viewModel.uiState.collectAsStateWithLifecycle()
Box(modifier = Modifier.fillMaxSize()) { PullToRefreshScaffold(onRefresh = { viewModel.refresh().join() }) {
when (val s = state) { when (val s = state) {
HiddenTabUiState.Loading -> LoadingCentered() HiddenTabUiState.Loading -> LoadingCentered()
HiddenTabUiState.Empty -> EmptyState( HiddenTabUiState.Empty -> EmptyState(
@@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
import com.fabledsword.minstrel.models.QuarantineRef import com.fabledsword.minstrel.models.QuarantineRef
import com.fabledsword.minstrel.quarantine.data.QuarantineRepository import com.fabledsword.minstrel.quarantine.data.QuarantineRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -30,21 +31,19 @@ class HiddenTabViewModel @Inject constructor(
refresh() refresh()
} }
fun refresh() { fun refresh(): Job = viewModelScope.launch {
viewModelScope.launch { internal.value = HiddenTabUiState.Loading
internal.value = HiddenTabUiState.Loading try {
try { val rows = repository.listMine()
val rows = repository.listMine() internal.value = if (rows.isEmpty()) {
internal.value = if (rows.isEmpty()) { HiddenTabUiState.Empty
HiddenTabUiState.Empty } else {
} else { HiddenTabUiState.Success(rows)
HiddenTabUiState.Success(rows)
}
} catch (
@Suppress("TooGenericExceptionCaught") e: Throwable,
) {
internal.value = HiddenTabUiState.Error(e.message ?: "Hidden load failed")
} }
} catch (
@Suppress("TooGenericExceptionCaught") e: Throwable,
) {
internal.value = HiddenTabUiState.Error(e.message ?: "Hidden load failed")
} }
} }