From 8603b0df6e4e0c1116836537f2c0946969f95f26 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Fri, 28 Jun 2024 21:44:33 +0530 Subject: [PATCH] Start implementing full playlist view, add view model --- .../newpipe/compose/playlist/Playlist.kt | 61 +++++++++++++++++++ .../compose/playlist/PlaylistHeader.kt | 36 +++++++++-- .../list/playlist/PlaylistFragment2.kt | 27 ++++++++ .../newpipe/paging/PlaylistItemsSource.kt | 27 ++++++++ .../schabi/newpipe/util/NavigationHelper.java | 9 ++- .../newpipe/viewmodels/PlaylistViewModel.kt | 43 +++++++++++++ 6 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/compose/playlist/Playlist.kt create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment2.kt create mode 100644 app/src/main/java/org/schabi/newpipe/paging/PlaylistItemsSource.kt create mode 100644 app/src/main/java/org/schabi/newpipe/viewmodels/PlaylistViewModel.kt diff --git a/app/src/main/java/org/schabi/newpipe/compose/playlist/Playlist.kt b/app/src/main/java/org/schabi/newpipe/compose/playlist/Playlist.kt new file mode 100644 index 000000000..941c2ce5a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/compose/playlist/Playlist.kt @@ -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))) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/compose/playlist/PlaylistHeader.kt b/app/src/main/java/org/schabi/newpipe/compose/playlist/PlaylistHeader.kt index bf0b788ae..882809342 100644 --- a/app/src/main/java/org/schabi/newpipe/compose/playlist/PlaylistHeader.kt +++ b/app/src/main/java/org/schabi/newpipe/compose/playlist/PlaylistHeader.kt @@ -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 = 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) + ) } } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment2.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment2.kt new file mode 100644 index 000000000..a0c42fd0c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment2.kt @@ -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() + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/paging/PlaylistItemsSource.kt b/app/src/main/java/org/schabi/newpipe/paging/PlaylistItemsSource.kt new file mode 100644 index 000000000..400b5ab34 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/paging/PlaylistItemsSource.kt @@ -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() { + private val service = NewPipe.getService(playlistInfo.serviceId) + + override suspend fun load(params: LoadParams): LoadResult { + 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) = null +} diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index e6af64ffe..4cf6271ca 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -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(); } diff --git a/app/src/main/java/org/schabi/newpipe/viewmodels/PlaylistViewModel.kt b/app/src/main/java/org/schabi/newpipe/viewmodels/PlaylistViewModel.kt new file mode 100644 index 000000000..d45c39327 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/viewmodels/PlaylistViewModel.kt @@ -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) +}