mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 15:23:00 +00:00 
			
		
		
		
	Start implementing full playlist view, add view model
This commit is contained in:
		| @@ -0,0 +1,61 @@ | ||||
| package org.schabi.newpipe.compose.playlist | ||||
|  | ||||
| import android.content.res.Configuration | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.material3.HorizontalDivider | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Surface | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.lifecycle.SavedStateHandle | ||||
| import androidx.lifecycle.viewmodel.compose.viewModel | ||||
| import androidx.paging.compose.collectAsLazyPagingItems | ||||
| import org.schabi.newpipe.DownloaderImpl | ||||
| 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.viewmodels.PlaylistViewModel | ||||
|  | ||||
| @Composable | ||||
| fun Playlist(playlistViewModel: PlaylistViewModel = viewModel()) { | ||||
|     val playlistInfo by playlistViewModel.playlistInfo.collectAsState() | ||||
|     val streams = playlistViewModel.streamItems.collectAsLazyPagingItems() | ||||
|     val totalDuration = streams.itemSnapshotList.sumOf { it!!.duration } | ||||
|  | ||||
|     playlistInfo?.let { | ||||
|         Surface(color = MaterialTheme.colorScheme.background) { | ||||
|             LazyColumn { | ||||
|                 item { | ||||
|                     PlaylistHeader(playlistInfo = it, totalDuration = totalDuration) | ||||
|                     HorizontalDivider(thickness = 1.dp) | ||||
|                 } | ||||
|  | ||||
|                 items(streams.itemCount) { | ||||
|                     Text(text = streams[it]!!.name) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @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", | ||||
|         ) | ||||
|  | ||||
|     AppTheme { | ||||
|         Playlist(PlaylistViewModel(SavedStateHandle(params))) | ||||
|     } | ||||
| } | ||||
| @@ -35,13 +35,19 @@ 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.Image | ||||
| 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.extractor.stream.StreamInfoItem | ||||
| import org.schabi.newpipe.extractor.stream.StreamType | ||||
| import org.schabi.newpipe.util.Localization | ||||
| import org.schabi.newpipe.util.NO_SERVICE_ID | ||||
| import org.schabi.newpipe.util.NavigationHelper | ||||
| import org.schabi.newpipe.util.image.ImageStrategy | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
| @Composable | ||||
| fun PlaylistHeader(playlistInfo: PlaylistInfo, totalDuration: Long) { | ||||
| @@ -106,10 +112,9 @@ fun PlaylistHeader(playlistInfo: PlaylistInfo, totalDuration: Long) { | ||||
|                 Text(text = uploader, style = MaterialTheme.typography.bodySmall) | ||||
|             } | ||||
|  | ||||
|             Text( | ||||
|                 text = playlistInfo.streamCount.toString(), | ||||
|                 style = MaterialTheme.typography.bodySmall | ||||
|             ) | ||||
|             val count = Localization.localizeStreamCount(context, playlistInfo.streamCount) | ||||
|             val formattedDuration = Localization.getDurationString(totalDuration, true, true) | ||||
|             Text(text = "$count • $formattedDuration", style = MaterialTheme.typography.bodySmall) | ||||
|         } | ||||
|  | ||||
|         val description = playlistInfo.description ?: Description.EMPTY_DESCRIPTION | ||||
| @@ -143,10 +148,26 @@ fun PlaylistHeader(playlistInfo: PlaylistInfo, totalDuration: Long) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| fun StreamInfoItem( | ||||
|     serviceId: Int = NO_SERVICE_ID, | ||||
|     url: String, | ||||
|     name: String, | ||||
|     streamType: StreamType = StreamType.NONE, | ||||
|     uploaderName: String? = null, | ||||
|     uploaderUrl: String? = null, | ||||
|     uploaderAvatars: List<Image> = emptyList(), | ||||
|     duration: Long, | ||||
| ) = StreamInfoItem(serviceId, url, name, streamType).apply { | ||||
|     this.uploaderName = uploaderName | ||||
|     this.uploaderUrl = uploaderUrl | ||||
|     this.uploaderAvatars = uploaderAvatars | ||||
|     this.duration = duration | ||||
| } | ||||
|  | ||||
| @Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) | ||||
| @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) | ||||
| @Composable | ||||
| fun PlaylistHeaderPreview() { | ||||
| private fun PlaylistHeaderPreview() { | ||||
|     NewPipe.init(DownloaderImpl.init(null)) | ||||
|     val playlistInfo = PlaylistInfo.getInfo( | ||||
|         ServiceList.YouTube, | ||||
| @@ -155,7 +176,10 @@ fun PlaylistHeaderPreview() { | ||||
|  | ||||
|     AppTheme { | ||||
|         Surface(color = MaterialTheme.colorScheme.background) { | ||||
|             PlaylistHeader(playlistInfo = playlistInfo, totalDuration = 1000) | ||||
|             PlaylistHeader( | ||||
|                 playlistInfo = playlistInfo, | ||||
|                 totalDuration = TimeUnit.HOURS.toSeconds(1) | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,27 @@ | ||||
| package org.schabi.newpipe.fragments.list.playlist | ||||
|  | ||||
| import android.os.Bundle | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import androidx.compose.ui.platform.ComposeView | ||||
| import androidx.compose.ui.platform.ViewCompositionStrategy | ||||
| import androidx.fragment.app.Fragment | ||||
| import org.schabi.newpipe.compose.playlist.Playlist | ||||
| import org.schabi.newpipe.compose.theme.AppTheme | ||||
|  | ||||
| class PlaylistFragment2 : Fragment() { | ||||
|     override fun onCreateView( | ||||
|         inflater: LayoutInflater, | ||||
|         container: ViewGroup?, | ||||
|         savedInstanceState: Bundle?, | ||||
|     ): View = | ||||
|         ComposeView(requireContext()).apply { | ||||
|             setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) | ||||
|             setContent { | ||||
|                 AppTheme { | ||||
|                     Playlist() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| } | ||||
| @@ -0,0 +1,27 @@ | ||||
| package org.schabi.newpipe.paging | ||||
|  | ||||
| import androidx.paging.PagingSource | ||||
| import androidx.paging.PagingState | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.withContext | ||||
| 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 | ||||
|  | ||||
| class PlaylistItemsSource( | ||||
|     private val playlistInfo: PlaylistInfo, | ||||
| ) : PagingSource<Page, StreamInfoItem>() { | ||||
|     private val service = NewPipe.getService(playlistInfo.serviceId) | ||||
|  | ||||
|     override suspend fun load(params: LoadParams<Page>): LoadResult<Page, StreamInfoItem> { | ||||
|         return params.key?.let { | ||||
|             withContext(Dispatchers.IO) { | ||||
|                 val response = PlaylistInfo.getMoreItems(service, playlistInfo.url, playlistInfo.nextPage) | ||||
|                 LoadResult.Page(response.items, null, response.nextPage) | ||||
|             } | ||||
|         } ?: LoadResult.Page(playlistInfo.relatedItems, null, playlistInfo.nextPage) | ||||
|     } | ||||
|  | ||||
|     override fun getRefreshKey(state: PagingState<Page, StreamInfoItem>) = null | ||||
| } | ||||
| @@ -9,6 +9,7 @@ import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.net.Uri; | ||||
| import android.os.Build; | ||||
| import android.os.Bundle; | ||||
| import android.util.Log; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| @@ -46,7 +47,7 @@ import org.schabi.newpipe.fragments.MainFragment; | ||||
| import org.schabi.newpipe.fragments.detail.VideoDetailFragment; | ||||
| import org.schabi.newpipe.fragments.list.channel.ChannelFragment; | ||||
| import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; | ||||
| import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; | ||||
| import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment2; | ||||
| import org.schabi.newpipe.fragments.list.search.SearchFragment; | ||||
| import org.schabi.newpipe.local.bookmark.BookmarkFragment; | ||||
| import org.schabi.newpipe.local.feed.FeedFragment; | ||||
| @@ -503,8 +504,12 @@ public final class NavigationHelper { | ||||
|     public static void openPlaylistFragment(final FragmentManager fragmentManager, | ||||
|                                             final int serviceId, final String url, | ||||
|                                             @NonNull final String name) { | ||||
|         final var args = new Bundle(); | ||||
|         args.putInt(Constants.KEY_SERVICE_ID, serviceId); | ||||
|         args.putString(Constants.KEY_URL, url); | ||||
|  | ||||
|         defaultTransaction(fragmentManager) | ||||
|                 .replace(R.id.fragment_holder, PlaylistFragment.getInstance(serviceId, url, name)) | ||||
|                 .replace(R.id.fragment_holder, PlaylistFragment2.class, args) | ||||
|                 .addToBackStack(null) | ||||
|                 .commit(); | ||||
|     } | ||||
|   | ||||
| @@ -0,0 +1,43 @@ | ||||
| package org.schabi.newpipe.viewmodels | ||||
|  | ||||
| import androidx.lifecycle.SavedStateHandle | ||||
| import androidx.lifecycle.ViewModel | ||||
| import androidx.lifecycle.viewModelScope | ||||
| import androidx.paging.Pager | ||||
| import androidx.paging.PagingConfig | ||||
| import androidx.paging.cachedIn | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.ExperimentalCoroutinesApi | ||||
| import kotlinx.coroutines.flow.SharingStarted | ||||
| import kotlinx.coroutines.flow.combine | ||||
| import kotlinx.coroutines.flow.filterNotNull | ||||
| import kotlinx.coroutines.flow.flatMapLatest | ||||
| import kotlinx.coroutines.flow.flowOn | ||||
| import kotlinx.coroutines.flow.stateIn | ||||
| import org.schabi.newpipe.extractor.NewPipe | ||||
| import org.schabi.newpipe.extractor.playlist.PlaylistInfo | ||||
| 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 | ||||
|  | ||||
| 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) | ||||
|     } | ||||
|         .flowOn(Dispatchers.IO) | ||||
|         .stateIn(viewModelScope, SharingStarted.Eagerly, null) | ||||
|  | ||||
|     @OptIn(ExperimentalCoroutinesApi::class) | ||||
|     val streamItems = playlistInfo | ||||
|         .filterNotNull() | ||||
|         .flatMapLatest { | ||||
|             Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) { | ||||
|                 PlaylistItemsSource(it) | ||||
|             }.flow | ||||
|         } | ||||
|         .cachedIn(viewModelScope) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Isira Seneviratne
					Isira Seneviratne