Skip to content

Commit dcf003c

Browse files
Start implementing full playlist view, add view model
1 parent 4d7123b commit dcf003c

6 files changed

Lines changed: 195 additions & 8 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package org.schabi.newpipe.compose.playlist
2+
3+
import android.content.res.Configuration
4+
import androidx.compose.foundation.lazy.LazyColumn
5+
import androidx.compose.material3.HorizontalDivider
6+
import androidx.compose.material3.MaterialTheme
7+
import androidx.compose.material3.Surface
8+
import androidx.compose.material3.Text
9+
import androidx.compose.runtime.Composable
10+
import androidx.compose.runtime.collectAsState
11+
import androidx.compose.runtime.getValue
12+
import androidx.compose.ui.tooling.preview.Preview
13+
import androidx.compose.ui.unit.dp
14+
import androidx.lifecycle.SavedStateHandle
15+
import androidx.lifecycle.viewmodel.compose.viewModel
16+
import androidx.paging.compose.collectAsLazyPagingItems
17+
import org.schabi.newpipe.DownloaderImpl
18+
import org.schabi.newpipe.compose.theme.AppTheme
19+
import org.schabi.newpipe.extractor.NewPipe
20+
import org.schabi.newpipe.extractor.ServiceList
21+
import org.schabi.newpipe.util.KEY_SERVICE_ID
22+
import org.schabi.newpipe.util.KEY_URL
23+
import org.schabi.newpipe.viewmodels.PlaylistViewModel
24+
25+
@Composable
26+
fun Playlist(playlistViewModel: PlaylistViewModel = viewModel()) {
27+
val playlistInfo by playlistViewModel.playlistInfo.collectAsState()
28+
val streams = playlistViewModel.streamItems.collectAsLazyPagingItems()
29+
val totalDuration = streams.itemSnapshotList.sumOf { it!!.duration }
30+
31+
playlistInfo?.let {
32+
Surface(color = MaterialTheme.colorScheme.background) {
33+
LazyColumn {
34+
item {
35+
PlaylistHeader(playlistInfo = it, totalDuration = totalDuration)
36+
HorizontalDivider(thickness = 1.dp)
37+
}
38+
39+
items(streams.itemCount) {
40+
Text(text = streams[it]!!.name)
41+
}
42+
}
43+
}
44+
}
45+
}
46+
47+
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
48+
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
49+
@Composable
50+
private fun PlaylistPreview() {
51+
NewPipe.init(DownloaderImpl.init(null))
52+
val params =
53+
mapOf(
54+
KEY_SERVICE_ID to ServiceList.YouTube.serviceId,
55+
KEY_URL to "https://www.youtube.com/playlist?list=PLAIcZs9N4171hRrG_4v32Ca2hLvSuQ6QI",
56+
)
57+
58+
AppTheme {
59+
Playlist(PlaylistViewModel(SavedStateHandle(params)))
60+
}
61+
}

app/src/main/java/org/schabi/newpipe/compose/playlist/PlaylistHeader.kt

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,19 @@ import org.schabi.newpipe.R
3535
import org.schabi.newpipe.compose.theme.AppTheme
3636
import org.schabi.newpipe.compose.util.rememberParsedDescription
3737
import org.schabi.newpipe.error.ErrorUtil
38+
import org.schabi.newpipe.extractor.Image
3839
import org.schabi.newpipe.extractor.NewPipe
3940
import org.schabi.newpipe.extractor.ServiceList
4041
import org.schabi.newpipe.extractor.playlist.PlaylistInfo
4142
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper
4243
import org.schabi.newpipe.extractor.stream.Description
44+
import org.schabi.newpipe.extractor.stream.StreamInfoItem
45+
import org.schabi.newpipe.extractor.stream.StreamType
46+
import org.schabi.newpipe.util.Localization
47+
import org.schabi.newpipe.util.NO_SERVICE_ID
4348
import org.schabi.newpipe.util.NavigationHelper
4449
import org.schabi.newpipe.util.image.ImageStrategy
50+
import java.util.concurrent.TimeUnit
4551

