diff --git a/app/src/main/java/org/schabi/newpipe/ui/screens/PlaylistScreen.kt b/app/src/main/java/org/schabi/newpipe/ui/screens/PlaylistScreen.kt index c63f8bcc3..745bf6cc4 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/screens/PlaylistScreen.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/PlaylistScreen.kt @@ -5,11 +5,11 @@ import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import androidx.paging.PagingData import androidx.paging.compose.collectAsLazyPagingItems @@ -23,48 +23,67 @@ import org.schabi.newpipe.ui.components.items.ItemList import org.schabi.newpipe.ui.components.items.stream.StreamInfoItem import org.schabi.newpipe.ui.components.playlist.PlaylistHeader import org.schabi.newpipe.ui.components.playlist.PlaylistInfo +import org.schabi.newpipe.ui.emptystate.EmptyStateComposable +import org.schabi.newpipe.ui.emptystate.EmptyStateSpec import org.schabi.newpipe.ui.theme.AppTheme import org.schabi.newpipe.viewmodels.PlaylistViewModel +import org.schabi.newpipe.viewmodels.util.Resource @Composable fun PlaylistScreen(playlistViewModel: PlaylistViewModel = viewModel()) { Surface(color = MaterialTheme.colorScheme.background) { - val playlistInfo by playlistViewModel.playlistInfo.collectAsState() - PlaylistScreen(playlistInfo, playlistViewModel.streamItems) + val uiState by playlistViewModel.uiState.collectAsStateWithLifecycle() + PlaylistScreen(uiState, playlistViewModel.streamItems) } } @Composable private fun PlaylistScreen( - playlistInfo: PlaylistInfo?, + uiState: Resource, streamFlow: Flow> ) { - playlistInfo?.let { - val streams = streamFlow.collectAsLazyPagingItems() + when (uiState) { + is Resource.Success -> { + val info = uiState.data + val streams = streamFlow.collectAsLazyPagingItems() - // Paging's load states only indicate when loading is currently happening, not if it can/will - // happen. As such, the duration initially displayed will be the incomplete duration if more - // items can be loaded. - val totalDuration by remember { - derivedStateOf { - streams.itemSnapshotList.sumOf { it!!.duration } + // Paging's load states only indicate when loading is currently happening, not if it can/will + // happen. As such, the duration initially displayed will be the incomplete duration if more + // items can be loaded. + val totalDuration by remember { + derivedStateOf { + streams.itemSnapshotList.sumOf { it!!.duration } + } } + + ItemList( + items = streams, + gridHeader = { + item(span = { GridItemSpan(maxLineSpan) }) { + PlaylistHeader(info, totalDuration) + } + }, + listHeader = { + item { + PlaylistHeader(info, totalDuration) + } + } + ) } - ItemList( - items = streams, - gridHeader = { - item(span = { GridItemSpan(maxLineSpan) }) { - PlaylistHeader(it, totalDuration) - } - }, - listHeader = { - item { - PlaylistHeader(it, totalDuration) - } - } - ) - } ?: LoadingIndicator() + is Resource.Loading -> { + LoadingIndicator() + } + + is Resource.Error -> { + // TODO use error panel instead + EmptyStateComposable( + EmptyStateSpec.DisabledComments.copy( + descriptionText = { "Could not load streams" } + ) + ) + } + } } @Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) @@ -81,7 +100,7 @@ private fun PlaylistPreview() { AppTheme { Surface(color = MaterialTheme.colorScheme.background) { - PlaylistScreen(playlistInfo, streamFlow) + PlaylistScreen(Resource.Success(playlistInfo), streamFlow) } } } diff --git a/app/src/main/java/org/schabi/newpipe/viewmodels/PlaylistViewModel.kt b/app/src/main/java/org/schabi/newpipe/viewmodels/PlaylistViewModel.kt index 8efe69791..99dd9f0d0 100644 --- a/app/src/main/java/org/schabi/newpipe/viewmodels/PlaylistViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/viewmodels/PlaylistViewModel.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn @@ -21,30 +21,37 @@ import org.schabi.newpipe.ui.components.playlist.PlaylistInfo import org.schabi.newpipe.util.KEY_SERVICE_ID import org.schabi.newpipe.util.KEY_URL import org.schabi.newpipe.util.NO_SERVICE_ID +import org.schabi.newpipe.viewmodels.util.Resource import org.schabi.newpipe.extractor.playlist.PlaylistInfo as ExtractorPlaylistInfo class PlaylistViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private val serviceIdState = savedStateHandle.getStateFlow(KEY_SERVICE_ID, NO_SERVICE_ID) private val urlState = savedStateHandle.getStateFlow(KEY_URL, "") - val playlistInfo = serviceIdState.combine(urlState) { id, url -> - val info = ExtractorPlaylistInfo.getInfo(NewPipe.getService(id), url) - val description = info.description ?: Description.EMPTY_DESCRIPTION - PlaylistInfo( - info.id, info.serviceId, info.url, info.name, description, info.relatedItems, - info.streamCount, info.uploaderUrl, info.uploaderName, info.uploaderAvatars, - info.nextPage - ) + val uiState = serviceIdState.combine(urlState) { id, url -> + try { + val extractorInfo = ExtractorPlaylistInfo.getInfo(NewPipe.getService(id), url) + val description = extractorInfo.description ?: Description.EMPTY_DESCRIPTION + val info = PlaylistInfo( + extractorInfo.id, extractorInfo.serviceId, extractorInfo.url, extractorInfo.name, + description, extractorInfo.relatedItems, extractorInfo.streamCount, + extractorInfo.uploaderUrl, extractorInfo.uploaderName, extractorInfo.uploaderAvatars, + extractorInfo.nextPage + ) + Resource.Success(info) + } catch (e: Exception) { + Resource.Error(e) + } } .flowOn(Dispatchers.IO) - .stateIn(viewModelScope, SharingStarted.Eagerly, null) + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Resource.Loading) @OptIn(ExperimentalCoroutinesApi::class) - val streamItems = playlistInfo - .filterNotNull() + val streamItems = uiState + .filterIsInstance>() .flatMapLatest { Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) { - PlaylistItemsSource(it) + PlaylistItemsSource(it.data) }.flow } .cachedIn(viewModelScope)