Create playlist header composable

This commit is contained in:
Isira Seneviratne 2024-06-28 11:08:16 +05:30
parent 10dd5710ee
commit 68b3dd5546
4 changed files with 173 additions and 16 deletions

View File

@ -0,0 +1,161 @@
package org.schabi.newpipe.compose.playlist
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
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.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.NavigationHelper
import org.schabi.newpipe.util.image.ImageStrategy
@Composable
fun PlaylistHeader(playlistInfo: PlaylistInfo, totalDuration: Long) {
val context = LocalContext.current
Column(
modifier = Modifier.padding(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = playlistInfo.name,
style = MaterialTheme.typography.titleMedium
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.apply {
if (playlistInfo.uploaderName != null && playlistInfo.uploaderUrl != null) {
clickable {
try {
NavigationHelper.openChannelFragment(
(context as FragmentActivity).supportFragmentManager,
playlistInfo.serviceId, playlistInfo.uploaderUrl,
playlistInfo.uploaderName
)
} catch (e: Exception) {
ErrorUtil.showUiErrorSnackbar(context, "Opening channel fragment", e)
}
}
}
}
) {
val imageModifier = Modifier
.size(24.dp)
.clip(CircleShape)
val isMix = YoutubeParsingHelper.isYoutubeMixId(playlistInfo.id) ||
YoutubeParsingHelper.isYoutubeMusicMixId(playlistInfo.id)
if (playlistInfo.serviceId == ServiceList.YouTube.serviceId && isMix) {
Image(
painter = painterResource(R.drawable.ic_radio),
contentDescription = null,
modifier = imageModifier
)
} else {
AsyncImage(
model = ImageStrategy.choosePreferredImage(playlistInfo.uploaderAvatars),
contentDescription = null,
placeholder = painterResource(R.drawable.placeholder_person),
error = painterResource(R.drawable.placeholder_person),
modifier = imageModifier
)
}
val uploader = playlistInfo.uploaderName.orEmpty()
.ifEmpty { stringResource(R.string.playlist_no_uploader) }
Text(text = uploader, style = MaterialTheme.typography.bodySmall)
}
Text(
text = playlistInfo.streamCount.toString(),
style = MaterialTheme.typography.bodySmall
)
}
val description = playlistInfo.description ?: Description.EMPTY_DESCRIPTION
if (description != Description.EMPTY_DESCRIPTION) {
var isExpanded by rememberSaveable { mutableStateOf(false) }
var isExpandable by rememberSaveable { mutableStateOf(false) }
DescriptionText(
description = description,
maxLines = if (isExpanded) Int.MAX_VALUE else 5,
style = MaterialTheme.typography.bodyMedium,
overflow = TextOverflow.Ellipsis,
onTextLayout = {
if (it.hasVisualOverflow) {
isExpandable = true
}
}
)
if (isExpandable) {
TextButton(
onClick = { isExpanded = !isExpanded },
modifier = Modifier.align(Alignment.End)
) {
Text(
text = stringResource(if (isExpanded) R.string.show_less else R.string.show_more)
)
}
}
}
}
}
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun PlaylistHeaderPreview() {
NewPipe.init(DownloaderImpl.init(null))
val playlistInfo = PlaylistInfo.getInfo(
ServiceList.YouTube,
"https://www.youtube.com/playlist?list=PLAIcZs9N4171hRrG_4v32Ca2hLvSuQ6QI"
)
AppTheme {
Surface(color = MaterialTheme.colorScheme.background) {
PlaylistHeader(playlistInfo = playlistInfo, totalDuration = 1000)
}
}
}

View File

@ -500,7 +500,7 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
final boolean isDurationComplete) {
if (activity != null && headerBinding != null) {
playlistOverallDurationSeconds += list.stream()
.mapToLong(x -> x.getDuration())
.mapToLong(StreamInfoItem::getDuration)
.sum();
headerBinding.playlistStreamCount.setText(
Localization.concatenateStrings(

View File

@ -15,7 +15,6 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
@ -36,10 +35,10 @@ import org.schabi.newpipe.local.holder.LocalBookmarkPlaylistItemHolder;
import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.util.debounce.DebounceSavable;
import org.schabi.newpipe.util.debounce.DebounceSaver;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.debounce.DebounceSavable;
import org.schabi.newpipe.util.debounce.DebounceSaver;
import java.util.ArrayList;
import java.util.List;
@ -134,20 +133,14 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
itemListAdapter.setSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final LocalItem selectedItem) {
final FragmentManager fragmentManager = getFM();
final var fragmentManager = getFM();
if (selectedItem instanceof PlaylistMetadataEntry) {
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
if (selectedItem instanceof PlaylistMetadataEntry entry) {
NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.getUid(),
entry.name);
} else if (selectedItem instanceof PlaylistRemoteEntity) {
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
NavigationHelper.openPlaylistFragment(
fragmentManager,
entry.getServiceId(),
entry.getUrl(),
entry.getName());
} else if (selectedItem instanceof PlaylistRemoteEntity entry) {
NavigationHelper.openPlaylistFragment(fragmentManager, entry.getServiceId(),
entry.getUrl(), entry.getName());
}
}

View File

@ -8,6 +8,7 @@ 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
@ -21,6 +22,7 @@ fun DescriptionText(
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.
@ -38,6 +40,7 @@ fun DescriptionText(
text = parsedDescription,
maxLines = maxLines,
style = style,
overflow = overflow
overflow = overflow,
onTextLayout = onTextLayout
)
}