mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-10-03 22:31:48 +00:00
Display number of comments
This commit is contained in:
@@ -8,13 +8,19 @@ import org.schabi.newpipe.extractor.NewPipe
|
||||
import org.schabi.newpipe.extractor.Page
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfo
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||
import org.schabi.newpipe.ui.components.video.comment.CommentInfo
|
||||
import org.schabi.newpipe.util.NO_SERVICE_ID
|
||||
|
||||
class CommentsSource(
|
||||
serviceId: Int,
|
||||
private val url: String?,
|
||||
private val repliesPage: Page?
|
||||
private val url: String,
|
||||
private val repliesPage: Page?,
|
||||
private val commentInfo: CommentInfo? = null,
|
||||
) : PagingSource<Page, CommentsInfoItem>() {
|
||||
constructor(commentInfo: CommentInfo) : this(
|
||||
commentInfo.serviceId, commentInfo.url, commentInfo.nextPage, commentInfo
|
||||
)
|
||||
|
||||
init {
|
||||
require(serviceId != NO_SERVICE_ID) { "serviceId is NO_SERVICE_ID" }
|
||||
}
|
||||
@@ -29,17 +35,11 @@ class CommentsSource(
|
||||
val info = CommentsInfo.getMoreItems(service, url, it)
|
||||
LoadResult.Page(info.items, null, info.nextPage)
|
||||
} ?: run {
|
||||
val info = CommentsInfo.getInfo(service, url)
|
||||
if (info.isCommentsDisabled) {
|
||||
LoadResult.Error(CommentsDisabledException())
|
||||
} else {
|
||||
LoadResult.Page(info.relatedItems, null, info.nextPage)
|
||||
}
|
||||
val info = commentInfo ?: CommentInfo(CommentsInfo.getInfo(service, url))
|
||||
LoadResult.Page(info.comments, null, info.nextPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Page, CommentsInfoItem>) = null
|
||||
}
|
||||
|
||||
class CommentsDisabledException : RuntimeException()
|
||||
|
||||
@@ -166,7 +166,13 @@ fun Comment(comment: CommentsInfoItem) {
|
||||
.cachedIn(coroutineScope)
|
||||
}
|
||||
|
||||
CommentSection(parentComment = comment, commentsFlow = flow)
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
CommentSection(
|
||||
commentsFlow = flow,
|
||||
commentCount = comment.replyCount,
|
||||
parentComment = comment
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.schabi.newpipe.ui.components.video.comment
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import org.schabi.newpipe.extractor.Page
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfo
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||
|
||||
@Immutable
|
||||
class CommentInfo(
|
||||
val serviceId: Int,
|
||||
val url: String,
|
||||
val comments: List<CommentsInfoItem>,
|
||||
val nextPage: Page?,
|
||||
val commentCount: Int,
|
||||
val isCommentsDisabled: Boolean
|
||||
) {
|
||||
constructor(commentsInfo: CommentsInfo) : this(
|
||||
commentsInfo.serviceId, commentsInfo.url, commentsInfo.relatedItems, commentsInfo.nextPage,
|
||||
commentsInfo.commentsCount, commentsInfo.isCommentsDisabled
|
||||
)
|
||||
}
|
||||
+125
-73
@@ -7,20 +7,19 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.LoadStates
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -30,106 +29,157 @@ import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.Page
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.Description
|
||||
import org.schabi.newpipe.paging.CommentsDisabledException
|
||||
import org.schabi.newpipe.ui.components.common.LoadingIndicator
|
||||
import org.schabi.newpipe.ui.components.common.NoItemsMessage
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import org.schabi.newpipe.viewmodels.CommentsViewModel
|
||||
import org.schabi.newpipe.viewmodels.util.Resource
|
||||
|
||||
@Composable
|
||||
fun CommentSection(commentsViewModel: CommentsViewModel = viewModel()) {
|
||||
CommentSection(commentsFlow = commentsViewModel.comments)
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
val state by commentsViewModel.uiState.collectAsStateWithLifecycle()
|
||||
CommentSection(state, commentsViewModel.comments)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CommentSection(
|
||||
uiState: Resource<CommentInfo>,
|
||||
commentsFlow: Flow<PagingData<CommentsInfoItem>>
|
||||
) {
|
||||
when (uiState) {
|
||||
is Resource.Loading -> LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
|
||||
|
||||
is Resource.Success -> {
|
||||
val commentsInfo = uiState.data
|
||||
CommentSection(
|
||||
commentsFlow = commentsFlow,
|
||||
commentCount = commentsInfo.commentCount,
|
||||
isCommentsDisabled = commentsInfo.isCommentsDisabled
|
||||
)
|
||||
}
|
||||
|
||||
is Resource.Error -> {
|
||||
// This is not rendered as VideoDetailFragment handles errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CommentSection(
|
||||
commentsFlow: Flow<PagingData<CommentsInfoItem>>,
|
||||
commentCount: Int,
|
||||
parentComment: CommentsInfoItem? = null,
|
||||
commentsFlow: Flow<PagingData<CommentsInfoItem>>
|
||||
isCommentsDisabled: Boolean = false,
|
||||
) {
|
||||
val comments = commentsFlow.collectAsLazyPagingItems()
|
||||
val itemCount by remember { derivedStateOf { comments.itemCount } }
|
||||
val nestedScrollInterop = rememberNestedScrollInteropConnection()
|
||||
val state = rememberLazyListState()
|
||||
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
LazyColumnScrollbar(state = state) {
|
||||
LazyColumn(modifier = Modifier.nestedScroll(nestedScrollInterop), state = state) {
|
||||
if (parentComment != null) {
|
||||
LazyColumnScrollbar(state = state) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.nestedScroll(nestedScrollInterop),
|
||||
state = state
|
||||
) {
|
||||
if (parentComment != null) {
|
||||
item {
|
||||
CommentRepliesHeader(comment = parentComment)
|
||||
HorizontalDivider(thickness = 1.dp)
|
||||
}
|
||||
}
|
||||
|
||||
if (comments.itemCount == 0) {
|
||||
item {
|
||||
val refresh = comments.loadState.refresh
|
||||
if (refresh is LoadState.Loading) {
|
||||
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
|
||||
} else {
|
||||
val message = if (refresh is LoadState.Error) {
|
||||
R.string.error_unable_to_load_comments
|
||||
} else if (isCommentsDisabled) {
|
||||
R.string.comments_are_disabled
|
||||
} else {
|
||||
R.string.no_comments
|
||||
}
|
||||
NoItemsMessage(message)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// The number of replies is already shown in the main comment section
|
||||
if (parentComment == null) {
|
||||
item {
|
||||
CommentRepliesHeader(comment = parentComment)
|
||||
HorizontalDivider(thickness = 1.dp)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
text = pluralStringResource(R.plurals.comments, commentCount, commentCount),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (itemCount == 0) {
|
||||
item {
|
||||
val refresh = comments.loadState.refresh
|
||||
if (refresh is LoadState.Loading) {
|
||||
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
|
||||
} else {
|
||||
val error = (refresh as? LoadState.Error)?.error
|
||||
val message = if (error is CommentsDisabledException) {
|
||||
R.string.comments_are_disabled
|
||||
} else {
|
||||
R.string.no_comments
|
||||
}
|
||||
NoItemsMessage(message)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
items(itemCount) {
|
||||
Comment(comment = comments[it]!!)
|
||||
}
|
||||
items(comments.itemCount) {
|
||||
Comment(comment = comments[it]!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CommentDataProvider : PreviewParameterProvider<PagingData<CommentsInfoItem>> {
|
||||
private val notLoading = LoadState.NotLoading(true)
|
||||
|
||||
override val values = sequenceOf(
|
||||
// Normal view
|
||||
PagingData.from(
|
||||
listOf(
|
||||
CommentsInfoItem(
|
||||
commentText = Description(
|
||||
"Comment 1\n\nThis line should be hidden by default.",
|
||||
Description.PLAIN_TEXT
|
||||
),
|
||||
uploaderName = "Test",
|
||||
replies = Page(""),
|
||||
replyCount = 10
|
||||
)
|
||||
) + (2..10).map {
|
||||
CommentsInfoItem(
|
||||
commentText = Description("Comment $it", Description.PLAIN_TEXT),
|
||||
uploaderName = "Test"
|
||||
)
|
||||
}
|
||||
),
|
||||
// Comments disabled
|
||||
PagingData.from(
|
||||
listOf<CommentsInfoItem>(),
|
||||
LoadStates(LoadState.Error(CommentsDisabledException()), notLoading, notLoading)
|
||||
),
|
||||
// No comments
|
||||
PagingData.from(
|
||||
listOf<CommentsInfoItem>(),
|
||||
LoadStates(notLoading, notLoading, notLoading)
|
||||
)
|
||||
)
|
||||
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun CommentSectionLoadingPreview() {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
CommentSection(uiState = Resource.Loading, commentsFlow = flowOf())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun CommentSectionPreview(
|
||||
@PreviewParameter(CommentDataProvider::class) pagingData: PagingData<CommentsInfoItem>
|
||||
) {
|
||||
private fun CommentSectionSuccessPreview() {
|
||||
val comments = listOf(
|
||||
CommentsInfoItem(
|
||||
commentText = Description(
|
||||
"Comment 1\n\nThis line should be hidden by default.",
|
||||
Description.PLAIN_TEXT
|
||||
),
|
||||
uploaderName = "Test",
|
||||
replies = Page(""),
|
||||
replyCount = 10
|
||||
)
|
||||
) + (2..10).map {
|
||||
CommentsInfoItem(
|
||||
commentText = Description("Comment $it", Description.PLAIN_TEXT),
|
||||
uploaderName = "Test"
|
||||
)
|
||||
}
|
||||
|
||||
AppTheme {
|
||||
CommentSection(commentsFlow = flowOf(pagingData))
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
CommentSection(
|
||||
uiState = Resource.Success(
|
||||
CommentInfo(
|
||||
serviceId = 1, url = "", comments = comments, nextPage = null,
|
||||
commentCount = 10, isCommentsDisabled = false
|
||||
)
|
||||
),
|
||||
commentsFlow = flowOf(PagingData.from(comments))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun CommentSectionErrorPreview() {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
CommentSection(uiState = Resource.Error(RuntimeException()), commentsFlow = flowOf())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +203,8 @@ private fun CommentRepliesPreview() {
|
||||
val flow = flowOf(PagingData.from(replies))
|
||||
|
||||
AppTheme {
|
||||
CommentSection(parentComment = comment, commentsFlow = flow)
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
CommentSection(parentComment = comment, commentsFlow = flow, commentCount = 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,17 +6,39 @@ import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.cachedIn
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfo
|
||||
import org.schabi.newpipe.paging.CommentsSource
|
||||
import org.schabi.newpipe.util.KEY_SERVICE_ID
|
||||
import org.schabi.newpipe.ui.components.video.comment.CommentInfo
|
||||
import org.schabi.newpipe.util.KEY_URL
|
||||
import org.schabi.newpipe.util.NO_SERVICE_ID
|
||||
import org.schabi.newpipe.viewmodels.util.Resource
|
||||
|
||||
class CommentsViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
|
||||
private val serviceId = savedStateHandle[KEY_SERVICE_ID] ?: NO_SERVICE_ID
|
||||
private val url = savedStateHandle.get<String>(KEY_URL)
|
||||
val uiState = savedStateHandle.getStateFlow(KEY_URL, "")
|
||||
.map {
|
||||
try {
|
||||
Resource.Success(CommentInfo(CommentsInfo.getInfo(it)))
|
||||
} catch (e: Exception) {
|
||||
Resource.Error(e)
|
||||
}
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Resource.Loading)
|
||||
|
||||
val comments = Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
|
||||
CommentsSource(serviceId, url, null)
|
||||
}.flow
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val comments = uiState
|
||||
.filterIsInstance<Resource.Success<CommentInfo>>()
|
||||
.flatMapLatest {
|
||||
Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
|
||||
CommentsSource(it.data)
|
||||
}.flow
|
||||
}
|
||||
.cachedIn(viewModelScope)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.schabi.newpipe.viewmodels.util
|
||||
|
||||
sealed class Resource<out T> {
|
||||
data object Loading : Resource<Nothing>()
|
||||
class Success<T>(val data: T) : Resource<T>()
|
||||
class Error(val throwable: Throwable) : Resource<Nothing>()
|
||||
}
|
||||
Reference in New Issue
Block a user