1
0
mirror of https://github.com/TeamNewPipe/NewPipe.git synced 2024-11-25 12:32:31 +01:00

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.Page
import org.schabi.newpipe.extractor.comments.CommentsInfo import org.schabi.newpipe.extractor.comments.CommentsInfo
import org.schabi.newpipe.extractor.comments.CommentsInfoItem import org.schabi.newpipe.extractor.comments.CommentsInfoItem
import org.schabi.newpipe.ui.components.video.comment.CommentInfo
import org.schabi.newpipe.util.NO_SERVICE_ID import org.schabi.newpipe.util.NO_SERVICE_ID
class CommentsSource( class CommentsSource(
serviceId: Int, serviceId: Int,
private val url: String?, private val url: String,
private val repliesPage: Page? private val repliesPage: Page?,
private val commentInfo: CommentInfo? = null,
) : PagingSource<Page, CommentsInfoItem>() { ) : PagingSource<Page, CommentsInfoItem>() {
constructor(commentInfo: CommentInfo) : this(
commentInfo.serviceId, commentInfo.url, commentInfo.nextPage, commentInfo
)
init { init {
require(serviceId != NO_SERVICE_ID) { "serviceId is NO_SERVICE_ID" } require(serviceId != NO_SERVICE_ID) { "serviceId is NO_SERVICE_ID" }
} }
@ -29,17 +35,11 @@ class CommentsSource(
val info = CommentsInfo.getMoreItems(service, url, it) val info = CommentsInfo.getMoreItems(service, url, it)
LoadResult.Page(info.items, null, info.nextPage) LoadResult.Page(info.items, null, info.nextPage)
} ?: run { } ?: run {
val info = CommentsInfo.getInfo(service, url) val info = commentInfo ?: CommentInfo(CommentsInfo.getInfo(service, url))
if (info.isCommentsDisabled) { LoadResult.Page(info.comments, null, info.nextPage)
LoadResult.Error(CommentsDisabledException())
} else {
LoadResult.Page(info.relatedItems, null, info.nextPage)
}
} }
} }
} }
override fun getRefreshKey(state: PagingState<Page, CommentsInfoItem>) = null override fun getRefreshKey(state: PagingState<Page, CommentsInfoItem>) = null
} }
class CommentsDisabledException : RuntimeException()

View File

