Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate playlist fragment to Jetpack Compose #11259

Draft
wants to merge 61 commits into
base: refactor
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
08f0338
Convert comment replies views to Jetpack Compose
Isira-Seneviratne May 12, 2024
c429deb
Rename .java to .kt
Isira-Seneviratne May 12, 2024
e193265
Use reply header composable in fragment
Isira-Seneviratne May 12, 2024
fc8a748
Added like count
Isira-Seneviratne May 12, 2024
b302c1b
Added missing comment features, fixed theming
Isira-Seneviratne May 17, 2024
b14da3a
Add comment ellipsis
Isira-Seneviratne Jun 16, 2024
65fce6e
Update replies fragment to use the comment composable as well
Isira-Seneviratne Jun 18, 2024
f056edd
Improve previews, display date of comment
Isira-Seneviratne Jun 18, 2024
e75eb2d
Fixed some comment issues
Isira-Seneviratne Jun 18, 2024
c29fa70
Use AnnotatedString to handle HTML parsing
Isira-Seneviratne Jun 19, 2024
e71fc38
Add replies button
Isira-Seneviratne Jun 19, 2024
b6a0f5d
Set view strategy
Isira-Seneviratne Jun 20, 2024
552ea50
Fixed like count display
Isira-Seneviratne Jun 20, 2024
cf3fb0b
Fixed fragment title
Isira-Seneviratne Jun 21, 2024
f27273e
Rename .java to .kt
Isira-Seneviratne Jun 21, 2024
03bc4e2
Migrate comments fragment to Jetpack Compose
Isira-Seneviratne Jun 21, 2024
ff88184
Added scrollbar to comment section
Isira-Seneviratne Jun 21, 2024
754bf45
Replace CommentRepliesFragment with bottom sheet composable, improve …
Isira-Seneviratne Jun 23, 2024
cc6f1ff
Replace Spacers with the horizontalArrangement parameter
Isira-Seneviratne Jun 23, 2024
8d4c608
Handle no comments and comments disabled scenarios
Isira-Seneviratne Jun 23, 2024
e87a2e0
Rm redundant Surface
Isira-Seneviratne Jun 26, 2024
219da28
Improve code organization
Isira-Seneviratne Jun 28, 2024
975a341
Cache paging data using the cachedIn() extension
Isira-Seneviratne Jun 30, 2024
94ef79c
Add comment view model
Isira-Seneviratne Jul 2, 2024
21b22d3
Rm extra padding in header
Isira-Seneviratne Jul 5, 2024
4b13e30
Replace padding modifier with verticalArrangement in comment header
Isira-Seneviratne Jul 5, 2024
1c503ce
Added loading indicator
Isira-Seneviratne Jul 6, 2024
ddbfcf8
Rm unused method
Isira-Seneviratne Jul 8, 2024
3c55d95
Improve comment loading smoothness
Isira-Seneviratne Jul 8, 2024
f438ba4
Animate comment expand/collapse
Isira-Seneviratne Jul 9, 2024
b83f643
Make parsed links clickable, visible
Isira-Seneviratne Jul 10, 2024
0d9c4aa
Fix alignment of comment message
Isira-Seneviratne Jul 12, 2024
f285bc0
Fix some modifiers
Isira-Seneviratne Jul 16, 2024
f1591ab
Added DescriptionText composable
Isira-Seneviratne Jul 25, 2024
b2748cc
Improved component organisation
Isira-Seneviratne Jul 28, 2024
10dd571
Update Kotlin to 2.0, update dependencies and fix issues
Isira-Seneviratne Jul 28, 2024
68b3dd5
Create playlist header composable
Isira-Seneviratne Jun 28, 2024
8603b0d
Start implementing full playlist view, add view model
Isira-Seneviratne Jun 28, 2024
bf1c9ba
Start implementing stream composable, grid layout
Isira-Seneviratne Jul 1, 2024
72bbe0e
Implement card and list layouts, check for preferred layout from sett…
Isira-Seneviratne Jul 2, 2024
37e4064
Cache total duration calculation
Isira-Seneviratne Jul 2, 2024
462ed5c
Added loading indicator to playlist view
Isira-Seneviratne Jul 6, 2024
5e33b69
Moved stream display to separate composable for reusability
Isira-Seneviratne Jul 8, 2024
bbdff4b
Show dropdown menu on long click, make some adjustments
Isira-Seneviratne Jul 9, 2024
7501f2f
Animate playlist description expand/collapse
Isira-Seneviratne Jul 9, 2024
c978fb7
Fix stream thumbnail text color
Isira-Seneviratne Jul 12, 2024
3da4aee
Rename .java to .kt
Isira-Seneviratne Jul 13, 2024
9dfd064
Remove old playlist fragment
Isira-Seneviratne Jul 13, 2024
b9556a1
Improved PlaylistHeader
Isira-Seneviratne Jul 16, 2024
82e5b6b
Remove playlist preview dependency on external HTTP calls
Isira-Seneviratne Jul 22, 2024
d7de38c
Remove TextEllipsizer
Isira-Seneviratne Jul 25, 2024
b443abb
Improved component organisation
Isira-Seneviratne Jul 28, 2024
06a5828
Improved stream components
Isira-Seneviratne Jul 28, 2024
8e8f627
Dismiss popup menu on clicking an option
Isira-Seneviratne Jul 28, 2024
4453061
Added stream grid mini preview
Isira-Seneviratne Jul 28, 2024
2f9364a
Fix crash when opening YouTube mixes
Isira-Seneviratne Jul 29, 2024
75475da
Improve StreamThumbnail composable
Isira-Seneviratne Jul 29, 2024
a885a88
Add duration comment
Isira-Seneviratne Jul 29, 2024
aec18c7
Move item view mode composable
Isira-Seneviratne Aug 1, 2024
52a2acc
Merge branch 'refactor' into Playlist-Compose
Isira-Seneviratne Dec 26, 2024
b644160
Use UI state classes in playlist
Isira-Seneviratne Dec 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/src/main/java/org/schabi/newpipe/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,7 @@ private void handleIntent(final Intent intent) {
break;
case PLAYLIST:
NavigationHelper.openPlaylistFragment(getSupportFragmentManager(),
serviceId, url, title);
serviceId, url);
break;
}
} else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ public void held(final StreamInfoItem selectedItem) {
try {
onItemSelected(selectedItem);
NavigationHelper.openPlaylistFragment(getFM(), selectedItem.getServiceId(),
selectedItem.getUrl(), selectedItem.getName());
selectedItem.getUrl());
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Opening playlist fragment", e);
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.schabi.newpipe.fragments.list.playlist

import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.compose.content
import org.schabi.newpipe.ui.screens.PlaylistScreen
import org.schabi.newpipe.ui.theme.AppTheme
import org.schabi.newpipe.util.KEY_SERVICE_ID
import org.schabi.newpipe.util.KEY_URL

class PlaylistFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
) = content {
AppTheme {
PlaylistScreen()
}
}

