Display number of comments

This commit is contained in:
Isira Seneviratne 2024-08-30 08:37:42 +05:30
parent 4cac111b66
commit 3785404618
7 changed files with 203 additions and 91 deletions

View File

@ -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()

View File

@ -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
)
}
}
}
}

View File

@ -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
)
}

View File

@ -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)
}
}
}

View File

@ -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)
}

View File

@ -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>()
}

View File

@ -856,4 +856,8 @@
<string name="show_less">Show less</string>
<string name="import_settings_vulnerable_format">The settings in the export being imported use a vulnerable format that was deprecated since NewPipe 0.27.0. Make sure the export being imported is from a trusted source, and prefer using only exports obtained from NewPipe 0.27.0 or newer in the future. Support for importing settings in this vulnerable format will soon be removed completely, and then old versions of NewPipe will not be able to import settings of exports from new versions anymore.</string>
<string name="auto_queue_description">Next</string>
<plurals name="comments">
<item quantity="one">%d comment</item>
<item quantity="other">%d comments</item>
</plurals>
</resources>