4652
@Composable
4753
fun PlaylistHeader(playlistInfo: PlaylistInfo, totalDuration: Long) {
@@ -106,10 +112,9 @@ fun PlaylistHeader(playlistInfo: PlaylistInfo, totalDuration: Long) {
106112
Text(text = uploader, style = MaterialTheme.typography.bodySmall)
107113
}
108114

109-
Text(
110-
text = playlistInfo.streamCount.toString(),
111-
style = MaterialTheme.typography.bodySmall
112-
)
115+
val count = Localization.localizeStreamCount(context, playlistInfo.streamCount)
116+
val formattedDuration = Localization.getDurationString(totalDuration, true, true)
117+
Text(text = "$count$formattedDuration", style = MaterialTheme.typography.bodySmall)
113118
}
114119

115120
val description = playlistInfo.description ?: Description.EMPTY_DESCRIPTION
@@ -138,10 +143,26 @@ fun PlaylistHeader(playlistInfo: PlaylistInfo, totalDuration: Long) {
138143
}
139144
}
140145

146+
fun StreamInfoItem(
147+
serviceId: Int = NO_SERVICE_ID,
148+
url: String,
149+
name: String,
150+
streamType: StreamType = StreamType.NONE,
151+
uploaderName: String? = null,
152+
uploaderUrl: String? = null,
153+
uploaderAvatars: List<Image> = emptyList(),
154+
duration: Long,
155+
) = StreamInfoItem(serviceId, url, name, streamType).apply {
156+
this.uploaderName = uploaderName
157+
this.uploaderUrl = uploaderUrl
158+
this.uploaderAvatars = uploaderAvatars
159+
this.duration = duration
160+
}
161+
141162
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
142163
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
143164
@Composable
144-
fun PlaylistHeaderPreview() {
165+
private fun PlaylistHeaderPreview() {
145166
NewPipe.init(DownloaderImpl.init(null))
146167
val playlistInfo = PlaylistInfo.getInfo(
147168
ServiceList.YouTube,
@@ -150,7 +171,10 @@ fun PlaylistHeaderPreview() {
150171

151172
AppTheme {
152173
Surface(color = MaterialTheme.colorScheme.background) {
153-
PlaylistHeader(playlistInfo = playlistInfo, totalDuration = 1000)
174+
PlaylistHeader(
175+
playlistInfo = playlistInfo,
176+
totalDuration = TimeUnit.HOURS.toSeconds(1)
177+
)
154178
}
155179
}
156180
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package org.schabi.newpipe.fragments.list.playlist
2+
3+
import android.os.Bundle
4+
import android.view.LayoutInflater
5+
import android.view.View
6+
import android.view.ViewGroup
7+
import androidx.compose.ui.platform.ComposeView
8+
import androidx.compose.ui.platform.ViewCompositionStrategy
9+
import androidx.fragment.app.Fragment
10+
import org.schabi.newpipe.compose.playlist.Playlist
11+
import org.schabi.newpipe.compose.theme.AppTheme
12+
13+
class PlaylistFragment2 : Fragment() {
14+
override fun onCreateView(
15+
inflater: LayoutInflater,
16+
container: ViewGroup?,
17+
savedInstanceState: Bundle?,
18+
): View =
19+
ComposeView(requireContext()).apply {
20+
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
21+
setContent {
22+
AppTheme {
23+
Playlist()
24+
}
25+
}
26+
}
27+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package org.schabi.newpipe.paging
2+
3+
import androidx.paging.PagingSource
4+
import androidx.paging.PagingState
5+
import kotlinx.coroutines.Dispatchers
6+
import kotlinx.coroutines.withContext
7+
import org.schabi.newpipe.extractor.NewPipe
8+
import org.schabi.newpipe.extractor.Page
9+
import org.schabi.newpipe.extractor.playlist.PlaylistInfo
10+
import org.schabi.newpipe.extractor.stream.StreamInfoItem
11+
12+
class PlaylistItemsSource(
13+
private val playlistInfo: PlaylistInfo,
14+
) : PagingSource<Page, StreamInfoItem>() {
15+
private val service = NewPipe.getService(playlistInfo.serviceId)
16+
17+
override suspend fun load(params: LoadParams<Page>): LoadResult<Page, StreamInfoItem> {
18+
return params.key?.let {
19+
withContext(Dispatchers.IO) {
20+
val response = PlaylistInfo.getMoreItems(service, playlistInfo.url, playlistInfo.nextPage)
21+
LoadResult.Page(response.items, null, response.nextPage)
22+
}
23+
} ?: LoadResult.Page(playlistInfo.relatedItems, null, playlistInfo.nextPage)
24+
}
25+
26+
override fun getRefreshKey(state: PagingState<Page, StreamInfoItem>) = null
27+
}

app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import android.content.Intent;
1010
import android.net.Uri;
1111
import android.os.Build;
12+
import android.os.Bundle;
1213
import android.util.Log;
1314
import android.widget.Toast;
1415

@@ -46,7 +47,7 @@
4647
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
4748
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
4849
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
49-
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
50+
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment2;
5051
import org.schabi.newpipe.fragments.list.search.SearchFragment;
5152
import org.schabi.newpipe.local.bookmark.BookmarkFragment;
5253
import org.schabi.newpipe.local.feed.FeedFragment;
@@ -503,8 +504,12 @@ public static void openCommentAuthorIfPresent(@NonNull final FragmentActivity ac
503504
public static void openPlaylistFragment(final FragmentManager fragmentManager,
504505
final int serviceId, final String url,
505506
@NonNull final String name) {
507+
final var args = new Bundle();
508+
args.putInt(Constants.KEY_SERVICE_ID, serviceId);
509+
args.putString(Constants.KEY_URL, url);
510+
506511
defaultTransaction(fragmentManager)
507-
.replace(R.id.fragment_holder, PlaylistFragment.getInstance(serviceId, url, name))
512+
.replace(R.id.fragment_holder, PlaylistFragment2.class, args)
508513
.addToBackStack(null)
509514
.commit();
510515
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org.schabi.newpipe.viewmodels
2+
3+
import androidx.lifecycle.SavedStateHandle
4+
import androidx.lifecycle.ViewModel
5+
import androidx.lifecycle.viewModelScope
6+
import androidx.paging.Pager
7+
import androidx.paging.PagingConfig
8+
import androidx.paging.cachedIn
9+
import kotlinx.coroutines.Dispatchers
10+
import kotlinx.coroutines.ExperimentalCoroutinesApi
11+
import kotlinx.coroutines.flow.SharingStarted
12+
import kotlinx.coroutines.flow.combine
13+
import kotlinx.coroutines.flow.filterNotNull
14+
import kotlinx.coroutines.flow.flatMapLatest
15+
import kotlinx.coroutines.flow.flowOn
16+
import kotlinx.coroutines.flow.stateIn
17+
import org.schabi.newpipe.extractor.NewPipe
18+
import org.schabi.newpipe.extractor.playlist.PlaylistInfo
19+
import org.schabi.newpipe.paging.PlaylistItemsSource
20+
import org.schabi.newpipe.util.KEY_SERVICE_ID
21+
import org.schabi.newpipe.util.KEY_URL
22+
import org.schabi.newpipe.util.NO_SERVICE_ID
23+
24+
class PlaylistViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
25+
private val serviceIdState = savedStateHandle.getStateFlow(KEY_SERVICE_ID, NO_SERVICE_ID)
26+
private val urlState = savedStateHandle.getStateFlow(KEY_URL, "")
27+
28+
val playlistInfo = serviceIdState.combine(urlState) { id, url ->
29+
PlaylistInfo.getInfo(NewPipe.getService(id), url)
30+
}
31+
.flowOn(Dispatchers.IO)
32+
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
33+
34+
@OptIn(ExperimentalCoroutinesApi::class)
35+
val streamItems = playlistInfo
36+
.filterNotNull()
37+
.flatMapLatest {
38+
Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
39+
PlaylistItemsSource(it)
40+
}.flow
41+
}
42+
.cachedIn(viewModelScope)
43+
}

0 commit comments

Comments
 (0)