diff --git a/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminLandingScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminLandingScreen.kt index e5f694a3..b81a6e67 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminLandingScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminLandingScreen.kt @@ -45,7 +45,9 @@ import com.fabledsword.minstrel.nav.AdminRequests import com.fabledsword.minstrel.nav.AdminUsers import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.MainAppBarActions +import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow @@ -80,24 +82,22 @@ class AdminLandingViewModel @Inject constructor( refresh() } - fun refresh() { - viewModelScope.launch { - internal.value = AdminLandingUiState.Loading - try { - val counts = coroutineScope { - val req = async { requestsRepo.list().size } - val qua = async { quarantineRepo.list().size } - val usr = async { usersRepo.list().size } - 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", - ) + fun refresh(): Job = viewModelScope.launch { + internal.value = AdminLandingUiState.Loading + try { + val counts = coroutineScope { + val req = async { requestsRepo.list().size } + val qua = async { quarantineRepo.list().size } + val usr = async { usersRepo.list().size } + 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", + ) } } } @@ -130,7 +130,10 @@ fun AdminLandingScreen( ) }, ) { inner -> - Box(modifier = Modifier.fillMaxSize().padding(inner)) { + PullToRefreshScaffold( + onRefresh = { viewModel.refresh().join() }, + modifier = Modifier.fillMaxSize().padding(inner), + ) { when (val s = state) { AdminLandingUiState.Loading -> LoadingCentered() is AdminLandingUiState.Error -> EmptyState( diff --git a/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminQuarantineScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminQuarantineScreen.kt index 93691411..b56dfafd 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminQuarantineScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminQuarantineScreen.kt @@ -37,6 +37,7 @@ import com.fabledsword.minstrel.models.AdminQuarantineItemRef import com.fabledsword.minstrel.nav.AdminQuarantine import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.MainAppBarActions +import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -64,7 +65,10 @@ fun AdminQuarantineScreen( ) }, ) { inner -> - Box(modifier = Modifier.fillMaxSize().padding(inner)) { + PullToRefreshScaffold( + onRefresh = { viewModel.refresh().join() }, + modifier = Modifier.fillMaxSize().padding(inner), + ) { when (val s = state) { AdminQuarantineUiState.Loading -> LoadingCentered() AdminQuarantineUiState.Empty -> EmptyState( diff --git a/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminQuarantineViewModel.kt b/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminQuarantineViewModel.kt index 21bd45ba..04a1d3cc 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminQuarantineViewModel.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminQuarantineViewModel.kt @@ -6,6 +6,7 @@ import com.fabledsword.minstrel.admin.data.AdminQuarantineRepository import com.fabledsword.minstrel.events.EventsStream import com.fabledsword.minstrel.models.AdminQuarantineItemRef import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -38,23 +39,21 @@ class AdminQuarantineViewModel @Inject constructor( } } - fun refresh() { - viewModelScope.launch { - internal.value = AdminQuarantineUiState.Loading - try { - val rows = repository.list() - internal.value = if (rows.isEmpty()) { - AdminQuarantineUiState.Empty - } else { - AdminQuarantineUiState.Success(rows) - } - } catch ( - @Suppress("TooGenericExceptionCaught") e: Throwable, - ) { - internal.value = AdminQuarantineUiState.Error( - e.message ?: "Admin quarantine load failed", - ) + fun refresh(): Job = viewModelScope.launch { + internal.value = AdminQuarantineUiState.Loading + try { + val rows = repository.list() + internal.value = if (rows.isEmpty()) { + AdminQuarantineUiState.Empty + } else { + AdminQuarantineUiState.Success(rows) } + } catch ( + @Suppress("TooGenericExceptionCaught") e: Throwable, + ) { + internal.value = AdminQuarantineUiState.Error( + e.message ?: "Admin quarantine load failed", + ) } } diff --git a/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminRequestsScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminRequestsScreen.kt index 627cc197..013a3cb9 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminRequestsScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminRequestsScreen.kt @@ -36,6 +36,7 @@ import com.fabledsword.minstrel.models.RequestRef import com.fabledsword.minstrel.nav.AdminRequests import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.MainAppBarActions +import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -63,7 +64,10 @@ fun AdminRequestsScreen( ) }, ) { inner -> - Box(modifier = Modifier.fillMaxSize().padding(inner)) { + PullToRefreshScaffold( + onRefresh = { viewModel.refresh().join() }, + modifier = Modifier.fillMaxSize().padding(inner), + ) { when (val s = state) { AdminRequestsUiState.Loading -> LoadingCentered() AdminRequestsUiState.Empty -> EmptyState( diff --git a/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminRequestsViewModel.kt b/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminRequestsViewModel.kt index 6e3beb27..657722a1 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminRequestsViewModel.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminRequestsViewModel.kt @@ -6,6 +6,7 @@ import com.fabledsword.minstrel.admin.data.AdminRequestsRepository import com.fabledsword.minstrel.events.EventsStream import com.fabledsword.minstrel.models.RequestRef import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -40,23 +41,21 @@ class AdminRequestsViewModel @Inject constructor( } } - fun refresh() { - viewModelScope.launch { - internal.value = AdminRequestsUiState.Loading - try { - val rows = repository.list() - internal.value = if (rows.isEmpty()) { - AdminRequestsUiState.Empty - } else { - AdminRequestsUiState.Success(rows) - } - } catch ( - @Suppress("TooGenericExceptionCaught") e: Throwable, - ) { - internal.value = AdminRequestsUiState.Error( - e.message ?: "Admin requests load failed", - ) + fun refresh(): Job = viewModelScope.launch { + internal.value = AdminRequestsUiState.Loading + try { + val rows = repository.list() + internal.value = if (rows.isEmpty()) { + AdminRequestsUiState.Empty + } else { + AdminRequestsUiState.Success(rows) } + } catch ( + @Suppress("TooGenericExceptionCaught") e: Throwable, + ) { + internal.value = AdminRequestsUiState.Error( + e.message ?: "Admin requests load failed", + ) } } diff --git a/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminUsersScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminUsersScreen.kt index 9b42a9fa..3198712c 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminUsersScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminUsersScreen.kt @@ -43,6 +43,7 @@ import com.fabledsword.minstrel.models.AdminUserRef import com.fabledsword.minstrel.nav.AdminUsers import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.MainAppBarActions +import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -73,7 +74,10 @@ fun AdminUsersScreen( ) }, ) { inner -> - Box(modifier = Modifier.fillMaxSize().padding(inner)) { + PullToRefreshScaffold( + onRefresh = { viewModel.refresh().join() }, + modifier = Modifier.fillMaxSize().padding(inner), + ) { when (val s = state) { AdminUsersUiState.Loading -> LoadingCentered() AdminUsersUiState.Empty -> EmptyState( diff --git a/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminUsersViewModel.kt b/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminUsersViewModel.kt index b3d9b5f7..db5ba657 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminUsersViewModel.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminUsersViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import com.fabledsword.minstrel.admin.data.AdminUsersRepository import com.fabledsword.minstrel.models.AdminUserRef import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -30,21 +31,19 @@ class AdminUsersViewModel @Inject constructor( refresh() } - fun refresh() { - viewModelScope.launch { - internal.value = AdminUsersUiState.Loading - try { - val users = repository.list() - internal.value = if (users.isEmpty()) { - AdminUsersUiState.Empty - } else { - AdminUsersUiState.Success(users) - } - } catch ( - @Suppress("TooGenericExceptionCaught") e: Throwable, - ) { - internal.value = AdminUsersUiState.Error(e.message ?: "Users load failed") + fun refresh(): Job = viewModelScope.launch { + internal.value = AdminUsersUiState.Loading + try { + val users = repository.list() + internal.value = if (users.isEmpty()) { + AdminUsersUiState.Empty + } else { + AdminUsersUiState.Success(users) } + } catch ( + @Suppress("TooGenericExceptionCaught") e: Throwable, + ) { + internal.value = AdminUsersUiState.Error(e.message ?: "Users load failed") } } diff --git a/android/app/src/main/java/com/fabledsword/minstrel/history/ui/HistoryTab.kt b/android/app/src/main/java/com/fabledsword/minstrel/history/ui/HistoryTab.kt index d1497d1c..a5711036 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/history/ui/HistoryTab.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/history/ui/HistoryTab.kt @@ -29,9 +29,11 @@ import com.fabledsword.minstrel.history.data.HistoryRepository import com.fabledsword.minstrel.models.TrackRef import com.fabledsword.minstrel.player.PlayerController 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.trackactions.TrackActionsButton import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -68,21 +70,19 @@ class HistoryTabViewModel @Inject constructor( refresh() } - fun refresh() { - viewModelScope.launch { - internal.value = HistoryUiState.Loading - try { - val page = repository.fetchPage() - internal.value = if (page.entries.isEmpty()) { - HistoryUiState.Empty - } else { - HistoryUiState.Success(page.entries) - } - } catch ( - @Suppress("TooGenericExceptionCaught") e: Throwable, - ) { - internal.value = HistoryUiState.Error(e.message ?: "History load failed") + fun refresh(): Job = viewModelScope.launch { + internal.value = HistoryUiState.Loading + try { + val page = repository.fetchPage() + internal.value = if (page.entries.isEmpty()) { + HistoryUiState.Empty + } else { + HistoryUiState.Success(page.entries) } + } catch ( + @Suppress("TooGenericExceptionCaught") e: Throwable, + ) { + internal.value = HistoryUiState.Error(e.message ?: "History load failed") } } @@ -102,7 +102,7 @@ fun HistoryTab( viewModel: HistoryTabViewModel = hiltViewModel(), ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - Box(modifier = Modifier.fillMaxSize()) { + PullToRefreshScaffold(onRefresh = { viewModel.refresh().join() }) { when (val s = state) { HistoryUiState.Loading -> LoadingCentered() HistoryUiState.Empty -> EmptyState( diff --git a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryScreen.kt index 9c7956e8..89a11f09 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryScreen.kt @@ -44,6 +44,7 @@ import com.fabledsword.minstrel.nav.Library import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.ErrorRetry import com.fabledsword.minstrel.shared.widgets.MainAppBarActions +import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsViewModel /** @@ -133,17 +134,19 @@ private fun ArtistsTab( navController: NavHostController, ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - when (val s = state) { - LibraryUiState.Loading -> LoadingCentered() - LibraryUiState.Empty -> EmptyState( - 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( - artists = s.artists, - onArtistClick = { id -> navController.navigate(ArtistDetail(id)) }, - ) + PullToRefreshScaffold(onRefresh = { viewModel.refresh().join() }) { + when (val s = state) { + LibraryUiState.Loading -> LoadingCentered() + LibraryUiState.Empty -> EmptyState( + 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( + artists = s.artists, + onArtistClick = { id -> navController.navigate(ArtistDetail(id)) }, + ) + } } } @@ -153,17 +156,19 @@ private fun AlbumsTab( navController: NavHostController, ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - when (val s = state) { - LibraryUiState.Loading -> LoadingCentered() - LibraryUiState.Empty -> EmptyState( - 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( - albums = s.albums, - onAlbumClick = { id -> navController.navigate(AlbumDetail(id)) }, - ) + PullToRefreshScaffold(onRefresh = { viewModel.refresh().join() }) { + when (val s = state) { + LibraryUiState.Loading -> LoadingCentered() + LibraryUiState.Empty -> EmptyState( + 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( + albums = s.albums, + onAlbumClick = { id -> navController.navigate(AlbumDetail(id)) }, + ) + } } } diff --git a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryViewModel.kt b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryViewModel.kt index 2f7e0f86..10e392da 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryViewModel.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryViewModel.kt @@ -2,20 +2,24 @@ package com.fabledsword.minstrel.library.ui import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.fabledsword.minstrel.cache.sync.SyncController import com.fabledsword.minstrel.library.data.LibraryRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import javax.inject.Inject private const val SHARE_STOP_TIMEOUT_MS = 5_000L @HiltViewModel class LibraryViewModel @Inject constructor( - private val repository: LibraryRepository, + repository: LibraryRepository, + private val syncController: SyncController, ) : ViewModel() { val uiState: StateFlow = @@ -37,4 +41,13 @@ class LibraryViewModel @Inject constructor( started = SharingStarted.WhileSubscribed(SHARE_STOP_TIMEOUT_MS), 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() + } } diff --git a/android/app/src/main/java/com/fabledsword/minstrel/likes/ui/LikedTab.kt b/android/app/src/main/java/com/fabledsword/minstrel/likes/ui/LikedTab.kt index 918d5f94..a3faed34 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/likes/ui/LikedTab.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/likes/ui/LikedTab.kt @@ -40,9 +40,11 @@ import com.fabledsword.minstrel.nav.ArtistDetail import com.fabledsword.minstrel.player.PlayerController import com.fabledsword.minstrel.shared.widgets.EmptyState 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.trackactions.TrackActionsButton import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch @@ -80,9 +82,11 @@ class LikedTabViewModel @Inject constructor( ) : ViewModel() { init { - viewModelScope.launch { - runCatching { repository.refreshIds() } - } + refresh() + } + + fun refresh(): Job = viewModelScope.launch { + runCatching { repository.refreshIds() } } val uiState: StateFlow = combine( @@ -122,7 +126,7 @@ fun LikedTab( viewModel: LikedTabViewModel = hiltViewModel(), ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - Box(modifier = Modifier.fillMaxSize()) { + PullToRefreshScaffold(onRefresh = { viewModel.refresh().join() }) { when (val s = state) { LikedTabUiState.Loading -> LoadingCentered() LikedTabUiState.Empty -> EmptyState( diff --git a/android/app/src/main/java/com/fabledsword/minstrel/quarantine/ui/HiddenTab.kt b/android/app/src/main/java/com/fabledsword/minstrel/quarantine/ui/HiddenTab.kt index d3b55119..64afa193 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/quarantine/ui/HiddenTab.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/quarantine/ui/HiddenTab.kt @@ -28,11 +28,12 @@ import com.composables.icons.lucide.ArchiveRestore import com.composables.icons.lucide.Lucide import com.fabledsword.minstrel.models.QuarantineRef import com.fabledsword.minstrel.shared.widgets.EmptyState +import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold @Composable fun HiddenTab(viewModel: HiddenTabViewModel = hiltViewModel()) { val state by viewModel.uiState.collectAsStateWithLifecycle() - Box(modifier = Modifier.fillMaxSize()) { + PullToRefreshScaffold(onRefresh = { viewModel.refresh().join() }) { when (val s = state) { HiddenTabUiState.Loading -> LoadingCentered() HiddenTabUiState.Empty -> EmptyState( diff --git a/android/app/src/main/java/com/fabledsword/minstrel/quarantine/ui/HiddenTabViewModel.kt b/android/app/src/main/java/com/fabledsword/minstrel/quarantine/ui/HiddenTabViewModel.kt index 734b1a4d..a7dca162 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/quarantine/ui/HiddenTabViewModel.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/quarantine/ui/HiddenTabViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import com.fabledsword.minstrel.models.QuarantineRef import com.fabledsword.minstrel.quarantine.data.QuarantineRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -30,21 +31,19 @@ class HiddenTabViewModel @Inject constructor( refresh() } - fun refresh() { - viewModelScope.launch { - internal.value = HiddenTabUiState.Loading - try { - val rows = repository.listMine() - internal.value = if (rows.isEmpty()) { - HiddenTabUiState.Empty - } else { - HiddenTabUiState.Success(rows) - } - } catch ( - @Suppress("TooGenericExceptionCaught") e: Throwable, - ) { - internal.value = HiddenTabUiState.Error(e.message ?: "Hidden load failed") + fun refresh(): Job = viewModelScope.launch { + internal.value = HiddenTabUiState.Loading + try { + val rows = repository.listMine() + internal.value = if (rows.isEmpty()) { + HiddenTabUiState.Empty + } else { + HiddenTabUiState.Success(rows) } + } catch ( + @Suppress("TooGenericExceptionCaught") e: Throwable, + ) { + internal.value = HiddenTabUiState.Error(e.message ?: "Hidden load failed") } }