Display number of comments
This commit is contained in:
parent
4cac111b66
commit
3785404618
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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>()
|
||||
}
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue