mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 15:23:00 +00:00 
			
		
		
		
	Create playlist header composable
This commit is contained in:
		| @@ -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) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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( | ||||
|   | ||||
| @@ -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()); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|     ) | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Isira Seneviratne
					Isira Seneviratne