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.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)
|
||||
}
|
Loading…
Reference in New Issue