companion object {
@JvmStatic
fun getInstance(serviceId: Int, url: String?) = PlaylistFragment().apply {
arguments = bundleOf(KEY_SERVICE_ID to serviceId, KEY_URL to url)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/
public enum ItemViewMode {
/**
* Default mode.
* View mode is automatically determined based on the device configuration.
*/
AUTO,
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;

Expand Down Expand Up @@ -141,20 +140,14 @@ protected void initListeners() {
itemListAdapter.setSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final LocalItem selectedItem) {
final FragmentManager fragmentManager = getFM();
final var fragmentManager = getFM();

if (selectedItem instanceof PlaylistMetadataEntry) {
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
if (selectedItem instanceof PlaylistMetadataEntry entry) {
NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.getUid(),
entry.name);

} else if (selectedItem instanceof PlaylistRemoteEntity) {
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
NavigationHelper.openPlaylistFragment(
fragmentManager,
entry.getServiceId(),
entry.getUrl(),
entry.getName());
} else if (selectedItem instanceof PlaylistRemoteEntity entry) {
NavigationHelper.openPlaylistFragment(fragmentManager, entry.getServiceId(),
entry.getUrl());
}
}

Expand Down
29 changes: 29 additions & 0 deletions app/src/main/java/org/schabi/newpipe/paging/PlaylistItemsSource.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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.stream.StreamInfoItem
import org.schabi.newpipe.ui.components.playlist.PlaylistInfo
import org.schabi.newpipe.extractor.playlist.PlaylistInfo as ExtractorPlaylistInfo

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 = ExtractorPlaylistInfo
.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
}
7 changes: 2 additions & 5 deletions app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java
Original file line number Diff line number Diff line change
Expand Up @@ -577,9 +577,8 @@ public int getTabIconRes(final Context context) {
public Fragment getFragment(final Context context) {
if (playlistType == LocalItemType.PLAYLIST_LOCAL_ITEM) {
return LocalPlaylistFragment.getInstance(playlistId, playlistName);

} else { // playlistType == LocalItemType.PLAYLIST_REMOTE_ITEM
return PlaylistFragment.getInstance(playlistServiceId, playlistUrl, playlistName);
return PlaylistFragment.getInstance(playlistServiceId, playlistUrl);
}
}

Expand All @@ -606,12 +605,10 @@ protected void readDataFromJson(final JsonObject jsonObject) {

@Override
public boolean equals(final Object obj) {
if (!(obj instanceof PlaylistTab)) {
if (!(obj instanceof PlaylistTab other)) {
return false;
}

final PlaylistTab other = (PlaylistTab) obj;

return super.equals(obj)
&& playlistServiceId == other.playlistServiceId // Remote
&& playlistId == other.playlistId // Local
Expand Down
211 changes: 211 additions & 0 deletions app/src/main/java/org/schabi/newpipe/ui/components/comment/Comment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package org.schabi.newpipe.ui.components.comment

import android.content.res.Configuration
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentActivity
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.cachedIn
import coil3.compose.AsyncImage
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.Page
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
import org.schabi.newpipe.extractor.stream.Description
import org.schabi.newpipe.paging.CommentRepliesSource
import org.schabi.newpipe.ui.components.common.DescriptionText
import org.schabi.newpipe.ui.theme.AppTheme
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.image.ImageStrategy

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Comment(comment: CommentsInfoItem) {
val context = LocalContext.current
var isExpanded by rememberSaveable { mutableStateOf(false) }
var showReplies by rememberSaveable { mutableStateOf(false) }

Row(
modifier = Modifier
.animateContentSize()
.clickable { isExpanded = !isExpanded }
.padding(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
if (ImageStrategy.shouldLoadImages()) {
AsyncImage(
model = ImageStrategy.choosePreferredImage(comment.uploaderAvatars),
contentDescription = null,
placeholder = painterResource(R.drawable.placeholder_person),
error = painterResource(R.drawable.placeholder_person),
modifier = Modifier
.size(42.dp)
.clip(CircleShape)
.clickable {
NavigationHelper.openCommentAuthorIfPresent(
context as FragmentActivity, comment
)
}
)
}

Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
if (comment.isPinned) {
Image(
painter = painterResource(R.drawable.ic_pin),
contentDescription = stringResource(R.string.detail_pinned_comment_view_description)
)
}

val nameAndDate = remember(comment) {
val date = Localization.relativeTimeOrTextual(
context, comment.uploadDate, comment.textualUploadDate
)
Localization.concatenateStrings(comment.uploaderName, date)
}
Text(text = nameAndDate, color = MaterialTheme.colorScheme.secondary)
}

DescriptionText(
description = comment.commentText,
// If the comment is expanded, we display all its content
// otherwise we only display the first two lines
maxLines = if (isExpanded) Int.MAX_VALUE else 2,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium,
)

Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Image(
painter = painterResource(R.drawable.ic_thumb_up),
contentDescription = stringResource(R.string.detail_likes_img_view_description)
)
Text(text = Localization.likeCount(context, comment.likeCount))

if (comment.isHeartedByUploader) {
Image(
painter = painterResource(R.drawable.ic_heart),
contentDescription = stringResource(R.string.detail_heart_img_view_description)
)
}
}

if (comment.replies != null) {
TextButton(onClick = { showReplies = true }) {
val text = pluralStringResource(
R.plurals.replies, comment.replyCount, comment.replyCount.toString()
)
Text(text = text)
}
}
}
}
}

if (showReplies) {
ModalBottomSheet(onDismissRequest = { showReplies = false }) {
val coroutineScope = rememberCoroutineScope()
val flow = remember {
Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
CommentRepliesSource(comment)
}.flow
.cachedIn(coroutineScope)
}

CommentSection(parentComment = comment, commentsFlow = flow)
}
}
}

fun CommentsInfoItem(
serviceId: Int = 1,
url: String = "",
name: String = "",
commentText: Description,
uploaderName: String,
textualUploadDate: String = "5 months ago",
likeCount: Int = 0,
isHeartedByUploader: Boolean = false,
isPinned: Boolean = false,
replies: Page? = null,
replyCount: Int = 0,
) = CommentsInfoItem(serviceId, url, name).apply {
this.commentText = commentText
this.uploaderName = uploaderName
this.textualUploadDate = textualUploadDate
this.likeCount = likeCount
this.isHeartedByUploader = isHeartedByUploader
this.isPinned = isPinned
this.replies = replies
this.replyCount = replyCount
}

private class DescriptionPreviewProvider : PreviewParameterProvider<Description> {
override val values = sequenceOf(
Description("Hello world!<br><br>This line should be hidden by default.", Description.HTML),
Description("Hello world!\n\nThis line should be hidden by default.", Description.PLAIN_TEXT),
)
}

@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun CommentPreview(
@PreviewParameter(DescriptionPreviewProvider::class) description: Description
) {
val comment = CommentsInfoItem(
commentText = description,
uploaderName = "Test",
likeCount = 100,
isPinned = true,
isHeartedByUploader = true,
replies = Page(""),
replyCount = 10
)

AppTheme {
Surface(color = MaterialTheme.colorScheme.background) {
Comment(comment)
}
}
}
Loading
Loading