Start implementing full playlist view, add view model

This commit is contained in:
Isira Seneviratne 2024-06-28 21:44:33 +05:30
parent 68b3dd5546
commit 8603b0df6e
6 changed files with 195 additions and 8 deletions

View File

@ -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)))
}
}

View File

@ -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)
)
}
}
}

View File

@ -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()
}
}
}
}

View File

@ -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
}

View File

@ -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();
}

View File

@ -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)
}