1
0
mirror of https://github.com/TeamNewPipe/NewPipe.git synced 2024-11-25 04:22:30 +01:00

Improve loading indicator positioning

This commit is contained in:
Isira Seneviratne 2024-08-30 16:59:15 +05:30
parent 62d4044d6c
commit 823d4a041f
4 changed files with 204 additions and 126 deletions

View File

@ -3,6 +3,8 @@ package org.schabi.newpipe.fragments.list.comments
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.compose.content import androidx.fragment.compose.content
@ -18,7 +20,9 @@ class CommentsFragment : Fragment() {
savedInstanceState: Bundle? savedInstanceState: Bundle?
) = content { ) = content {
AppTheme { AppTheme {
CommentSection() Surface(color = MaterialTheme.colorScheme.background) {
CommentSection()
}
} }
} }

View File

@ -15,9 +15,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@ -25,7 +23,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -42,22 +39,18 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.cachedIn
import coil.compose.AsyncImage import coil.compose.AsyncImage
import org.schabi.newpipe.R 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.CommentsSource
import org.schabi.newpipe.ui.components.common.rememberParsedDescription import org.schabi.newpipe.ui.components.common.rememberParsedDescription
import org.schabi.newpipe.ui.theme.AppTheme import org.schabi.newpipe.ui.theme.AppTheme
import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.NavigationHelper import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.image.ImageStrategy import org.schabi.newpipe.util.image.ImageStrategy
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun Comment(comment: CommentsInfoItem) { fun Comment(comment: CommentsInfoItem) {
val clipboardManager = LocalClipboardManager.current val clipboardManager = LocalClipboardManager.current
@ -157,23 +150,7 @@ fun Comment(comment: CommentsInfoItem) {
} }
if (showReplies) { if (showReplies) {
ModalBottomSheet(onDismissRequest = { showReplies = false }) { CommentRepliesDialog(comment, onDismissRequest = { showReplies = false })
val coroutineScope = rememberCoroutineScope()
val flow = remember {
Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
CommentsSource(comment.serviceId, comment.url, comment.replies)
}.flow
.cachedIn(coroutineScope)
}
Surface(color = MaterialTheme.colorScheme.background) {
CommentSection(
commentsFlow = flow,
commentCount = comment.replyCount,
parentComment = comment
)
}
}
} }
} }

View File

