From d9a06a8acb23b4631a8a7ff4995b654ae77a1986 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Mon, 22 Jul 2024 08:11:16 +0530 Subject: [PATCH] Add DescriptionText composable, remove playlist preview dependency on external HTTP calls --- .../schabi/newpipe/compose/comment/Comment.kt | 6 +- .../compose/comment/CommentRepliesHeader.kt | 6 +- .../newpipe/compose/comment/CommentSection.kt | 2 +- .../newpipe/compose/common/DescriptionText.kt | 46 +++++++++++ .../{status => common}/LoadingIndicator.kt | 6 +- .../newpipe/compose/playlist/Playlist.kt | 76 +++++++++++-------- .../compose/playlist/PlaylistHeader.kt | 22 +++--- .../newpipe/compose/playlist/PlaylistInfo.kt | 22 ++++++ .../newpipe/compose/stream/StreamList.kt | 6 +- .../org/schabi/newpipe/compose/util/Utils.kt | 21 ----- .../newpipe/paging/PlaylistItemsSource.kt | 6 +- .../newpipe/viewmodels/PlaylistViewModel.kt | 12 ++- 12 files changed, 148 insertions(+), 83 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/compose/common/DescriptionText.kt rename app/src/main/java/org/schabi/newpipe/compose/{status => common}/LoadingIndicator.kt (79%) create mode 100644 app/src/main/java/org/schabi/newpipe/compose/playlist/PlaylistInfo.kt diff --git a/app/src/main/java/org/schabi/newpipe/compose/comment/Comment.kt b/app/src/main/java/org/schabi/newpipe/compose/comment/Comment.kt index a5bf00da5b4..00f07932f1c 100644 --- a/app/src/main/java/org/schabi/newpipe/compose/comment/Comment.kt +++ b/app/src/main/java/org/schabi/newpipe/compose/comment/Comment.kt @@ -42,8 +42,8 @@ import androidx.paging.PagingConfig import androidx.paging.cachedIn import coil.compose.AsyncImage import org.schabi.newpipe.R +import org.schabi.newpipe.compose.common.DescriptionText import org.schabi.newpipe.compose.theme.AppTheme -import org.schabi.newpipe.compose.util.rememberParsedDescription import org.schabi.newpipe.extractor.Page import org.schabi.newpipe.extractor.comments.CommentsInfoItem import org.schabi.newpipe.extractor.stream.Description @@ -101,8 +101,8 @@ fun Comment(comment: CommentsInfoItem) { Text(text = nameAndDate, color = MaterialTheme.colorScheme.secondary) } - Text( - text = rememberParsedDescription(comment.commentText), + DescriptionText( + description = comment.commentText, // If the comment is expanded, we display all its content // otherwise we only display the first two lines maxLines = if (isExpanded) Int.MAX_VALUE else 2, diff --git a/app/src/main/java/org/schabi/newpipe/compose/comment/CommentRepliesHeader.kt b/app/src/main/java/org/schabi/newpipe/compose/comment/CommentRepliesHeader.kt index 52a360c0c21..d4e4fd36c77 100644 --- a/app/src/main/java/org/schabi/newpipe/compose/comment/CommentRepliesHeader.kt +++ b/app/src/main/java/org/schabi/newpipe/compose/comment/CommentRepliesHeader.kt @@ -25,8 +25,8 @@ import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentActivity import coil.compose.AsyncImage import org.schabi.newpipe.R +import org.schabi.newpipe.compose.common.DescriptionText import org.schabi.newpipe.compose.theme.AppTheme -import org.schabi.newpipe.compose.util.rememberParsedDescription import org.schabi.newpipe.extractor.comments.CommentsInfoItem import org.schabi.newpipe.extractor.stream.Description import org.schabi.newpipe.util.Localization @@ -102,8 +102,8 @@ fun CommentRepliesHeader(comment: CommentsInfoItem) { } } - Text( - text = rememberParsedDescription(comment.commentText), + DescriptionText( + description = comment.commentText, style = MaterialTheme.typography.bodyMedium ) } diff --git a/app/src/main/java/org/schabi/newpipe/compose/comment/CommentSection.kt b/app/src/main/java/org/schabi/newpipe/compose/comment/CommentSection.kt index 7a9579fb1ce..5d5ad3b768a 100644 --- a/app/src/main/java/org/schabi/newpipe/compose/comment/CommentSection.kt +++ b/app/src/main/java/org/schabi/newpipe/compose/comment/CommentSection.kt @@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import my.nanihadesuka.compose.LazyColumnScrollbar import org.schabi.newpipe.R -import org.schabi.newpipe.compose.status.LoadingIndicator +import org.schabi.newpipe.compose.common.LoadingIndicator import org.schabi.newpipe.compose.theme.AppTheme import org.schabi.newpipe.extractor.comments.CommentsInfoItem import org.schabi.newpipe.extractor.stream.Description diff --git a/app/src/main/java/org/schabi/newpipe/compose/common/DescriptionText.kt b/app/src/main/java/org/schabi/newpipe/compose/common/DescriptionText.kt new file mode 100644 index 00000000000..29168c298aa --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/compose/common/DescriptionText.kt @@ -0,0 +1,46 @@ +package org.schabi.newpipe.compose.common + +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.ParagraphStyle +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.fromHtml +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import org.schabi.newpipe.extractor.stream.Description + +@Composable +fun DescriptionText( + description: Description, + modifier: Modifier = Modifier, + overflow: TextOverflow = TextOverflow.Clip, + maxLines: Int = Int.MAX_VALUE, + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current +) { + // TODO: Handle links and hashtags, Markdown. + val parsedDescription = remember(description) { + if (description.type == Description.HTML) { + val styles = TextLinkStyles(SpanStyle(textDecoration = TextDecoration.Underline)) + AnnotatedString.fromHtml(description.content, styles) + } else { + AnnotatedString(description.content, ParagraphStyle()) + } + } + + Text( + modifier = modifier, + text = parsedDescription, + maxLines = maxLines, + style = style, + overflow = overflow, + onTextLayout = onTextLayout + ) +} diff --git a/app/src/main/java/org/schabi/newpipe/compose/status/LoadingIndicator.kt b/app/src/main/java/org/schabi/newpipe/compose/common/LoadingIndicator.kt similarity index 79% rename from app/src/main/java/org/schabi/newpipe/compose/status/LoadingIndicator.kt rename to app/src/main/java/org/schabi/newpipe/compose/common/LoadingIndicator.kt index 8bed6f8c8fe..b5fc65413cd 100644 --- a/app/src/main/java/org/schabi/newpipe/compose/status/LoadingIndicator.kt +++ b/app/src/main/java/org/schabi/newpipe/compose/common/LoadingIndicator.kt @@ -1,4 +1,4 @@ -package org.schabi.newpipe.compose.status +package org.schabi.newpipe.compose.common import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.wrapContentSize @@ -11,7 +11,9 @@ import androidx.compose.ui.Modifier @Composable fun LoadingIndicator(modifier: Modifier = Modifier) { CircularProgressIndicator( - modifier = modifier.fillMaxSize().wrapContentSize(Alignment.Center), + modifier = modifier + .fillMaxSize() + .wrapContentSize(Alignment.Center), color = MaterialTheme.colorScheme.primary, trackColor = MaterialTheme.colorScheme.surfaceVariant, ) diff --git a/app/src/main/java/org/schabi/newpipe/compose/playlist/Playlist.kt b/app/src/main/java/org/schabi/newpipe/compose/playlist/Playlist.kt index 87f9eef6627..7d649e49c63 100644 --- a/app/src/main/java/org/schabi/newpipe/compose/playlist/Playlist.kt +++ b/app/src/main/java/org/schabi/newpipe/compose/playlist/Playlist.kt @@ -10,62 +10,72 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.tooling.preview.Preview -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.paging.PagingData import androidx.paging.compose.collectAsLazyPagingItems -import org.schabi.newpipe.DownloaderImpl -import org.schabi.newpipe.compose.status.LoadingIndicator +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import org.schabi.newpipe.compose.common.LoadingIndicator +import org.schabi.newpipe.compose.stream.StreamInfoItem import org.schabi.newpipe.compose.stream.StreamList import org.schabi.newpipe.compose.theme.AppTheme -import org.schabi.newpipe.extractor.NewPipe -import org.schabi.newpipe.extractor.ServiceList -import org.schabi.newpipe.util.KEY_SERVICE_ID -import org.schabi.newpipe.util.KEY_URL +import org.schabi.newpipe.extractor.stream.Description +import org.schabi.newpipe.extractor.stream.StreamInfoItem +import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.viewmodels.PlaylistViewModel @Composable fun Playlist(playlistViewModel: PlaylistViewModel = viewModel()) { Surface(color = MaterialTheme.colorScheme.background) { val playlistInfo by playlistViewModel.playlistInfo.collectAsState() + Playlist(playlistInfo, playlistViewModel.streamItems) + } +} - playlistInfo?.let { - val streams = playlistViewModel.streamItems.collectAsLazyPagingItems() - val totalDuration by remember { - derivedStateOf { - streams.itemSnapshotList.sumOf { it!!.duration } - } +@Composable +private fun Playlist( + playlistInfo: PlaylistInfo?, + streamFlow: Flow> +) { + playlistInfo?.let { + val streams = streamFlow.collectAsLazyPagingItems() + val totalDuration by remember { + derivedStateOf { + streams.itemSnapshotList.sumOf { it!!.duration } } + } - StreamList( - streams = streams, - gridHeader = { - item(span = { GridItemSpan(maxLineSpan) }) { - PlaylistHeader(it, totalDuration) - } - }, - listHeader = { - item { - PlaylistHeader(it, totalDuration) - } + StreamList( + streams = streams, + gridHeader = { + item(span = { GridItemSpan(maxLineSpan) }) { + PlaylistHeader(it, totalDuration) } - ) - } ?: LoadingIndicator() - } + }, + listHeader = { + item { + PlaylistHeader(it, totalDuration) + } + } + ) + } ?: LoadingIndicator() } @Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun PlaylistPreview() { - NewPipe.init(DownloaderImpl.init(null)) - - val params = mapOf( - KEY_SERVICE_ID to ServiceList.YouTube.serviceId, - KEY_URL to "https://www.youtube.com/playlist?list=PLAIcZs9N4171hRrG_4v32Ca2hLvSuQ6QI" + val description = Description("Example description", Description.PLAIN_TEXT) + val playlistInfo = PlaylistInfo( + "", 1, "", "Example playlist", description, listOf(), 1L, + null, "Uploader", listOf(), null ) + val stream = StreamInfoItem(streamType = StreamType.VIDEO_STREAM) + val streamFlow = flowOf(PagingData.from(listOf(stream))) + AppTheme { Surface(color = MaterialTheme.colorScheme.background) { - Playlist(PlaylistViewModel(SavedStateHandle(params))) + Playlist(playlistInfo, streamFlow) } } } diff --git a/app/src/main/java/org/schabi/newpipe/compose/playlist/PlaylistHeader.kt b/app/src/main/java/org/schabi/newpipe/compose/playlist/PlaylistHeader.kt index 88e29d2ad48..fca84c0de37 100644 --- a/app/src/main/java/org/schabi/newpipe/compose/playlist/PlaylistHeader.kt +++ b/app/src/main/java/org/schabi/newpipe/compose/playlist/PlaylistHeader.kt @@ -34,14 +34,11 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentActivity import coil.compose.AsyncImage -import org.schabi.newpipe.DownloaderImpl import org.schabi.newpipe.R +import org.schabi.newpipe.compose.common.DescriptionText import org.schabi.newpipe.compose.theme.AppTheme -import org.schabi.newpipe.compose.util.rememberParsedDescription import org.schabi.newpipe.error.ErrorUtil -import org.schabi.newpipe.extractor.NewPipe import org.schabi.newpipe.extractor.ServiceList -import org.schabi.newpipe.extractor.playlist.PlaylistInfo import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper import org.schabi.newpipe.extractor.stream.Description import org.schabi.newpipe.util.Localization @@ -67,7 +64,7 @@ fun PlaylistHeader(playlistInfo: PlaylistInfo, totalDuration: Long) { NavigationHelper.openChannelFragment( (context as FragmentActivity).supportFragmentManager, playlistInfo.serviceId, playlistInfo.uploaderUrl, - playlistInfo.uploaderName + playlistInfo.uploaderName!! ) } catch (e: Exception) { ErrorUtil.showUiErrorSnackbar(context, "Opening channel fragment", e) @@ -108,14 +105,13 @@ fun PlaylistHeader(playlistInfo: PlaylistInfo, totalDuration: Long) { Text(text = "$count • $formattedDuration", style = MaterialTheme.typography.bodySmall) } - val description = playlistInfo.description ?: Description.EMPTY_DESCRIPTION - if (description != Description.EMPTY_DESCRIPTION) { + if (playlistInfo.description != Description.EMPTY_DESCRIPTION) { var isExpanded by rememberSaveable { mutableStateOf(false) } var isExpandable by rememberSaveable { mutableStateOf(false) } - Text( + DescriptionText( modifier = Modifier.animateContentSize(), - text = rememberParsedDescription(description), + description = playlistInfo.description, maxLines = if (isExpanded) Int.MAX_VALUE else 5, style = MaterialTheme.typography.bodyMedium, overflow = TextOverflow.Ellipsis, @@ -144,10 +140,10 @@ fun PlaylistHeader(playlistInfo: PlaylistInfo, totalDuration: Long) { @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun PlaylistHeaderPreview() { - NewPipe.init(DownloaderImpl.init(null)) - val playlistInfo = PlaylistInfo.getInfo( - ServiceList.YouTube, - "https://www.youtube.com/playlist?list=PLAIcZs9N4171hRrG_4v32Ca2hLvSuQ6QI" + val description = Description("Example description", Description.PLAIN_TEXT) + val playlistInfo = PlaylistInfo( + "", 1, "", "Example playlist", description, listOf(), 1L, + null, "Uploader", listOf(), null ) AppTheme { diff --git a/app/src/main/java/org/schabi/newpipe/compose/playlist/PlaylistInfo.kt b/app/src/main/java/org/schabi/newpipe/compose/playlist/PlaylistInfo.kt new file mode 100644 index 00000000000..ded4d7b7ecf --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/compose/playlist/PlaylistInfo.kt @@ -0,0 +1,22 @@ +package org.schabi.newpipe.compose.playlist + +import androidx.compose.runtime.Immutable +import org.schabi.newpipe.extractor.Image +import org.schabi.newpipe.extractor.Page +import org.schabi.newpipe.extractor.stream.Description +import org.schabi.newpipe.extractor.stream.StreamInfoItem + +@Immutable +class PlaylistInfo( + val id: String, + val serviceId: Int, + val url: String, + val name: String, + val description: Description, + val relatedItems: List, + val streamCount: Long, + val uploaderUrl: String?, + val uploaderName: String?, + val uploaderAvatars: List, + val nextPage: Page? +) diff --git a/app/src/main/java/org/schabi/newpipe/compose/stream/StreamList.kt b/app/src/main/java/org/schabi/newpipe/compose/stream/StreamList.kt index e9c161d812b..bcf9fc62eb7 100644 --- a/app/src/main/java/org/schabi/newpipe/compose/stream/StreamList.kt +++ b/app/src/main/java/org/schabi/newpipe/compose/stream/StreamList.kt @@ -26,10 +26,10 @@ import org.schabi.newpipe.util.NavigationHelper @Composable fun StreamList( streams: LazyPagingItems, + itemViewMode: ItemViewMode = determineItemViewMode(), gridHeader: LazyGridScope.() -> Unit = {}, listHeader: LazyListScope.() -> Unit = {} ) { - val mode = determineItemViewMode() val context = LocalContext.current val onClick = remember { { stream: StreamInfoItem -> @@ -54,7 +54,7 @@ fun StreamList( } } - if (mode == ItemViewMode.GRID) { + if (itemViewMode == ItemViewMode.GRID) { val gridState = rememberLazyGridState() LazyVerticalGridScrollbar(state = gridState) { @@ -82,7 +82,7 @@ fun StreamList( val stream = streams[it]!! val isSelected = selectedStream == stream - if (mode == ItemViewMode.CARD) { + if (itemViewMode == ItemViewMode.CARD) { StreamCardItem(stream, isSelected, onClick, onLongClick, onDismissPopup) } else { StreamListItem(stream, isSelected, onClick, onLongClick, onDismissPopup) diff --git a/app/src/main/java/org/schabi/newpipe/compose/util/Utils.kt b/app/src/main/java/org/schabi/newpipe/compose/util/Utils.kt index c30a56a302e..90a513eefdf 100644 --- a/app/src/main/java/org/schabi/newpipe/compose/util/Utils.kt +++ b/app/src/main/java/org/schabi/newpipe/compose/util/Utils.kt @@ -2,33 +2,12 @@ package org.schabi.newpipe.compose.util import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.ParagraphStyle -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextLinkStyles -import androidx.compose.ui.text.fromHtml -import androidx.compose.ui.text.style.TextDecoration import androidx.preference.PreferenceManager import androidx.window.core.layout.WindowWidthSizeClass import org.schabi.newpipe.R -import org.schabi.newpipe.extractor.stream.Description import org.schabi.newpipe.info_list.ItemViewMode -@Composable -fun rememberParsedDescription(description: Description): AnnotatedString { - // TODO: Handle links and hashtags, Markdown. - return remember(description) { - if (description.type == Description.HTML) { - val styles = TextLinkStyles(SpanStyle(textDecoration = TextDecoration.Underline)) - AnnotatedString.fromHtml(description.content, styles) - } else { - AnnotatedString(description.content, ParagraphStyle()) - } - } -} - @Composable fun determineItemViewMode(): ItemViewMode { val context = LocalContext.current diff --git a/app/src/main/java/org/schabi/newpipe/paging/PlaylistItemsSource.kt b/app/src/main/java/org/schabi/newpipe/paging/PlaylistItemsSource.kt index 400b5ab349d..369ede2bfdb 100644 --- a/app/src/main/java/org/schabi/newpipe/paging/PlaylistItemsSource.kt +++ b/app/src/main/java/org/schabi/newpipe/paging/PlaylistItemsSource.kt @@ -4,10 +4,11 @@ import androidx.paging.PagingSource import androidx.paging.PagingState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.schabi.newpipe.compose.playlist.PlaylistInfo import org.schabi.newpipe.extractor.NewPipe import org.schabi.newpipe.extractor.Page -import org.schabi.newpipe.extractor.playlist.PlaylistInfo import org.schabi.newpipe.extractor.stream.StreamInfoItem +import org.schabi.newpipe.extractor.playlist.PlaylistInfo as ExtractorPlaylistInfo class PlaylistItemsSource( private val playlistInfo: PlaylistInfo, @@ -17,7 +18,8 @@ class PlaylistItemsSource( override suspend fun load(params: LoadParams): LoadResult { return params.key?.let { withContext(Dispatchers.IO) { - val response = PlaylistInfo.getMoreItems(service, playlistInfo.url, playlistInfo.nextPage) + val response = ExtractorPlaylistInfo + .getMoreItems(service, playlistInfo.url, playlistInfo.nextPage) LoadResult.Page(response.items, null, response.nextPage) } } ?: LoadResult.Page(playlistInfo.relatedItems, null, playlistInfo.nextPage) diff --git a/app/src/main/java/org/schabi/newpipe/viewmodels/PlaylistViewModel.kt b/app/src/main/java/org/schabi/newpipe/viewmodels/PlaylistViewModel.kt index d45c393276f..7e480277fee 100644 --- a/app/src/main/java/org/schabi/newpipe/viewmodels/PlaylistViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/viewmodels/PlaylistViewModel.kt @@ -14,19 +14,27 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn +import org.schabi.newpipe.compose.playlist.PlaylistInfo import org.schabi.newpipe.extractor.NewPipe -import org.schabi.newpipe.extractor.playlist.PlaylistInfo +import org.schabi.newpipe.extractor.stream.Description import org.schabi.newpipe.paging.PlaylistItemsSource import org.schabi.newpipe.util.KEY_SERVICE_ID import org.schabi.newpipe.util.KEY_URL import org.schabi.newpipe.util.NO_SERVICE_ID +import org.schabi.newpipe.extractor.playlist.PlaylistInfo as ExtractorPlaylistInfo class PlaylistViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private val serviceIdState = savedStateHandle.getStateFlow(KEY_SERVICE_ID, NO_SERVICE_ID) private val urlState = savedStateHandle.getStateFlow(KEY_URL, "") val playlistInfo = serviceIdState.combine(urlState) { id, url -> - PlaylistInfo.getInfo(NewPipe.getService(id), url) + val info = ExtractorPlaylistInfo.getInfo(NewPipe.getService(id), url) + val description = info.description ?: Description.EMPTY_DESCRIPTION + PlaylistInfo( + info.id, info.serviceId, info.url, info.name, description, info.relatedItems, + info.streamCount, info.uploaderUrl, info.uploaderName, info.uploaderAvatars, + info.nextPage + ) } .flowOn(Dispatchers.IO) .stateIn(viewModelScope, SharingStarted.Eagerly, null)