1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2025-01-04 22:40:32 +00:00

Use UI state classes in playlist

This commit is contained in:
Isira Seneviratne 2024-12-29 09:33:29 +05:30
parent 52a2accea9
commit b644160eb1
2 changed files with 66 additions and 40 deletions

View File

@ -5,11 +5,11 @@ import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems 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.items.stream.StreamInfoItem
import org.schabi.newpipe.ui.components.playlist.PlaylistHeader import org.schabi.newpipe.ui.components.playlist.PlaylistHeader
import org.schabi.newpipe.ui.components.playlist.PlaylistInfo 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.ui.theme.AppTheme
import org.schabi.newpipe.viewmodels.PlaylistViewModel import org.schabi.newpipe.viewmodels.PlaylistViewModel
import org.schabi.newpipe.viewmodels.util.Resource
@Composable @Composable
fun PlaylistScreen(playlistViewModel: PlaylistViewModel = viewModel()) { fun PlaylistScreen(playlistViewModel: PlaylistViewModel = viewModel()) {
Surface(color = MaterialTheme.colorScheme.background) { Surface(color = MaterialTheme.colorScheme.background) {
val playlistInfo by playlistViewModel.playlistInfo.collectAsState() val uiState by playlistViewModel.uiState.collectAsStateWithLifecycle()
PlaylistScreen(playlistInfo, playlistViewModel.streamItems) PlaylistScreen(uiState, playlistViewModel.streamItems)
} }
} }
@Composable @Composable
private fun PlaylistScreen( private fun PlaylistScreen(
playlistInfo: PlaylistInfo?, uiState: Resource<PlaylistInfo>,
streamFlow: Flow<PagingData<StreamInfoItem>> streamFlow: Flow<PagingData<StreamInfoItem>>
) { ) {
playlistInfo?.let { when (uiState) {
val streams = streamFlow.collectAsLazyPagingItems() 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 // 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 // happen. As such, the duration initially displayed will be the incomplete duration if more
// items can be loaded. // items can be loaded.
val totalDuration by remember { val totalDuration by remember {
derivedStateOf { derivedStateOf {
streams.itemSnapshotList.sumOf { it!!.duration } streams.itemSnapshotList.sumOf { it!!.duration }
}
} }
ItemList(
items = streams,
gridHeader = {
item(span = { GridItemSpan(maxLineSpan) }) {
PlaylistHeader(info, totalDuration)
}
},
listHeader = {
item {
PlaylistHeader(info, totalDuration)
}
}
)
} }
ItemList( is Resource.Loading -> {
items = streams, LoadingIndicator()
gridHeader = { }
item(span = { GridItemSpan(maxLineSpan) }) {
PlaylistHeader(it, totalDuration) is Resource.Error -> {
} // TODO use error panel instead
}, EmptyStateComposable(
listHeader = { EmptyStateSpec.DisabledComments.copy(
item { descriptionText = { "Could not load streams" }
PlaylistHeader(it, totalDuration) )
} )
} }
) }
} ?: LoadingIndicator()
} }
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
@ -81,7 +100,7 @@ private fun PlaylistPreview() {
AppTheme { AppTheme {
Surface(color = MaterialTheme.colorScheme.background) { Surface(color = MaterialTheme.colorScheme.background) {
PlaylistScreen(playlistInfo, streamFlow) PlaylistScreen(Resource.Success(playlistInfo), streamFlow)
} }
} }
} }

View File

@ -10,7 +10,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn 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_SERVICE_ID
import org.schabi.newpipe.util.KEY_URL import org.schabi.newpipe.util.KEY_URL
import org.schabi.newpipe.util.NO_SERVICE_ID import org.schabi.newpipe.util.NO_SERVICE_ID
import org.schabi.newpipe.viewmodels.util.Resource
import org.schabi.newpipe.extractor.playlist.PlaylistInfo as ExtractorPlaylistInfo import org.schabi.newpipe.extractor.playlist.PlaylistInfo as ExtractorPlaylistInfo
class PlaylistViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { class PlaylistViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
private val serviceIdState = savedStateHandle.getStateFlow(KEY_SERVICE_ID, NO_SERVICE_ID) private val serviceIdState = savedStateHandle.getStateFlow(KEY_SERVICE_ID, NO_SERVICE_ID)
private val urlState = savedStateHandle.getStateFlow(KEY_URL, "") private val urlState = savedStateHandle.getStateFlow(KEY_URL, "")
val playlistInfo = serviceIdState.combine(urlState) { id, url -> val uiState = serviceIdState.combine(urlState) { id, url ->
val info = ExtractorPlaylistInfo.getInfo(NewPipe.getService(id), url) try {
val description = info.description ?: Description.EMPTY_DESCRIPTION val extractorInfo = ExtractorPlaylistInfo.getInfo(NewPipe.getService(id), url)
PlaylistInfo( val description = extractorInfo.description ?: Description.EMPTY_DESCRIPTION
info.id, info.serviceId, info.url, info.name, description, info.relatedItems, val info = PlaylistInfo(
info.streamCount, info.uploaderUrl, info.uploaderName, info.uploaderAvatars, extractorInfo.id, extractorInfo.serviceId, extractorInfo.url, extractorInfo.name,
info.nextPage 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) .flowOn(Dispatchers.IO)
.stateIn(viewModelScope, SharingStarted.Eagerly, null) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Resource.Loading)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
val streamItems = playlistInfo val streamItems = uiState
.filterNotNull() .filterIsInstance<Resource.Success<PlaylistInfo>>()
.flatMapLatest { .flatMapLatest {
Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) { Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
PlaylistItemsSource(it) PlaylistItemsSource(it.data)
}.flow }.flow
} }
.cachedIn(viewModelScope) .cachedIn(viewModelScope)