@ -0,0 +1,132 @@
package org.schabi.newpipe.ui.components.video.comment
import android.content.res.Configuration
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.compose.collectAsLazyPagingItems
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import my.nanihadesuka.compose.LazyColumnScrollbar
import my.nanihadesuka.compose.ScrollbarSettings
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
import org.schabi.newpipe.extractor.stream.Description
import org.schabi.newpipe.paging.CommentsSource
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.ui.theme.md_theme_dark_primary
@Composable
fun CommentRepliesDialog(
parentComment: CommentsInfoItem,
onDismissRequest: () -> Unit,
) {
val coroutineScope = rememberCoroutineScope()
val commentsFlow = remember {
Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
CommentsSource(parentComment.serviceId, parentComment.url, parentComment.replies)
}.flow
.cachedIn(coroutineScope)
}
CommentRepliesDialog(parentComment, commentsFlow, onDismissRequest)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CommentRepliesDialog(
parentComment: CommentsInfoItem,
commentsFlow: Flow<PagingData<CommentsInfoItem>>,
onDismissRequest: () -> Unit,
) {
val comments = commentsFlow.collectAsLazyPagingItems()
val nestedScrollInterop = rememberNestedScrollInteropConnection()
val state = rememberLazyListState()
ModalBottomSheet(onDismissRequest = onDismissRequest) {
Surface(color = MaterialTheme.colorScheme.background) {
LazyColumnScrollbar(
state = state,
settings = ScrollbarSettings.Default.copy(
thumbSelectedColor = md_theme_dark_primary,
thumbUnselectedColor = Color.Red
)
) {
LazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop),
state = state
) {
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 {
R.string.no_comments
}
NoItemsMessage(message)
}
}
} else {
items(comments.itemCount) {
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 CommentRepliesDialogPreview() {
val comment = CommentsInfoItem(
commentText = Description("Hello world!", Description.PLAIN_TEXT),
uploaderName = "Test",
likeCount = 100,
isPinned = true,
isHeartedByUploader = true
)
val replies = (1..10).map {
CommentsInfoItem(
commentText = Description("Reply $it", Description.PLAIN_TEXT),
uploaderName = "Test"
)
}
val flow = flowOf(PagingData.from(replies))
AppTheme {
CommentRepliesDialog(comment, flow, onDismissRequest = {})
}
}

View File

@ -4,7 +4,6 @@ import android.content.res.Configuration
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
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.material3.Text
@ -40,94 +39,86 @@ import org.schabi.newpipe.viewmodels.util.Resource
@Composable @Composable
fun CommentSection(commentsViewModel: CommentsViewModel = viewModel()) { fun CommentSection(commentsViewModel: CommentsViewModel = viewModel()) {
Surface(color = MaterialTheme.colorScheme.background) { val state by commentsViewModel.uiState.collectAsStateWithLifecycle()
val state by commentsViewModel.uiState.collectAsStateWithLifecycle() CommentSection(state, commentsViewModel.comments)
CommentSection(state, commentsViewModel.comments)
}
} }
@Composable @Composable
private fun CommentSection( private fun CommentSection(
uiState: Resource<CommentInfo>, uiState: Resource<CommentInfo>,
commentsFlow: Flow<PagingData<CommentsInfoItem>> 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,
isCommentsDisabled: Boolean = false,
) { ) {
val comments = commentsFlow.collectAsLazyPagingItems() val comments = commentsFlow.collectAsLazyPagingItems()
val nestedScrollInterop = rememberNestedScrollInteropConnection() val nestedScrollInterop = rememberNestedScrollInteropConnection()
val state = rememberLazyListState() val state = rememberLazyListState()
LazyColumnScrollbar( Surface(color = MaterialTheme.colorScheme.background) {
state = state, LazyColumnScrollbar(
settings = ScrollbarSettings.Default.copy( state = state,
thumbSelectedColor = md_theme_dark_primary, settings = ScrollbarSettings.Default.copy(
thumbUnselectedColor = Color.Red thumbSelectedColor = md_theme_dark_primary,
) thumbUnselectedColor = Color.Red
) { )
LazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop),
state = state
) { ) {
if (parentComment != null) { LazyColumn(
item { modifier = Modifier.nestedScroll(nestedScrollInterop),
CommentRepliesHeader(comment = parentComment) state = state
HorizontalDivider(thickness = 1.dp) ) {
} when (uiState) {
} is Resource.Loading -> {
item {
if (comments.itemCount == 0) { LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
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 {
Text(
modifier = Modifier.padding(start = 8.dp),
text = pluralStringResource(R.plurals.comments, commentCount, commentCount),
fontWeight = FontWeight.Bold
)
}
}
items(comments.itemCount) { is Resource.Success -> {
Comment(comment = comments[it]!!) val commentInfo = uiState.data
val count = commentInfo.commentCount
if (commentInfo.isCommentsDisabled) {
item {
NoItemsMessage(R.string.comments_are_disabled)
}
} else if (count == 0) {
item {
NoItemsMessage(R.string.no_comments)
}
} else {
item {
Text(
modifier = Modifier.padding(start = 8.dp),
text = pluralStringResource(R.plurals.comments, count, count),
fontWeight = FontWeight.Bold
)
}
when (comments.loadState.refresh) {
is LoadState.Loading -> {
item {
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
}
}
is LoadState.Error -> {
item {
NoItemsMessage(R.string.error_unable_to_load_comments)
}
}
else -> {
items(comments.itemCount) {
Comment(comment = comments[it]!!)
}
}
}
}
}
is Resource.Error -> {
item {
NoItemsMessage(R.string.error_unable_to_load_comments)
}
}
} }
} }
} }
@ -191,29 +182,3 @@ private fun CommentSectionErrorPreview() {
} }
} }
} }
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun CommentRepliesPreview() {
val comment = CommentsInfoItem(
commentText = Description("Hello world!", Description.PLAIN_TEXT),
uploaderName = "Test",
likeCount = 100,
isPinned = true,
isHeartedByUploader = true
)
val replies = (1..10).map {
CommentsInfoItem(
commentText = Description("Reply $it", Description.PLAIN_TEXT),
uploaderName = "Test"
)
}
val flow = flowOf(PagingData.from(replies))
AppTheme {
Surface(color = MaterialTheme.colorScheme.background) {
CommentSection(parentComment = comment, commentsFlow = flow, commentCount = 10)
}
}
}