@ -166,7 +166,13 @@ fun Comment(comment: CommentsInfoItem) {
.cachedIn(coroutineScope) .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.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection 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.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.LoadStates
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -30,30 +29,59 @@ import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.Page import org.schabi.newpipe.extractor.Page
import org.schabi.newpipe.extractor.comments.CommentsInfoItem import org.schabi.newpipe.extractor.comments.CommentsInfoItem
import org.schabi.newpipe.extractor.stream.Description 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.LoadingIndicator
import org.schabi.newpipe.ui.components.common.NoItemsMessage import org.schabi.newpipe.ui.components.common.NoItemsMessage
import org.schabi.newpipe.ui.theme.AppTheme import org.schabi.newpipe.ui.theme.AppTheme
import org.schabi.newpipe.viewmodels.CommentsViewModel import org.schabi.newpipe.viewmodels.CommentsViewModel
import org.schabi.newpipe.viewmodels.util.Resource
@Composable @Composable
fun CommentSection(commentsViewModel: CommentsViewModel = viewModel()) { 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 @Composable
fun CommentSection( fun CommentSection(
commentsFlow: Flow<PagingData<CommentsInfoItem>>,
commentCount: Int,
parentComment: CommentsInfoItem? = null, parentComment: CommentsInfoItem? = null,
commentsFlow: Flow<PagingData<CommentsInfoItem>> isCommentsDisabled: Boolean = false,
) { ) {
val comments = commentsFlow.collectAsLazyPagingItems() val comments = commentsFlow.collectAsLazyPagingItems()
val itemCount by remember { derivedStateOf { comments.itemCount } }
val nestedScrollInterop = rememberNestedScrollInteropConnection() val nestedScrollInterop = rememberNestedScrollInteropConnection()
val state = rememberLazyListState() val state = rememberLazyListState()
Surface(color = MaterialTheme.colorScheme.background) {
LazyColumnScrollbar(state = state) { LazyColumnScrollbar(state = state) {
LazyColumn(modifier = Modifier.nestedScroll(nestedScrollInterop), state = state) { LazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop),
state = state
) {
if (parentComment != null) { if (parentComment != null) {
item { item {
CommentRepliesHeader(comment = parentComment) CommentRepliesHeader(comment = parentComment)
@ -61,14 +89,15 @@ fun CommentSection(
} }
} }
if (itemCount == 0) { if (comments.itemCount == 0) {
item { item {
val refresh = comments.loadState.refresh val refresh = comments.loadState.refresh
if (refresh is LoadState.Loading) { if (refresh is LoadState.Loading) {
LoadingIndicator(modifier = Modifier.padding(top = 8.dp)) LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
} else { } else {
val error = (refresh as? LoadState.Error)?.error val message = if (refresh is LoadState.Error) {
val message = if (error is CommentsDisabledException) { R.string.error_unable_to_load_comments
} else if (isCommentsDisabled) {
R.string.comments_are_disabled R.string.comments_are_disabled
} else { } else {
R.string.no_comments R.string.no_comments
@ -77,22 +106,41 @@ fun CommentSection(
} }
} }
} else { } else {
items(itemCount) { // The number of replies is already shown in the main comment section
if (parentComment == null) {
item {
Text(
modifier = Modifier.padding(start = 8.dp),
text = pluralStringResource(R.plurals.comments, commentCount, commentCount),
fontWeight = FontWeight.Bold
)
}
}
items(comments.itemCount) {
Comment(comment = comments[it]!!) Comment(comment = comments[it]!!)
} }
} }
} }
} }
} }
@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())
}
}
} }
private class CommentDataProvider : PreviewParameterProvider<PagingData<CommentsInfoItem>> { @Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
private val notLoading = LoadState.NotLoading(true) @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
override val values = sequenceOf( private fun CommentSectionSuccessPreview() {
// Normal view val comments = listOf(
PagingData.from(
listOf(
CommentsInfoItem( CommentsInfoItem(
commentText = Description( commentText = Description(
"Comment 1\n\nThis line should be hidden by default.", "Comment 1\n\nThis line should be hidden by default.",
@ -108,28 +156,30 @@ private class CommentDataProvider : PreviewParameterProvider<PagingData<Comments
uploaderName = "Test" uploaderName = "Test"
) )
} }
),
// Comments disabled AppTheme {
PagingData.from( Surface(color = MaterialTheme.colorScheme.background) {
listOf<CommentsInfoItem>(), CommentSection(
LoadStates(LoadState.Error(CommentsDisabledException()), notLoading, notLoading) uiState = Resource.Success(
), CommentInfo(
// No comments serviceId = 1, url = "", comments = comments, nextPage = null,
PagingData.from( commentCount = 10, isCommentsDisabled = false
listOf<CommentsInfoItem>(),
LoadStates(notLoading, notLoading, notLoading)
) )
),
commentsFlow = flowOf(PagingData.from(comments))
) )
} }
}
}
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable @Composable
private fun CommentSectionPreview( private fun CommentSectionErrorPreview() {
@PreviewParameter(CommentDataProvider::class) pagingData: PagingData<CommentsInfoItem>
) {
AppTheme { AppTheme {
CommentSection(commentsFlow = flowOf(pagingData)) 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)) val flow = flowOf(PagingData.from(replies))
AppTheme { 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.Pager
import androidx.paging.PagingConfig import androidx.paging.PagingConfig
import androidx.paging.cachedIn 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.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.KEY_URL
import org.schabi.newpipe.util.NO_SERVICE_ID import org.schabi.newpipe.viewmodels.util.Resource
class CommentsViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { class CommentsViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
private val serviceId = savedStateHandle[KEY_SERVICE_ID] ?: NO_SERVICE_ID val uiState = savedStateHandle.getStateFlow(KEY_URL, "")
private val url = savedStateHandle.get<String>(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)) { @OptIn(ExperimentalCoroutinesApi::class)
CommentsSource(serviceId, url, null) val comments = uiState
.filterIsInstance<Resource.Success<CommentInfo>>()
.flatMapLatest {
Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
CommentsSource(it.data)
}.flow }.flow
}
.cachedIn(viewModelScope) .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="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="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> <string name="auto_queue_description">Next</string>
<plurals name="comments">
<item quantity="one">%d comment</item>
<item quantity="other">%d comments</item>
</plurals>
</resources> </resources>