mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-06-27 23:52:53 +00:00
Start implementing full playlist view, add view model
This commit is contained in:
parent
68b3dd5546
commit
8603b0df6e
@ -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.common.DescriptionText
|
||||||
import org.schabi.newpipe.compose.theme.AppTheme
|
import org.schabi.newpipe.compose.theme.AppTheme
|
||||||
import org.schabi.newpipe.error.ErrorUtil
|
import org.schabi.newpipe.error.ErrorUtil
|
||||||
|
import org.schabi.newpipe.extractor.Image
|
||||||
import org.schabi.newpipe.extractor.NewPipe
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
import org.schabi.newpipe.extractor.ServiceList
|
import org.schabi.newpipe.extractor.ServiceList
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfo
|
||||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper
|
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper
|
||||||
import org.schabi.newpipe.extractor.stream.Description
|
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.NavigationHelper
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PlaylistHeader(playlistInfo: PlaylistInfo, totalDuration: Long) {
|
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 = uploader, style = MaterialTheme.typography.bodySmall)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
val count = Localization.localizeStreamCount(context, playlistInfo.streamCount)
|
||||||
text = playlistInfo.streamCount.toString(),
|
val formattedDuration = Localization.getDurationString(totalDuration, true, true)
|
||||||
style = MaterialTheme.typography.bodySmall
|
Text(text = "$count • $formattedDuration", style = MaterialTheme.typography.bodySmall)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val description = playlistInfo.description ?: Description.EMPTY_DESCRIPTION
|
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 = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
@Composable
|
@Composable
|
||||||
fun PlaylistHeaderPreview() {
|
private fun PlaylistHeaderPreview() {
|
||||||
NewPipe.init(DownloaderImpl.init(null))
|
NewPipe.init(DownloaderImpl.init(null))
|
||||||
val playlistInfo = PlaylistInfo.getInfo(
|
val playlistInfo = PlaylistInfo.getInfo(
|
||||||
ServiceList.YouTube,
|
ServiceList.YouTube,
|
||||||
@ -155,7 +176,10 @@ fun PlaylistHeaderPreview() {
|
|||||||
|
|
||||||
AppTheme {
|
AppTheme {
|
||||||
Surface(color = MaterialTheme.colorScheme.background) {
|
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.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.widget.Toast;
|
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.detail.VideoDetailFragment;
|
||||||
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
|
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
|
||||||
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
|
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.fragments.list.search.SearchFragment;
|
||||||
import org.schabi.newpipe.local.bookmark.BookmarkFragment;
|
import org.schabi.newpipe.local.bookmark.BookmarkFragment;
|
||||||
import org.schabi.newpipe.local.feed.FeedFragment;
|
import org.schabi.newpipe.local.feed.FeedFragment;
|
||||||
@ -503,8 +504,12 @@ public final class NavigationHelper {
|
|||||||
public static void openPlaylistFragment(final FragmentManager fragmentManager,
|
public static void openPlaylistFragment(final FragmentManager fragmentManager,
|
||||||
final int serviceId, final String url,
|
final int serviceId, final String url,
|
||||||
@NonNull final String name) {
|
@NonNull final String name) {
|
||||||
|
final var args = new Bundle();
|
||||||
|
args.putInt(Constants.KEY_SERVICE_ID, serviceId);
|
||||||
|
args.putString(Constants.KEY_URL, url);
|
||||||
|
|
||||||
defaultTransaction(fragmentManager)
|
defaultTransaction(fragmentManager)
|
||||||
.replace(R.id.fragment_holder, PlaylistFragment.getInstance(serviceId, url, name))
|
.replace(R.id.fragment_holder, PlaylistFragment2.class, args)
|
||||||
.addToBackStack(null)
|
.addToBackStack(null)
|
||||||
.commit();
|
.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)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user