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 new file mode 100644 index 000000000..bf0b788ae --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/compose/playlist/PlaylistHeader.kt @@ -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) + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index d4607a9ff..74ce5c627 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -500,7 +500,7 @@ public class PlaylistFragment extends BaseListInfoFragment x.getDuration()) + .mapToLong(StreamInfoItem::getDuration) .sum(); headerBinding.playlistStreamCount.setText( Localization.concatenateStrings( diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index a366723e0..e21e14500 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -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() { @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()); } } diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/common/DescriptionText.kt b/app/src/main/java/org/schabi/newpipe/ui/components/common/DescriptionText.kt index 59ce07c94..3127794f5 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/common/DescriptionText.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/common/DescriptionText.kt @@ -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 ) }