diff --git a/app/build.gradle b/app/build.gradle index 5b5b25350..200af2209 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -106,7 +106,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion = "1.5.13" + kotlinCompilerExtensionVersion = "1.5.14" } } @@ -289,11 +289,15 @@ dependencies { implementation "org.ocpsoft.prettytime:prettytime:5.0.8.Final" // Jetpack Compose - implementation(platform('androidx.compose:compose-bom:2024.05.00')) + implementation(platform('androidx.compose:compose-bom:2024.06.00')) implementation 'androidx.compose.material3:material3' implementation 'androidx.activity:activity-compose' implementation 'androidx.compose.ui:ui-tooling-preview' + // Paging + implementation 'androidx.paging:paging-rxjava3:3.3.0' + implementation 'androidx.paging:paging-compose:3.3.0' + /** Debugging **/ // Memory leak detection debugImplementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}" diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 175694125..d288ab4c6 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -66,7 +66,6 @@ import org.schabi.newpipe.databinding.ToolbarLayoutBinding; import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance; import org.schabi.newpipe.fragments.BackPressable; @@ -868,10 +867,9 @@ public class MainActivity extends AppCompatActivity { } // the root comment is the comment for which the user opened the replies page - @Nullable final CommentRepliesFragment repliesFragment = - (CommentRepliesFragment) fm.findFragmentByTag(CommentRepliesFragment.TAG); - @Nullable final CommentsInfoItem rootComment = - repliesFragment == null ? null : repliesFragment.getCommentsInfoItem(); + final var repliesFragment = (CommentRepliesFragment) + fm.findFragmentByTag(CommentRepliesFragment.TAG); + final var rootComment = repliesFragment == null ? null : repliesFragment.getComment(); // sometimes this function pops the backstack, other times it's handled by the system if (popBackStack) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/Comment.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/Comment.kt index 8b1726fff..bdffa8561 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/Comment.kt +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/Comment.kt @@ -73,6 +73,7 @@ fun Comment(comment: CommentsInfoItem) { color = MaterialTheme.colorScheme.secondary ) + // TODO: Handle HTML and Markdown formats. Text( text = comment.commentText.content, // If the comment is expanded, we display all its content @@ -110,16 +111,33 @@ fun Comment(comment: CommentsInfoItem) { } } +fun CommentsInfoItem( + serviceId: Int = 1, + url: String = "", + name: String = "", + commentText: Description, + uploaderName: String, + textualUploadDate: String = "5 months ago", + likeCount: Int = 100, + isHeartedByUploader: Boolean = true, + isPinned: Boolean = true, +) = CommentsInfoItem(serviceId, url, name).apply { + this.commentText = commentText + this.uploaderName = uploaderName + this.textualUploadDate = textualUploadDate + this.likeCount = likeCount + this.isHeartedByUploader = isHeartedByUploader + this.isPinned = isPinned +} + @Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -fun CommentPreview() { - val comment = CommentsInfoItem(1, "", "") - comment.commentText = Description("Hello world!\n\nThis line should be hidden by default.", Description.PLAIN_TEXT) - comment.uploaderName = "Test" - comment.likeCount = 100 - comment.isHeartedByUploader = true - comment.isPinned = true +private fun CommentPreview() { + val comment = CommentsInfoItem( + commentText = Description("Hello world!\n\nThis line should be hidden by default.", Description.PLAIN_TEXT), + uploaderName = "Test", + ) AppTheme { Comment(comment) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentReplies.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentReplies.kt new file mode 100644 index 000000000..05a6d1c3b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentReplies.kt @@ -0,0 +1,60 @@ +package org.schabi.newpipe.fragments.list.comments + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.paging.PagingData +import androidx.paging.compose.collectAsLazyPagingItems +import io.reactivex.rxjava3.disposables.CompositeDisposable +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import org.schabi.newpipe.extractor.comments.CommentsInfoItem +import org.schabi.newpipe.extractor.stream.Description +import org.schabi.newpipe.ui.theme.AppTheme + +@Composable +fun CommentReplies( + comment: CommentsInfoItem, + flow: Flow>, + disposables: CompositeDisposable +) { + val replies = flow.collectAsLazyPagingItems() + + Column { + CommentRepliesHeader(comment = comment, disposables = disposables) + HorizontalDivider(thickness = 1.dp) + LazyColumn { + items(replies.itemCount) { + Comment(comment = replies[it]!!) + } + } + } +} + +@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun CommentRepliesPreview() { + val comment = CommentsInfoItem( + commentText = Description("Hello world!", Description.PLAIN_TEXT), + uploaderName = "Test", + ) + + val reply1 = CommentsInfoItem( + commentText = Description("This is a reply", Description.PLAIN_TEXT), + uploaderName = "Test 2", + ) + val reply2 = CommentsInfoItem( + commentText = Description("This is another reply.
This is another line.", Description.HTML), + uploaderName = "Test 3", + ) + val flow = flowOf(PagingData.from(listOf(reply1, reply2))) + + AppTheme { + CommentReplies(comment = comment, flow = flow, disposables = CompositeDisposable()) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesFragment.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesFragment.kt index 1847d7d31..6afbc7e20 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesFragment.kt @@ -4,104 +4,50 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.runtime.remember import androidx.compose.ui.platform.ComposeView -import icepick.State -import io.reactivex.rxjava3.core.Single +import androidx.fragment.app.Fragment +import androidx.paging.Pager +import androidx.paging.PagingConfig import io.reactivex.rxjava3.disposables.CompositeDisposable -import org.schabi.newpipe.R -import org.schabi.newpipe.error.UserAction -import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage import org.schabi.newpipe.extractor.comments.CommentsInfoItem -import org.schabi.newpipe.fragments.list.BaseListInfoFragment -import org.schabi.newpipe.info_list.ItemViewMode +import org.schabi.newpipe.ktx.serializable import org.schabi.newpipe.ui.theme.AppTheme -import org.schabi.newpipe.util.ExtractorHelper -import org.schabi.newpipe.util.Localization -import java.util.Queue -import java.util.function.Supplier -class CommentRepliesFragment() : BaseListInfoFragment(UserAction.REQUESTED_COMMENT_REPLIES) { - /** - * @return the comment to which the replies are shown - */ - @State - lateinit var commentsInfoItem: CommentsInfoItem // the comment to show replies of +class CommentRepliesFragment : Fragment() { private val disposables = CompositeDisposable() - - constructor(commentsInfoItem: CommentsInfoItem) : this() { - this.commentsInfoItem = commentsInfoItem - // setting "" as title since the title will be properly set right after - setInitialData(commentsInfoItem.serviceId, commentsInfoItem.url, "") - } + lateinit var comment: CommentsInfoItem override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - return inflater.inflate(R.layout.fragment_comments, container, false) - } + comment = requireArguments().serializable(COMMENT_KEY)!! + return ComposeView(requireContext()).apply { + setContent { + val flow = remember(comment) { + Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) { + CommentRepliesSource(comment) + }.flow + } - override fun onDestroyView() { - disposables.clear() - super.onDestroyView() - } - - override fun getListHeaderSupplier(): Supplier { - return Supplier { - ComposeView(requireContext()).apply { - setContent { - AppTheme { - CommentRepliesHeader(commentsInfoItem, disposables) - } + AppTheme { + CommentReplies(comment = comment, flow = flow, disposables = disposables) } } } } - /*////////////////////////////////////////////////////////////////////////// - // State saving - ////////////////////////////////////////////////////////////////////////// */ - override fun writeTo(objectsToSave: Queue) { - super.writeTo(objectsToSave) - objectsToSave.add(commentsInfoItem) - } - - @Throws(Exception::class) - override fun readFrom(savedObjects: Queue) { - super.readFrom(savedObjects) - commentsInfoItem = savedObjects.poll() as CommentsInfoItem - } - - /*////////////////////////////////////////////////////////////////////////// - // Data loading - ////////////////////////////////////////////////////////////////////////// */ - override fun loadResult(forceLoad: Boolean): Single { - return Single.fromCallable { - CommentRepliesInfo( - commentsInfoItem, // the reply count string will be shown as the activity title - Localization.replyCount(requireContext(), commentsInfoItem.replyCount) - ) - } - } - - override fun loadMoreItemsLogic(): Single>? { - // commentsInfoItem.getUrl() should contain the url of the original - // ListInfo, which should be the stream url - return ExtractorHelper.getMoreCommentItems( - serviceId, commentsInfoItem.url, currentNextPage - ) - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - ////////////////////////////////////////////////////////////////////////// */ - override fun getItemViewMode(): ItemViewMode { - return ItemViewMode.LIST + override fun onDestroyView() { + super.onDestroyView() + disposables.clear() } companion object { @JvmField - val TAG: String = CommentRepliesFragment::class.java.simpleName + val TAG = CommentRepliesFragment::class.simpleName!! + + const val COMMENT_KEY = "comment" } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesHeader.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesHeader.kt index 4c4dc53b3..5a2a3ede0 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesHeader.kt +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesHeader.kt @@ -136,14 +136,8 @@ fun CommentRepliesHeader(comment: CommentsInfoItem, disposables: CompositeDispos } } -@Preview( - name = "Light mode", - uiMode = Configuration.UI_MODE_NIGHT_NO -) -@Preview( - name = "Dark mode", - uiMode = Configuration.UI_MODE_NIGHT_YES -) +@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable fun CommentRepliesHeaderPreview() { val disposables = CompositeDisposable() diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesInfo.java b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesInfo.java deleted file mode 100644 index cc160c395..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesInfo.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.schabi.newpipe.fragments.list.comments; - -import org.schabi.newpipe.extractor.ListInfo; -import org.schabi.newpipe.extractor.comments.CommentsInfoItem; -import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; - -import java.util.Collections; - -public final class CommentRepliesInfo extends ListInfo { - /** - * This class is used to wrap the comment replies page into a ListInfo object. - * - * @param comment the comment from which to get replies - * @param name will be shown as the fragment title - */ - public CommentRepliesInfo(final CommentsInfoItem comment, final String name) { - super(comment.getServiceId(), - new ListLinkHandler("", "", "", Collections.emptyList(), null), name); - setNextPage(comment.getReplies()); - setRelatedItems(Collections.emptyList()); // since it must be non-null - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesSource.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesSource.kt new file mode 100644 index 000000000..ed602d917 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesSource.kt @@ -0,0 +1,22 @@ +package org.schabi.newpipe.fragments.list.comments + +import androidx.paging.PagingState +import androidx.paging.rxjava3.RxPagingSource +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers +import org.schabi.newpipe.extractor.Page +import org.schabi.newpipe.extractor.comments.CommentsInfoItem +import org.schabi.newpipe.util.ExtractorHelper + +class CommentRepliesSource( + private val commentsInfoItem: CommentsInfoItem, +) : RxPagingSource() { + override fun loadSingle(params: LoadParams): Single> { + val nextPage = params.key ?: commentsInfoItem.replies + return ExtractorHelper.getMoreCommentItems(commentsInfoItem.serviceId, commentsInfoItem.url, nextPage) + .subscribeOn(Schedulers.io()) + .map { LoadResult.Page(it.items, null, it.nextPage) } + } + + override fun getRefreshKey(state: PagingState) = null +} diff --git a/app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt b/app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt index 61721d546..e01cf620e 100644 --- a/app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt +++ b/app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt @@ -1,9 +1,25 @@ package org.schabi.newpipe.ktx +import android.os.Build import android.os.Bundle import android.os.Parcelable import androidx.core.os.BundleCompat +import java.io.Serializable +import kotlin.reflect.safeCast inline fun Bundle.parcelableArrayList(key: String?): ArrayList? { return BundleCompat.getParcelableArrayList(this, key, T::class.java) } + +inline fun Bundle.serializable(key: String?): T? { + return getSerializable(this, key, T::class.java) +} + +fun getSerializable(bundle: Bundle, key: String?, clazz: Class): T? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + bundle.getSerializable(key, clazz) + } else { + @Suppress("DEPRECATION") + clazz.kotlin.safeCast(bundle.getSerializable(key)) + } +} 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 5dee32371..9accf22a5 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; @@ -503,8 +504,11 @@ public final class NavigationHelper { public static void openCommentRepliesFragment(@NonNull final FragmentActivity activity, @NonNull final CommentsInfoItem comment) { + final var bundle = new Bundle(); + bundle.putSerializable(CommentRepliesFragment.COMMENT_KEY, comment); + defaultTransaction(activity.getSupportFragmentManager()) - .replace(R.id.fragment_holder, new CommentRepliesFragment(comment), + .replace(R.id.fragment_holder, CommentRepliesFragment.class, bundle, CommentRepliesFragment.TAG) .addToBackStack(CommentRepliesFragment.TAG) .commit(); diff --git a/build.gradle b/build.gradle index 5a1ae1945..49de98659 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.9.23' + ext.kotlin_version = '1.9.24' repositories { google() mavenCentral()