From bda961a04cb8172bd3d5d0c4d7ea61da965e8c72 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sun, 12 May 2024 08:21:13 +0530 Subject: [PATCH 01/72] Convert comment replies views to Jetpack Compose --- app/build.gradle | 6 +- .../fragments/list/comments/Comment.kt | 84 ++++++++++++ .../list/comments/CommentRepliesHeader.kt | 121 ++++++++++++++++++ build.gradle | 2 +- 4 files changed, 209 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/comments/Comment.kt create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesHeader.kt diff --git a/app/build.gradle b/app/build.gradle index 9ea725ad9..5b5b25350 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -106,7 +106,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion = "1.5.3" + kotlinCompilerExtensionVersion = "1.5.13" } } @@ -267,7 +267,7 @@ dependencies { implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}" // Image loading - implementation 'io.coil-kt:coil:2.7.0' + implementation 'io.coil-kt:coil-compose:2.7.0' // Markdown library for Android implementation "io.noties.markwon:core:${markwonVersion}" @@ -289,7 +289,7 @@ dependencies { implementation "org.ocpsoft.prettytime:prettytime:5.0.8.Final" // Jetpack Compose - implementation(platform('androidx.compose:compose-bom:2024.02.01')) + implementation(platform('androidx.compose:compose-bom:2024.05.00')) implementation 'androidx.compose.material3:material3' implementation 'androidx.activity:activity-compose' implementation 'androidx.compose.ui:ui-tooling-preview' 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 new file mode 100644 index 000000000..c622aa18d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/Comment.kt @@ -0,0 +1,84 @@ +package org.schabi.newpipe.fragments.list.comments + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.fragment.app.FragmentActivity +import coil.compose.AsyncImage +import org.schabi.newpipe.R +import org.schabi.newpipe.extractor.comments.CommentsInfoItem +import org.schabi.newpipe.extractor.stream.Description +import org.schabi.newpipe.util.NavigationHelper +import org.schabi.newpipe.util.image.ImageStrategy + +@Composable +fun Comment(comment: CommentsInfoItem) { + val context = LocalContext.current + + Row(modifier = Modifier.padding(all = 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) + } + ) + } + + Spacer(modifier = Modifier.width(8.dp)) + + var isExpanded by rememberSaveable { mutableStateOf(false) } + + Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) { + Text( + text = comment.uploaderName, + color = MaterialTheme.colors.secondary + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = comment.commentText.content, + // If the comment is expanded, we display all its content + // otherwise we only display the first line + maxLines = if (isExpanded) Int.MAX_VALUE else 1, + style = MaterialTheme.typography.body2 + ) + } + } +} + +@Preview +@Composable +fun CommentPreview() { + val comment = CommentsInfoItem(1, "", "") + comment.commentText = Description("Hello world!", Description.PLAIN_TEXT) + comment.uploaderName = "Test" + + Comment(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 new file mode 100644 index 000000000..243d887cd --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesHeader.kt @@ -0,0 +1,121 @@ +package org.schabi.newpipe.fragments.list.comments + +import android.widget.TextView +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +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.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.text.HtmlCompat +import androidx.core.text.method.LinkMovementMethodCompat +import androidx.fragment.app.FragmentActivity +import coil.compose.AsyncImage +import io.reactivex.rxjava3.disposables.CompositeDisposable +import org.schabi.newpipe.R +import org.schabi.newpipe.extractor.comments.CommentsInfoItem +import org.schabi.newpipe.extractor.stream.Description +import org.schabi.newpipe.util.Localization +import org.schabi.newpipe.util.NavigationHelper +import org.schabi.newpipe.util.ServiceHelper +import org.schabi.newpipe.util.image.ImageStrategy +import org.schabi.newpipe.util.text.TextLinkifier + +@Composable +fun CommentRepliesHeader(comment: CommentsInfoItem, disposables: CompositeDisposable) { + val context = LocalContext.current + + Column(modifier = Modifier.padding(all = 8.dp)) { + Row { + Row( + modifier = Modifier + .padding(all = 8.dp) + .clickable { + NavigationHelper.openCommentAuthorIfPresent( + context as FragmentActivity, + comment + ) + } + ) { + 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) + ) + } + + Spacer(modifier = Modifier.width(8.dp)) + + Column { + Text(text = comment.uploaderName) + + Text( + text = Localization.relativeTimeOrTextual( + context, comment.uploadDate, comment.textualUploadDate + ) + ) + } + } + + if (comment.isHeartedByUploader) { + Image( + painter = painterResource(R.drawable.ic_heart), + contentDescription = stringResource(R.string.detail_heart_img_view_description) + ) + } + + if (comment.isPinned) { + Image( + painter = painterResource(R.drawable.ic_pin), + contentDescription = stringResource(R.string.detail_pinned_comment_view_description) + ) + } + } + + AndroidView( + factory = { context -> + TextView(context).apply { + movementMethod = LinkMovementMethodCompat.getInstance() + } + }, + update = { view -> + // setup comment content + TextLinkifier.fromDescription(view, comment.commentText, + HtmlCompat.FROM_HTML_MODE_LEGACY, + ServiceHelper.getServiceById(comment.serviceId), comment.url, disposables, + null + ) + } + ) + } +} + +@Preview +@Composable +fun CommentRepliesHeaderPreview() { + val disposables = CompositeDisposable() + val comment = CommentsInfoItem(1, "", "") + comment.commentText = Description("Hello world!", Description.PLAIN_TEXT) + comment.uploaderName = "Test" + comment.textualUploadDate = "5 months ago" + + CommentRepliesHeader(comment, disposables) +} diff --git a/build.gradle b/build.gradle index 6d19a6f8a..5a1ae1945 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.10' + ext.kotlin_version = '1.9.23' repositories { google() mavenCentral() From 644a345b5571fd34ae3471c686ee59afc98fe412 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sun, 12 May 2024 08:49:14 +0530 Subject: [PATCH 02/72] Rename .java to .kt --- .../{CommentRepliesFragment.java => CommentRepliesFragment.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/org/schabi/newpipe/fragments/list/comments/{CommentRepliesFragment.java => CommentRepliesFragment.kt} (100%) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesFragment.kt similarity index 100% rename from app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesFragment.java rename to app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesFragment.kt From e05d97732eb48c1641367234a26f98db94c1bb98 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sun, 12 May 2024 08:49:15 +0530 Subject: [PATCH 03/72] Use reply header composable in fragment --- .../java/org/schabi/newpipe/MainActivity.java | 2 +- .../list/comments/CommentRepliesFragment.kt | 260 +++++++----------- .../list/comments/CommentRepliesHeader.kt | 4 +- .../res/layout/comment_replies_header.xml | 137 --------- 4 files changed, 100 insertions(+), 303 deletions(-) delete mode 100644 app/src/main/res/layout/comment_replies_header.xml diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 175694125..43b01c70d 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -871,7 +871,7 @@ public class MainActivity extends AppCompatActivity { @Nullable final CommentRepliesFragment repliesFragment = (CommentRepliesFragment) fm.findFragmentByTag(CommentRepliesFragment.TAG); @Nullable final CommentsInfoItem rootComment = - repliesFragment == null ? null : repliesFragment.getCommentsInfoItem(); + repliesFragment == null ? null : repliesFragment.commentsInfoItem; // 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/CommentRepliesFragment.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesFragment.kt index 4eb73520f..4bfb04d6e 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 @@ -1,170 +1,104 @@ -package org.schabi.newpipe.fragments.list.comments; +package org.schabi.newpipe.fragments.list.comments -import static org.schabi.newpipe.util.ServiceHelper.getServiceById; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.core.text.HtmlCompat; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.databinding.CommentRepliesHeaderBinding; -import org.schabi.newpipe.error.UserAction; -import org.schabi.newpipe.extractor.ListExtractor; -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.util.DeviceUtils; -import org.schabi.newpipe.util.ExtractorHelper; -import org.schabi.newpipe.util.Localization; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.image.CoilHelper; -import org.schabi.newpipe.util.image.ImageStrategy; -import org.schabi.newpipe.util.text.TextLinkifier; - -import java.util.Queue; -import java.util.function.Supplier; - -import icepick.State; -import io.reactivex.rxjava3.core.Single; -import io.reactivex.rxjava3.disposables.CompositeDisposable; - -public final class CommentRepliesFragment - extends BaseListInfoFragment { - - public static final String TAG = CommentRepliesFragment.class.getSimpleName(); - - @State - CommentsInfoItem commentsInfoItem; // the comment to show replies of - private final CompositeDisposable disposables = new CompositeDisposable(); - - - /*////////////////////////////////////////////////////////////////////////// - // Constructors and lifecycle - //////////////////////////////////////////////////////////////////////////*/ - - // only called by the Android framework, after which readFrom is called and restores all data - public CommentRepliesFragment() { - super(UserAction.REQUESTED_COMMENT_REPLIES); - } - - public CommentRepliesFragment(@NonNull final CommentsInfoItem commentsInfoItem) { - this(); - this.commentsInfoItem = commentsInfoItem; - // setting "" as title since the title will be properly set right after - setInitialData(commentsInfoItem.getServiceId(), commentsInfoItem.getUrl(), ""); - } - - @Nullable - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - @Nullable final ViewGroup container, - @Nullable final Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_comments, container, false); - } - - @Override - public void onDestroyView() { - disposables.clear(); - super.onDestroyView(); - } - - @Override - protected Supplier getListHeaderSupplier() { - return () -> { - final CommentRepliesHeaderBinding binding = CommentRepliesHeaderBinding - .inflate(activity.getLayoutInflater(), itemsList, false); - final CommentsInfoItem item = commentsInfoItem; - - // load the author avatar - CoilHelper.INSTANCE.loadAvatar(binding.authorAvatar, item.getUploaderAvatars()); - binding.authorAvatar.setVisibility(ImageStrategy.shouldLoadImages() - ? View.VISIBLE : View.GONE); - - // setup author name and comment date - binding.authorName.setText(item.getUploaderName()); - binding.uploadDate.setText(Localization.relativeTimeOrTextual( - getContext(), item.getUploadDate(), item.getTextualUploadDate())); - binding.authorTouchArea.setOnClickListener( - v -> NavigationHelper.openCommentAuthorIfPresent(requireActivity(), item)); - - // setup like count, hearted and pinned - binding.thumbsUpCount.setText( - Localization.likeCount(requireContext(), item.getLikeCount())); - // for heartImage goneMarginEnd was used, but there is no way to tell ConstraintLayout - // not to use a different margin only when both the next two views are gone - ((ConstraintLayout.LayoutParams) binding.thumbsUpCount.getLayoutParams()) - .setMarginEnd(DeviceUtils.dpToPx( - (item.isHeartedByUploader() || item.isPinned() ? 8 : 16), - requireContext())); - binding.heartImage.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE); - binding.pinnedImage.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE); - - // setup comment content - TextLinkifier.fromDescription(binding.commentContent, item.getCommentText(), - HtmlCompat.FROM_HTML_MODE_LEGACY, getServiceById(item.getServiceId()), - item.getUrl(), disposables, null); - - return binding.getRoot(); - }; - } - - - /*////////////////////////////////////////////////////////////////////////// - // State saving - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void writeTo(final Queue objectsToSave) { - super.writeTo(objectsToSave); - objectsToSave.add(commentsInfoItem); - } - - @Override - public void readFrom(@NonNull final Queue savedObjects) throws Exception { - super.readFrom(savedObjects); - commentsInfoItem = (CommentsInfoItem) savedObjects.poll(); - } - - - /*////////////////////////////////////////////////////////////////////////// - // Data loading - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected Single loadResult(final boolean forceLoad) { - return Single.fromCallable(() -> new CommentRepliesInfo(commentsInfoItem, - // the reply count string will be shown as the activity title - Localization.replyCount(requireContext(), commentsInfoItem.getReplyCount()))); - } - - @Override - protected Single> loadMoreItemsLogic() { - // commentsInfoItem.getUrl() should contain the url of the original - // ListInfo, which should be the stream url - return ExtractorHelper.getMoreCommentItems( - serviceId, commentsInfoItem.getUrl(), currentNextPage); - } - - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected ItemViewMode getItemViewMode() { - return ItemViewMode.LIST; - } +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import icepick.State +import io.reactivex.rxjava3.core.Single +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.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 */ - public CommentsInfoItem getCommentsInfoItem() { - return commentsInfoItem; + @State + lateinit var commentsInfoItem: CommentsInfoItem // the comment to show replies of + 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, "") + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return inflater.inflate(R.layout.fragment_comments, container, false) + } + + override fun onDestroyView() { + disposables.clear() + super.onDestroyView() + } + + override fun getListHeaderSupplier(): Supplier { + return Supplier { + ComposeView(requireContext()).apply { + setContent { + CommentRepliesHeader(commentsInfoItem, 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 + } + + companion object { + @JvmField + val TAG: String = CommentRepliesFragment::class.java.simpleName } } 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 243d887cd..89f6985a3 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 @@ -98,8 +98,8 @@ fun CommentRepliesHeader(comment: CommentsInfoItem, disposables: CompositeDispos }, update = { view -> // setup comment content - TextLinkifier.fromDescription(view, comment.commentText, - HtmlCompat.FROM_HTML_MODE_LEGACY, + TextLinkifier.fromDescription( + view, comment.commentText, HtmlCompat.FROM_HTML_MODE_LEGACY, ServiceHelper.getServiceById(comment.serviceId), comment.url, disposables, null ) diff --git a/app/src/main/res/layout/comment_replies_header.xml b/app/src/main/res/layout/comment_replies_header.xml deleted file mode 100644 index ed5ba1a10..000000000 --- a/app/src/main/res/layout/comment_replies_header.xml +++ /dev/null @@ -1,137 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 8ce9a7e43c1bb4aa9213d56725aa3eaaf6fbc96d Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sun, 12 May 2024 18:09:16 +0530 Subject: [PATCH 04/72] Added like count --- .../list/comments/CommentRepliesHeader.kt | 47 ++++++++++++++----- 1 file changed, 35 insertions(+), 12 deletions(-) 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 89f6985a3..cbd0a8f1b 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 @@ -3,15 +3,18 @@ package org.schabi.newpipe.fragments.list.comments import android.widget.TextView 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.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext @@ -39,16 +42,20 @@ fun CommentRepliesHeader(comment: CommentsInfoItem, disposables: CompositeDispos val context = LocalContext.current Column(modifier = Modifier.padding(all = 8.dp)) { - Row { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { Row( modifier = Modifier - .padding(all = 8.dp) + .padding(top = 8.dp, bottom = 8.dp, end = 8.dp) .clickable { NavigationHelper.openCommentAuthorIfPresent( context as FragmentActivity, comment ) - } + }, + verticalAlignment = Alignment.CenterVertically ) { if (ImageStrategy.shouldLoadImages()) { AsyncImage( @@ -75,18 +82,31 @@ fun CommentRepliesHeader(comment: CommentsInfoItem, disposables: CompositeDispos } } - if (comment.isHeartedByUploader) { - Image( - painter = painterResource(R.drawable.ic_heart), - contentDescription = stringResource(R.string.detail_heart_img_view_description) - ) - } + Spacer(modifier = Modifier.weight(1f)) - if (comment.isPinned) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { Image( - painter = painterResource(R.drawable.ic_pin), - contentDescription = stringResource(R.string.detail_pinned_comment_view_description) + painter = painterResource(R.drawable.ic_thumb_up), + contentDescription = stringResource(R.string.detail_likes_img_view_description) ) + Text(text = comment.likeCount.toString()) + + if (comment.isHeartedByUploader) { + Image( + painter = painterResource(R.drawable.ic_heart), + contentDescription = stringResource(R.string.detail_heart_img_view_description) + ) + } + + if (comment.isPinned) { + Image( + painter = painterResource(R.drawable.ic_pin), + contentDescription = stringResource(R.string.detail_pinned_comment_view_description) + ) + } } } @@ -116,6 +136,9 @@ fun CommentRepliesHeaderPreview() { comment.commentText = Description("Hello world!", Description.PLAIN_TEXT) comment.uploaderName = "Test" comment.textualUploadDate = "5 months ago" + comment.likeCount = 100 + comment.isPinned = true + comment.isHeartedByUploader = true CommentRepliesHeader(comment, disposables) } From 56c80ce6dd554eab6899a05066c8274b34e6f8d2 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Fri, 17 May 2024 10:17:09 +0530 Subject: [PATCH 05/72] Added missing comment features, fixed theming --- .../java/org/schabi/newpipe/MainActivity.java | 2 +- .../fragments/list/comments/Comment.kt | 123 +++++++++---- .../list/comments/CommentRepliesFragment.kt | 5 +- .../list/comments/CommentRepliesHeader.kt | 165 ++++++++++-------- 4 files changed, 181 insertions(+), 114 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 43b01c70d..175694125 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -871,7 +871,7 @@ public class MainActivity extends AppCompatActivity { @Nullable final CommentRepliesFragment repliesFragment = (CommentRepliesFragment) fm.findFragmentByTag(CommentRepliesFragment.TAG); @Nullable final CommentsInfoItem rootComment = - repliesFragment == null ? null : repliesFragment.commentsInfoItem; + repliesFragment == null ? null : repliesFragment.getCommentsInfoItem(); // 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 c622aa18d..b293adedc 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 @@ -1,16 +1,19 @@ package org.schabi.newpipe.fragments.list.comments +import android.content.res.Configuration +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.Spacer -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -20,6 +23,7 @@ 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.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentActivity @@ -27,6 +31,7 @@ import coil.compose.AsyncImage import org.schabi.newpipe.R import org.schabi.newpipe.extractor.comments.CommentsInfoItem import org.schabi.newpipe.extractor.stream.Description +import org.schabi.newpipe.ui.theme.AppTheme import org.schabi.newpipe.util.NavigationHelper import org.schabi.newpipe.util.image.ImageStrategy @@ -34,51 +39,93 @@ import org.schabi.newpipe.util.image.ImageStrategy fun Comment(comment: CommentsInfoItem) { val context = LocalContext.current - Row(modifier = Modifier.padding(all = 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) + Surface(color = MaterialTheme.colorScheme.background) { + Row(modifier = Modifier.padding(all = 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 + ) + } + ) + } + + Spacer(modifier = Modifier.width(8.dp)) + + var isExpanded by rememberSaveable { mutableStateOf(false) } + + Column( + modifier = Modifier.clickable { isExpanded = !isExpanded }, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = comment.uploaderName, + color = MaterialTheme.colorScheme.secondary + ) + + Text( + text = comment.commentText.content, + // 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, + style = MaterialTheme.typography.bodyMedium + ) + + 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 = comment.likeCount.toString()) + + if (comment.isHeartedByUploader) { + Image( + painter = painterResource(R.drawable.ic_heart), + contentDescription = stringResource(R.string.detail_heart_img_view_description) + ) } - ) + + if (comment.isPinned) { + Image( + painter = painterResource(R.drawable.ic_pin), + contentDescription = stringResource(R.string.detail_pinned_comment_view_description) + ) + } + } + } } - Spacer(modifier = Modifier.width(8.dp)) - - var isExpanded by rememberSaveable { mutableStateOf(false) } - - Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) { - Text( - text = comment.uploaderName, - color = MaterialTheme.colors.secondary - ) - - Spacer(modifier = Modifier.height(4.dp)) - - Text( - text = comment.commentText.content, - // If the comment is expanded, we display all its content - // otherwise we only display the first line - maxLines = if (isExpanded) Int.MAX_VALUE else 1, - style = MaterialTheme.typography.body2 - ) - } + // TODO: Add support for comment replies } } -@Preview +@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!", Description.PLAIN_TEXT) comment.uploaderName = "Test" + comment.likeCount = 100 + comment.isHeartedByUploader = true + comment.isPinned = true - Comment(comment) + AppTheme { + Comment(comment) + } } 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 4bfb04d6e..1847d7d31 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 @@ -14,6 +14,7 @@ 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.ui.theme.AppTheme import org.schabi.newpipe.util.ExtractorHelper import org.schabi.newpipe.util.Localization import java.util.Queue @@ -50,7 +51,9 @@ class CommentRepliesFragment() : BaseListInfoFragment + TextView(context).apply { + movementMethod = LinkMovementMethodCompat.getInstance() + } + }, + update = { view -> + // setup comment content + TextLinkifier.fromDescription( + view, comment.commentText, HtmlCompat.FROM_HTML_MODE_LEGACY, + ServiceHelper.getServiceById(comment.serviceId), comment.url, disposables, + null ) } - - if (comment.isPinned) { - Image( - painter = painterResource(R.drawable.ic_pin), - contentDescription = stringResource(R.string.detail_pinned_comment_view_description) - ) - } - } + ) } - - AndroidView( - factory = { context -> - TextView(context).apply { - movementMethod = LinkMovementMethodCompat.getInstance() - } - }, - update = { view -> - // setup comment content - TextLinkifier.fromDescription( - view, comment.commentText, HtmlCompat.FROM_HTML_MODE_LEGACY, - ServiceHelper.getServiceById(comment.serviceId), comment.url, disposables, - null - ) - } - ) } } -@Preview +@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() @@ -140,5 +155,7 @@ fun CommentRepliesHeaderPreview() { comment.isPinned = true comment.isHeartedByUploader = true - CommentRepliesHeader(comment, disposables) + AppTheme { + CommentRepliesHeader(comment, disposables) + } } From 1620668966cc9793cec97ab68149bbc7d5120874 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sun, 16 Jun 2024 18:29:29 +0530 Subject: [PATCH 06/72] Add comment ellipsis --- .../newpipe/fragments/list/comments/Comment.kt | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) 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 b293adedc..8b1726fff 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 @@ -24,6 +24,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentActivity @@ -77,6 +78,7 @@ fun Comment(comment: CommentsInfoItem) { // 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 ) @@ -108,18 +110,12 @@ fun Comment(comment: CommentsInfoItem) { } } -@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 CommentPreview() { val comment = CommentsInfoItem(1, "", "") - comment.commentText = Description("Hello world!", Description.PLAIN_TEXT) + 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 From 341cc37ce76933bdaa8ae45b7ef17c2828324d83 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Tue, 18 Jun 2024 07:59:24 +0530 Subject: [PATCH 07/72] Update replies fragment to use the comment composable as well --- app/build.gradle | 8 +- .../java/org/schabi/newpipe/MainActivity.java | 8 +- .../fragments/list/comments/Comment.kt | 32 ++++-- .../fragments/list/comments/CommentReplies.kt | 60 +++++++++++ .../list/comments/CommentRepliesFragment.kt | 100 ++++-------------- .../list/comments/CommentRepliesHeader.kt | 10 +- .../list/comments/CommentRepliesInfo.java | 22 ---- .../list/comments/CommentRepliesSource.kt | 22 ++++ .../java/org/schabi/newpipe/ktx/Bundle.kt | 16 +++ .../schabi/newpipe/util/NavigationHelper.java | 6 +- build.gradle | 2 +- 11 files changed, 163 insertions(+), 123 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentReplies.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesInfo.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesSource.kt 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() From 11bb2495ba9bcd555146bdcf2dd8b810d9287e35 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Tue, 18 Jun 2024 09:34:12 +0530 Subject: [PATCH 08/72] Improve previews, display date of comment --- .../newpipe/fragments/list/comments/Comment.kt | 15 +++++++++++---- .../fragments/list/comments/CommentReplies.kt | 3 +++ .../list/comments/CommentRepliesHeader.kt | 17 ++++++++--------- 3 files changed, 22 insertions(+), 13 deletions(-) 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 bdffa8561..78c9f88f4 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 @@ -33,6 +33,7 @@ import org.schabi.newpipe.R import org.schabi.newpipe.extractor.comments.CommentsInfoItem import org.schabi.newpipe.extractor.stream.Description 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 @@ -68,8 +69,11 @@ fun Comment(comment: CommentsInfoItem) { modifier = Modifier.clickable { isExpanded = !isExpanded }, verticalArrangement = Arrangement.spacedBy(4.dp) ) { + val date = Localization.relativeTimeOrTextual( + context, comment.uploadDate, comment.textualUploadDate + ) Text( - text = comment.uploaderName, + text = Localization.concatenateStrings(comment.uploaderName, date), color = MaterialTheme.colorScheme.secondary ) @@ -118,9 +122,9 @@ fun CommentsInfoItem( commentText: Description, uploaderName: String, textualUploadDate: String = "5 months ago", - likeCount: Int = 100, - isHeartedByUploader: Boolean = true, - isPinned: Boolean = true, + likeCount: Int = 0, + isHeartedByUploader: Boolean = false, + isPinned: Boolean = false, ) = CommentsInfoItem(serviceId, url, name).apply { this.commentText = commentText this.uploaderName = uploaderName @@ -137,6 +141,9 @@ private fun CommentPreview() { val comment = CommentsInfoItem( commentText = Description("Hello world!\n\nThis line should be hidden by default.", Description.PLAIN_TEXT), uploaderName = "Test", + likeCount = 100, + isPinned = true, + isHeartedByUploader = true ) AppTheme { 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 index 05a6d1c3b..42813f087 100644 --- 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 @@ -42,6 +42,9 @@ private fun CommentRepliesPreview() { val comment = CommentsInfoItem( commentText = Description("Hello world!", Description.PLAIN_TEXT), uploaderName = "Test", + likeCount = 100, + isPinned = true, + isHeartedByUploader = true ) val reply1 = CommentsInfoItem( 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 5a2a3ede0..1853a134b 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 @@ -140,16 +140,15 @@ fun CommentRepliesHeader(comment: CommentsInfoItem, disposables: CompositeDispos @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable fun CommentRepliesHeaderPreview() { - val disposables = CompositeDisposable() - val comment = CommentsInfoItem(1, "", "") - comment.commentText = Description("Hello world!", Description.PLAIN_TEXT) - comment.uploaderName = "Test" - comment.textualUploadDate = "5 months ago" - comment.likeCount = 100 - comment.isPinned = true - comment.isHeartedByUploader = true + val comment = CommentsInfoItem( + commentText = Description("Hello world!", Description.PLAIN_TEXT), + uploaderName = "Test", + likeCount = 100, + isPinned = true, + isHeartedByUploader = true + ) AppTheme { - CommentRepliesHeader(comment, disposables) + CommentRepliesHeader(comment, CompositeDisposable()) } } From e30d5e43050eaf2d4f7a6ec77bdf83f227d6ad26 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Tue, 18 Jun 2024 16:13:20 +0530 Subject: [PATCH 09/72] Fixed some comment issues --- .../fragments/list/comments/Comment.kt | 44 ++++++++++--------- .../fragments/list/comments/CommentReplies.kt | 16 +++---- 2 files changed, 32 insertions(+), 28 deletions(-) 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 78c9f88f4..bde37ac95 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 @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -40,9 +41,15 @@ import org.schabi.newpipe.util.image.ImageStrategy @Composable fun Comment(comment: CommentsInfoItem) { val context = LocalContext.current + var isExpanded by rememberSaveable { mutableStateOf(false) } Surface(color = MaterialTheme.colorScheme.background) { - Row(modifier = Modifier.padding(all = 8.dp)) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { isExpanded = !isExpanded } + .padding(all = 8.dp) + ) { if (ImageStrategy.shouldLoadImages()) { AsyncImage( model = ImageStrategy.choosePreferredImage(comment.uploaderAvatars), @@ -63,19 +70,23 @@ fun Comment(comment: CommentsInfoItem) { Spacer(modifier = Modifier.width(8.dp)) - var isExpanded by rememberSaveable { mutableStateOf(false) } + 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) + ) + } - Column( - modifier = Modifier.clickable { isExpanded = !isExpanded }, - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - val date = Localization.relativeTimeOrTextual( - context, comment.uploadDate, comment.textualUploadDate - ) - Text( - text = Localization.concatenateStrings(comment.uploaderName, date), - color = MaterialTheme.colorScheme.secondary - ) + val date = Localization.relativeTimeOrTextual( + context, comment.uploadDate, comment.textualUploadDate + ) + Text( + text = Localization.concatenateStrings(comment.uploaderName, date), + color = MaterialTheme.colorScheme.secondary + ) + } // TODO: Handle HTML and Markdown formats. Text( @@ -100,13 +111,6 @@ fun Comment(comment: CommentsInfoItem) { contentDescription = stringResource(R.string.detail_heart_img_view_description) ) } - - if (comment.isPinned) { - Image( - painter = painterResource(R.drawable.ic_pin), - contentDescription = stringResource(R.string.detail_pinned_comment_view_description) - ) - } } } } 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 index 42813f087..dcabedb48 100644 --- 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 @@ -1,7 +1,6 @@ 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 @@ -24,13 +23,14 @@ fun CommentReplies( ) { val replies = flow.collectAsLazyPagingItems() - Column { - CommentRepliesHeader(comment = comment, disposables = disposables) - HorizontalDivider(thickness = 1.dp) - LazyColumn { - items(replies.itemCount) { - Comment(comment = replies[it]!!) - } + LazyColumn { + item { + CommentRepliesHeader(comment = comment, disposables = disposables) + HorizontalDivider(thickness = 1.dp) + } + + items(replies.itemCount) { + Comment(comment = replies[it]!!) } } } From 1908e18dc4e02d15983cbf35952ffa4ab04a7114 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Wed, 19 Jun 2024 12:40:49 +0530 Subject: [PATCH 10/72] Use AnnotatedString to handle HTML parsing --- app/build.gradle | 1 + .../fragments/list/comments/Comment.kt | 50 ++++++++++++++----- .../fragments/list/comments/CommentReplies.kt | 8 ++- .../list/comments/CommentRepliesFragment.kt | 9 +--- .../list/comments/CommentRepliesHeader.kt | 28 ++--------- 5 files changed, 48 insertions(+), 48 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 200af2209..6e6e29efb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -293,6 +293,7 @@ dependencies { implementation 'androidx.compose.material3:material3' implementation 'androidx.activity:activity-compose' implementation 'androidx.compose.ui:ui-tooling-preview' + implementation 'androidx.compose.ui:ui-text:1.7.0-beta03' // Needed for parsing HTML to AnnotatedString // Paging implementation 'androidx.paging:paging-rxjava3:3.3.0' 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 bde37ac95..0f5972879 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 @@ -18,6 +18,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -25,8 +26,13 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.ParagraphStyle +import androidx.compose.ui.text.fromHtml 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 coil.compose.AsyncImage @@ -38,6 +44,18 @@ import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.NavigationHelper import org.schabi.newpipe.util.image.ImageStrategy +@Composable +fun rememberParsedText(commentText: Description): AnnotatedString { + // TODO: Handle links and hashtags, Markdown. + return remember(commentText) { + if (commentText.type == Description.HTML) { + AnnotatedString.fromHtml(commentText.content) + } else { + AnnotatedString(commentText.content, ParagraphStyle()) + } + } +} + @Composable fun Comment(comment: CommentsInfoItem) { val context = LocalContext.current @@ -79,23 +97,22 @@ fun Comment(comment: CommentsInfoItem) { ) } - val date = Localization.relativeTimeOrTextual( - context, comment.uploadDate, comment.textualUploadDate - ) - Text( - text = Localization.concatenateStrings(comment.uploaderName, date), - color = MaterialTheme.colorScheme.secondary - ) + 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) } - // TODO: Handle HTML and Markdown formats. Text( - text = comment.commentText.content, + text = rememberParsedText(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 + style = MaterialTheme.typography.bodyMedium, ) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { @@ -138,12 +155,21 @@ fun CommentsInfoItem( this.isPinned = isPinned } +class DescriptionPreviewProvider : PreviewParameterProvider { + override val values = sequenceOf( + Description("Hello world!

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() { +private fun CommentPreview( + @PreviewParameter(DescriptionPreviewProvider::class) description: Description +) { val comment = CommentsInfoItem( - commentText = Description("Hello world!\n\nThis line should be hidden by default.", Description.PLAIN_TEXT), + commentText = description, uploaderName = "Test", likeCount = 100, isPinned = true, 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 index dcabedb48..53a4fa4bf 100644 --- 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 @@ -8,7 +8,6 @@ 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 @@ -18,14 +17,13 @@ import org.schabi.newpipe.ui.theme.AppTheme @Composable fun CommentReplies( comment: CommentsInfoItem, - flow: Flow>, - disposables: CompositeDisposable + flow: Flow> ) { val replies = flow.collectAsLazyPagingItems() LazyColumn { item { - CommentRepliesHeader(comment = comment, disposables = disposables) + CommentRepliesHeader(comment = comment) HorizontalDivider(thickness = 1.dp) } @@ -58,6 +56,6 @@ private fun CommentRepliesPreview() { val flow = flowOf(PagingData.from(listOf(reply1, reply2))) AppTheme { - CommentReplies(comment = comment, flow = flow, disposables = CompositeDisposable()) + CommentReplies(comment = comment, flow = flow) } } 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 6afbc7e20..e1ed3041e 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 @@ -9,13 +9,11 @@ import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.paging.Pager import androidx.paging.PagingConfig -import io.reactivex.rxjava3.disposables.CompositeDisposable import org.schabi.newpipe.extractor.comments.CommentsInfoItem import org.schabi.newpipe.ktx.serializable import org.schabi.newpipe.ui.theme.AppTheme class CommentRepliesFragment : Fragment() { - private val disposables = CompositeDisposable() lateinit var comment: CommentsInfoItem override fun onCreateView( @@ -33,17 +31,12 @@ class CommentRepliesFragment : Fragment() { } AppTheme { - CommentReplies(comment = comment, flow = flow, disposables = disposables) + CommentReplies(comment = comment, flow = flow) } } } } - override fun onDestroyView() { - super.onDestroyView() - disposables.clear() - } - companion object { @JvmField val TAG = CommentRepliesFragment::class.simpleName!! 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 1853a134b..2c93999e8 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 @@ -1,7 +1,6 @@ package org.schabi.newpipe.fragments.list.comments import android.content.res.Configuration -import android.widget.TextView import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -25,24 +24,18 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.text.HtmlCompat -import androidx.core.text.method.LinkMovementMethodCompat import androidx.fragment.app.FragmentActivity import coil.compose.AsyncImage -import io.reactivex.rxjava3.disposables.CompositeDisposable import org.schabi.newpipe.R import org.schabi.newpipe.extractor.comments.CommentsInfoItem import org.schabi.newpipe.extractor.stream.Description import org.schabi.newpipe.ui.theme.AppTheme import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.NavigationHelper -import org.schabi.newpipe.util.ServiceHelper import org.schabi.newpipe.util.image.ImageStrategy -import org.schabi.newpipe.util.text.TextLinkifier @Composable -fun CommentRepliesHeader(comment: CommentsInfoItem, disposables: CompositeDisposable) { +fun CommentRepliesHeader(comment: CommentsInfoItem) { val context = LocalContext.current Surface(color = MaterialTheme.colorScheme.background) { @@ -117,20 +110,9 @@ fun CommentRepliesHeader(comment: CommentsInfoItem, disposables: CompositeDispos } } - AndroidView( - factory = { context -> - TextView(context).apply { - movementMethod = LinkMovementMethodCompat.getInstance() - } - }, - update = { view -> - // setup comment content - TextLinkifier.fromDescription( - view, comment.commentText, HtmlCompat.FROM_HTML_MODE_LEGACY, - ServiceHelper.getServiceById(comment.serviceId), comment.url, disposables, - null - ) - } + Text( + text = rememberParsedText(comment.commentText), + style = MaterialTheme.typography.bodyMedium ) } } @@ -149,6 +131,6 @@ fun CommentRepliesHeaderPreview() { ) AppTheme { - CommentRepliesHeader(comment, CompositeDisposable()) + CommentRepliesHeader(comment) } } From e92ba8f5d1f43d6dd569e4a399f6aaa8fdd0a235 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Wed, 19 Jun 2024 12:59:19 +0530 Subject: [PATCH 11/72] Add replies button --- .../fragments/list/comments/Comment.kt | 50 ++++++++++++++----- 1 file changed, 37 insertions(+), 13 deletions(-) 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 0f5972879..d7595f9fa 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 @@ -15,16 +15,19 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme 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.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.AnnotatedString import androidx.compose.ui.text.ParagraphStyle @@ -37,6 +40,7 @@ import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentActivity import coil.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.ui.theme.AppTheme @@ -115,24 +119,38 @@ fun Comment(comment: CommentsInfoItem) { style = MaterialTheme.typography.bodyMedium, ) - 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 = comment.likeCount.toString()) - - if (comment.isHeartedByUploader) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Image( - painter = painterResource(R.drawable.ic_heart), - contentDescription = stringResource(R.string.detail_heart_img_view_description) + painter = painterResource(R.drawable.ic_thumb_up), + contentDescription = stringResource(R.string.detail_likes_img_view_description) ) + Text(text = comment.likeCount.toString()) + + 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 = { /*TODO*/ }) { + Text( + text = pluralStringResource( + R.plurals.replies, comment.replyCount, comment.replyCount.toString() + ) + ) + } } } } } - - // TODO: Add support for comment replies } } @@ -146,6 +164,8 @@ fun CommentsInfoItem( 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 @@ -153,6 +173,8 @@ fun CommentsInfoItem( this.likeCount = likeCount this.isHeartedByUploader = isHeartedByUploader this.isPinned = isPinned + this.replies = replies + this.replyCount = replyCount } class DescriptionPreviewProvider : PreviewParameterProvider { @@ -173,7 +195,9 @@ private fun CommentPreview( uploaderName = "Test", likeCount = 100, isPinned = true, - isHeartedByUploader = true + isHeartedByUploader = true, + replies = Page(""), + replyCount = 10 ) AppTheme { From 5841eaa6d787d8cdb15ba3a2723a3bb592fa3d99 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Thu, 20 Jun 2024 07:08:12 +0530 Subject: [PATCH 12/72] Set view strategy --- .../newpipe/fragments/list/comments/CommentRepliesFragment.kt | 2 ++ 1 file changed, 2 insertions(+) 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 e1ed3041e..555d1e58d 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 @@ -6,6 +6,7 @@ import android.view.View import android.view.ViewGroup import androidx.compose.runtime.remember import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment import androidx.paging.Pager import androidx.paging.PagingConfig @@ -23,6 +24,7 @@ class CommentRepliesFragment : Fragment() { ): View { comment = requireArguments().serializable(COMMENT_KEY)!! return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { val flow = remember(comment) { Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) { From 0ec81c9e52eaf669410b43d2bd7b91be1f8fb114 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Thu, 20 Jun 2024 09:12:40 +0530 Subject: [PATCH 13/72] Fixed like count display --- .../org/schabi/newpipe/fragments/list/comments/Comment.kt | 2 +- .../newpipe/fragments/list/comments/CommentRepliesHeader.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 d7595f9fa..36e7aebf6 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 @@ -129,7 +129,7 @@ fun Comment(comment: CommentsInfoItem) { painter = painterResource(R.drawable.ic_thumb_up), contentDescription = stringResource(R.string.detail_likes_img_view_description) ) - Text(text = comment.likeCount.toString()) + Text(text = Localization.likeCount(context, comment.likeCount)) if (comment.isHeartedByUploader) { Image( 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 2c93999e8..e3f5295be 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 @@ -92,7 +92,7 @@ fun CommentRepliesHeader(comment: CommentsInfoItem) { painter = painterResource(R.drawable.ic_thumb_up), contentDescription = stringResource(R.string.detail_likes_img_view_description) ) - Text(text = comment.likeCount.toString()) + Text(text = Localization.likeCount(context, comment.likeCount)) if (comment.isHeartedByUploader) { Image( @@ -125,7 +125,7 @@ fun CommentRepliesHeaderPreview() { val comment = CommentsInfoItem( commentText = Description("Hello world!", Description.PLAIN_TEXT), uploaderName = "Test", - likeCount = 100, + likeCount = 1000, isPinned = true, isHeartedByUploader = true ) From 5bfb0449cfdbbaeba4446c790d0808272c56610f Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Fri, 21 Jun 2024 08:28:26 +0530 Subject: [PATCH 14/72] Fixed fragment title --- .../fragments/list/comments/CommentRepliesFragment.kt | 8 ++++++++ 1 file changed, 8 insertions(+) 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 555d1e58d..4a362b8f8 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,6 +4,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.remember import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy @@ -13,6 +14,7 @@ import androidx.paging.PagingConfig import org.schabi.newpipe.extractor.comments.CommentsInfoItem import org.schabi.newpipe.ktx.serializable import org.schabi.newpipe.ui.theme.AppTheme +import org.schabi.newpipe.util.Localization class CommentRepliesFragment : Fragment() { lateinit var comment: CommentsInfoItem @@ -23,6 +25,12 @@ class CommentRepliesFragment : Fragment() { savedInstanceState: Bundle? ): View { comment = requireArguments().serializable(COMMENT_KEY)!! + + val activity = requireActivity() as AppCompatActivity + val bar = activity.supportActionBar!! + bar.setDisplayShowTitleEnabled(true) + bar.title = Localization.replyCount(activity, comment.replyCount) + return ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { From be037e075619c3f220258913671ca801ebc7311f Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Fri, 21 Jun 2024 16:05:44 +0530 Subject: [PATCH 15/72] Rename .java to .kt --- .../list/comments/{CommentsFragment.java => CommentsFragment.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/org/schabi/newpipe/fragments/list/comments/{CommentsFragment.java => CommentsFragment.kt} (100%) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.kt similarity index 100% rename from app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java rename to app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.kt From 9c52e039ee00b6d980920ae9abb9319160d0d643 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Fri, 21 Jun 2024 16:05:45 +0530 Subject: [PATCH 16/72] Migrate comments fragment to Jetpack Compose --- .../fragments/detail/VideoDetailFragment.java | 17 +- .../fragments/list/comments/Comment.kt | 9 +- .../list/comments/CommentRepliesFragment.kt | 6 +- .../list/comments/CommentRepliesSource.kt | 22 -- .../{CommentReplies.kt => CommentSection.kt} | 35 ++- .../list/comments/CommentsFragment.kt | 162 ++++---------- .../fragments/list/comments/CommentsSource.kt | 36 +++ .../newpipe/info_list/InfoItemBuilder.java | 26 +-- .../newpipe/info_list/InfoListAdapter.java | 61 ++--- .../holder/CommentInfoItemHolder.java | 208 ------------------ .../schabi/newpipe/util/ExtractorHelper.java | 9 - app/src/main/res/layout/fragment_comments.xml | 71 ------ app/src/main/res/layout/list_comment_item.xml | 104 --------- 13 files changed, 161 insertions(+), 605 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesSource.kt rename app/src/main/java/org/schabi/newpipe/fragments/list/comments/{CommentReplies.kt => CommentSection.kt} (62%) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsSource.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/info_list/holder/CommentInfoItemHolder.java delete mode 100644 app/src/main/res/layout/fragment_comments.xml delete mode 100644 app/src/main/res/layout/list_comment_item.xml diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 11a315d69..a364c42cd 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -882,7 +882,7 @@ public final class VideoDetailFragment if (shouldShowComments()) { pageAdapter.addFragment( - CommentsFragment.getInstance(serviceId, url, title), COMMENTS_TAB_TAG); + CommentsFragment.getInstance(serviceId, url), COMMENTS_TAB_TAG); tabIcons.add(R.drawable.ic_comment); tabContentDescriptions.add(R.string.comments_tab_description); } @@ -1014,16 +1014,15 @@ public final class VideoDetailFragment public void scrollToComment(final CommentsInfoItem comment) { final int commentsTabPos = pageAdapter.getItemPositionByTitle(COMMENTS_TAB_TAG); - final Fragment fragment = pageAdapter.getItem(commentsTabPos); - if (!(fragment instanceof CommentsFragment)) { - return; - } + final var fragment = pageAdapter.getItem(commentsTabPos); + // TODO: Implement the scrolling with Compose. // unexpand the app bar only if scrolling to the comment succeeded - if (((CommentsFragment) fragment).scrollToComment(comment)) { - binding.appBarLayout.setExpanded(false, false); - binding.viewPager.setCurrentItem(commentsTabPos, false); - } +// if (fragment instanceof CommentsFragment commentsFragment && +// commentsFragment.scrollToComment(comment)) { +// binding.appBarLayout.setExpanded(false, false); +// binding.viewPager.setCurrentItem(commentsTabPos, false); +// } } /*////////////////////////////////////////////////////////////////////////// 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 36e7aebf6..ac254a5b3 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 @@ -83,8 +83,7 @@ fun Comment(comment: CommentsInfoItem) { .clip(CircleShape) .clickable { NavigationHelper.openCommentAuthorIfPresent( - context as FragmentActivity, - comment + context as FragmentActivity, comment ) } ) @@ -140,7 +139,11 @@ fun Comment(comment: CommentsInfoItem) { } if (comment.replies != null) { - TextButton(onClick = { /*TODO*/ }) { + TextButton(onClick = { + NavigationHelper.openCommentRepliesFragment( + context as FragmentActivity, comment + ) + }) { Text( text = pluralStringResource( R.plurals.replies, comment.replyCount, comment.replyCount.toString() 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 4a362b8f8..e25b3a960 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 @@ -31,17 +31,17 @@ class CommentRepliesFragment : Fragment() { bar.setDisplayShowTitleEnabled(true) bar.title = Localization.replyCount(activity, comment.replyCount) - return ComposeView(requireContext()).apply { + return ComposeView(activity).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { val flow = remember(comment) { Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) { - CommentRepliesSource(comment) + CommentsSource(comment.serviceId, comment.url, comment.replies) }.flow } AppTheme { - CommentReplies(comment = comment, flow = flow) + CommentSection(parentComment = comment, flow = flow) } } } 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 deleted file mode 100644 index ed602d917..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentRepliesSource.kt +++ /dev/null @@ -1,22 +0,0 @@ -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/fragments/list/comments/CommentReplies.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentSection.kt similarity index 62% rename from app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentReplies.kt rename to app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentSection.kt index 53a4fa4bf..45a8b5e72 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentReplies.kt +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentSection.kt @@ -15,16 +15,18 @@ import org.schabi.newpipe.extractor.stream.Description import org.schabi.newpipe.ui.theme.AppTheme @Composable -fun CommentReplies( - comment: CommentsInfoItem, - flow: Flow> +fun CommentSection( + flow: Flow>, + parentComment: CommentsInfoItem? = null, ) { val replies = flow.collectAsLazyPagingItems() LazyColumn { - item { - CommentRepliesHeader(comment = comment) - HorizontalDivider(thickness = 1.dp) + if (parentComment != null) { + item { + CommentRepliesHeader(comment = parentComment) + HorizontalDivider(thickness = 1.dp) + } } items(replies.itemCount) { @@ -33,6 +35,25 @@ fun CommentReplies( } } +@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun CommentSectionPreview() { + val comment1 = CommentsInfoItem( + commentText = Description("This is a comment", Description.PLAIN_TEXT), + uploaderName = "Test", + ) + val comment2 = CommentsInfoItem( + commentText = Description("This is another comment.
This is another line.", Description.HTML), + uploaderName = "Test 2", + ) + val flow = flowOf(PagingData.from(listOf(comment1, comment2))) + + AppTheme { + CommentSection(flow = flow) + } +} + @Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -56,6 +77,6 @@ private fun CommentRepliesPreview() { val flow = flowOf(PagingData.from(listOf(reply1, reply2))) AppTheme { - CommentReplies(comment = comment, flow = flow) + CommentSection(parentComment = comment, flow = flow) } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.kt index e25e02794..decd9391c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.kt @@ -1,123 +1,55 @@ -package org.schabi.newpipe.fragments.list.comments; +package org.schabi.newpipe.fragments.list.comments -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; +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 androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.paging.Pager +import androidx.paging.PagingConfig +import org.schabi.newpipe.ui.theme.AppTheme +import org.schabi.newpipe.util.NO_SERVICE_ID -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; +class CommentsFragment : Fragment() { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val arguments = requireArguments() + val serviceId = arguments.getInt(SERVICE_ID, NO_SERVICE_ID) + val url = arguments.getString(URL) -import org.schabi.newpipe.R; -import org.schabi.newpipe.error.UserAction; -import org.schabi.newpipe.extractor.ListExtractor; -import org.schabi.newpipe.extractor.comments.CommentsInfo; -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.ViewUtils; -import org.schabi.newpipe.util.ExtractorHelper; + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + val flow = remember(serviceId, url) { + Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) { + CommentsSource(serviceId, url, null) + }.flow + } -import io.reactivex.rxjava3.core.Single; -import io.reactivex.rxjava3.disposables.CompositeDisposable; - -public class CommentsFragment extends BaseListInfoFragment { - private final CompositeDisposable disposables = new CompositeDisposable(); - - private TextView emptyStateDesc; - - public static CommentsFragment getInstance(final int serviceId, final String url, - final String name) { - final CommentsFragment instance = new CommentsFragment(); - instance.setInitialData(serviceId, url, name); - return instance; - } - - public CommentsFragment() { - super(UserAction.REQUESTED_COMMENTS); - } - - @Override - protected void initViews(final View rootView, final Bundle savedInstanceState) { - super.initViews(rootView, savedInstanceState); - - emptyStateDesc = rootView.findViewById(R.id.empty_state_desc); - } - - /*////////////////////////////////////////////////////////////////////////// - // LifeCycle - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - @Nullable final ViewGroup container, - @Nullable final Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_comments, container, false); - } - - @Override - public void onDestroy() { - super.onDestroy(); - disposables.clear(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Load and handle - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected Single> loadMoreItemsLogic() { - return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPage); - } - - @Override - protected Single loadResult(final boolean forceLoad) { - return ExtractorHelper.getCommentsInfo(serviceId, url, forceLoad); - } - - /*////////////////////////////////////////////////////////////////////////// - // Contract - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void handleResult(@NonNull final CommentsInfo result) { - super.handleResult(result); - - emptyStateDesc.setText( - result.isCommentsDisabled() - ? R.string.comments_are_disabled - : R.string.no_comments); - - ViewUtils.slideUp(requireView(), 120, 150, 0.06f); - disposables.clear(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void setTitle(final String title) { } - - @Override - public void onCreateOptionsMenu(@NonNull final Menu menu, - @NonNull final MenuInflater inflater) { } - - @Override - protected ItemViewMode getItemViewMode() { - return ItemViewMode.LIST; - } - - public boolean scrollToComment(final CommentsInfoItem comment) { - final int position = infoListAdapter.getItemsList().indexOf(comment); - if (position < 0) { - return false; + AppTheme { + CommentSection(flow = flow) + } + } } + } - itemsList.scrollToPosition(position); - return true; + companion object { + private const val SERVICE_ID = "serviceId" + private const val URL = "url" + + @JvmStatic + fun getInstance(serviceId: Int, url: String?) = CommentsFragment().apply { + arguments = bundleOf( + SERVICE_ID to serviceId, + URL to url + ) + } } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsSource.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsSource.kt new file mode 100644 index 000000000..6288efaec --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsSource.kt @@ -0,0 +1,36 @@ +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 CommentsSource( + private val serviceId: Int, + private val url: String?, + private val repliesPage: Page? +) : RxPagingSource() { + override fun loadSingle(params: LoadParams): Single> { + // repliesPage is non-null only when used to load the comment replies + val nextKey = params.key ?: repliesPage + + return nextKey?.let { + ExtractorHelper.getMoreCommentItems(serviceId, url, it) + .subscribeOn(Schedulers.io()) + .map { LoadResult.Page(it.items, null, it.nextPage) } + } ?: ExtractorHelper.getCommentsInfo(serviceId, url, false) + .subscribeOn(Schedulers.io()) + .map { + if (it.isCommentsDisabled) { + LoadResult.Invalid() + } else { + LoadResult.Page(it.relatedItems, null, it.nextPage) + } + } + } + + override fun getRefreshKey(state: PagingState) = null +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java index d959c6327..a1526af28 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java @@ -13,7 +13,6 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder; -import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder; import org.schabi.newpipe.info_list.holder.InfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder; @@ -75,21 +74,16 @@ public class InfoItemBuilder { private InfoItemHolder holderFromInfoType(@NonNull final ViewGroup parent, @NonNull final InfoItem.InfoType infoType, final boolean useMiniVariant) { - switch (infoType) { - case STREAM: - return useMiniVariant ? new StreamMiniInfoItemHolder(this, parent) - : new StreamInfoItemHolder(this, parent); - case CHANNEL: - return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) - : new ChannelInfoItemHolder(this, parent); - case PLAYLIST: - return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent) - : new PlaylistInfoItemHolder(this, parent); - case COMMENT: - return new CommentInfoItemHolder(this, parent); - default: - throw new RuntimeException("InfoType not expected = " + infoType.name()); - } + return switch (infoType) { + case STREAM -> useMiniVariant ? new StreamMiniInfoItemHolder(this, parent) + : new StreamInfoItemHolder(this, parent); + case CHANNEL -> useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) + : new ChannelInfoItemHolder(this, parent); + case PLAYLIST -> useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent) + : new PlaylistInfoItemHolder(this, parent); + case COMMENT -> + throw new IllegalArgumentException("Comments should be rendered using Compose"); + }; } public Context getContext() { diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index 575568c00..e7cf9ba9a 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -21,7 +21,6 @@ import org.schabi.newpipe.info_list.holder.ChannelCardInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder; -import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder; import org.schabi.newpipe.info_list.holder.InfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistCardInfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder; @@ -283,46 +282,32 @@ public class InfoListAdapter extends RecyclerView.Adapter new HFHolder(headerSupplier.get()); + case FOOTER_TYPE -> new HFHolder(PignateFooterBinding + .inflate(layoutInflater, parent, false) + .getRoot() + ); + case MINI_STREAM_HOLDER_TYPE -> new StreamMiniInfoItemHolder(infoItemBuilder, parent); + case STREAM_HOLDER_TYPE -> new StreamInfoItemHolder(infoItemBuilder, parent); + case GRID_STREAM_HOLDER_TYPE -> new StreamGridInfoItemHolder(infoItemBuilder, parent); + case CARD_STREAM_HOLDER_TYPE -> new StreamCardInfoItemHolder(infoItemBuilder, parent); + case MINI_CHANNEL_HOLDER_TYPE -> new ChannelMiniInfoItemHolder(infoItemBuilder, parent); + case CHANNEL_HOLDER_TYPE -> new ChannelInfoItemHolder(infoItemBuilder, parent); + case CARD_CHANNEL_HOLDER_TYPE -> new ChannelCardInfoItemHolder(infoItemBuilder, parent); + case GRID_CHANNEL_HOLDER_TYPE -> new ChannelGridInfoItemHolder(infoItemBuilder, parent); + case MINI_PLAYLIST_HOLDER_TYPE -> + new PlaylistMiniInfoItemHolder(infoItemBuilder, parent); + case PLAYLIST_HOLDER_TYPE -> new PlaylistInfoItemHolder(infoItemBuilder, parent); + case GRID_PLAYLIST_HOLDER_TYPE -> + new PlaylistGridInfoItemHolder(infoItemBuilder, parent); + case CARD_PLAYLIST_HOLDER_TYPE -> + new PlaylistCardInfoItemHolder(infoItemBuilder, parent); + default -> new FallbackViewHolder(new View(parent.getContext())); + }; } @Override diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentInfoItemHolder.java deleted file mode 100644 index a3316d3fe..000000000 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentInfoItemHolder.java +++ /dev/null @@ -1,208 +0,0 @@ -package org.schabi.newpipe.info_list.holder; - -import static org.schabi.newpipe.util.ServiceHelper.getServiceById; -import static org.schabi.newpipe.util.text.TouchUtils.getOffsetForHorizontalLine; - -import android.text.Spanned; -import android.text.method.LinkMovementMethod; -import android.text.style.ClickableSpan; -import android.text.style.URLSpan; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.fragment.app.FragmentActivity; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.comments.CommentsInfoItem; -import org.schabi.newpipe.info_list.InfoItemBuilder; -import org.schabi.newpipe.local.history.HistoryRecordManager; -import org.schabi.newpipe.util.DeviceUtils; -import org.schabi.newpipe.util.Localization; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.external_communication.ShareUtils; -import org.schabi.newpipe.util.image.CoilHelper; -import org.schabi.newpipe.util.image.ImageStrategy; -import org.schabi.newpipe.util.text.TextEllipsizer; - -public class CommentInfoItemHolder extends InfoItemHolder { - - private static final int COMMENT_DEFAULT_LINES = 2; - private final int commentHorizontalPadding; - private final int commentVerticalPadding; - - private final RelativeLayout itemRoot; - private final ImageView itemThumbnailView; - private final TextView itemContentView; - private final ImageView itemThumbsUpView; - private final TextView itemLikesCountView; - private final TextView itemTitleView; - private final ImageView itemHeartView; - private final ImageView itemPinnedView; - private final Button repliesButton; - - @NonNull - private final TextEllipsizer textEllipsizer; - - public CommentInfoItemHolder(final InfoItemBuilder infoItemBuilder, - final ViewGroup parent) { - super(infoItemBuilder, R.layout.list_comment_item, parent); - - itemRoot = itemView.findViewById(R.id.itemRoot); - itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); - itemContentView = itemView.findViewById(R.id.itemCommentContentView); - itemThumbsUpView = itemView.findViewById(R.id.detail_thumbs_up_img_view); - itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view); - itemTitleView = itemView.findViewById(R.id.itemTitleView); - itemHeartView = itemView.findViewById(R.id.detail_heart_image_view); - itemPinnedView = itemView.findViewById(R.id.detail_pinned_view); - repliesButton = itemView.findViewById(R.id.replies_button); - - commentHorizontalPadding = (int) infoItemBuilder.getContext() - .getResources().getDimension(R.dimen.comments_horizontal_padding); - commentVerticalPadding = (int) infoItemBuilder.getContext() - .getResources().getDimension(R.dimen.comments_vertical_padding); - - textEllipsizer = new TextEllipsizer(itemContentView, COMMENT_DEFAULT_LINES, null); - textEllipsizer.setStateChangeListener(isEllipsized -> { - if (Boolean.TRUE.equals(isEllipsized)) { - denyLinkFocus(); - } else { - determineMovementMethod(); - } - }); - } - - @Override - public void updateFromItem(final InfoItem infoItem, - final HistoryRecordManager historyRecordManager) { - if (!(infoItem instanceof CommentsInfoItem item)) { - return; - } - - // load the author avatar - CoilHelper.INSTANCE.loadAvatar(itemThumbnailView, item.getUploaderAvatars()); - if (ImageStrategy.shouldLoadImages()) { - itemThumbnailView.setVisibility(View.VISIBLE); - itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding, - commentVerticalPadding, commentVerticalPadding); - } else { - itemThumbnailView.setVisibility(View.GONE); - itemRoot.setPadding(commentHorizontalPadding, commentVerticalPadding, - commentHorizontalPadding, commentVerticalPadding); - } - itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item)); - - - // setup the top row, with pinned icon, author name and comment date - itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE); - itemTitleView.setText(Localization.concatenateStrings(item.getUploaderName(), - Localization.relativeTimeOrTextual(itemBuilder.getContext(), item.getUploadDate(), - item.getTextualUploadDate()))); - - - // setup bottom row, with likes, heart and replies button - itemLikesCountView.setText( - Localization.likeCount(itemBuilder.getContext(), item.getLikeCount())); - - itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE); - - final boolean hasReplies = item.getReplies() != null; - repliesButton.setOnClickListener(hasReplies ? v -> openCommentReplies(item) : null); - repliesButton.setVisibility(hasReplies ? View.VISIBLE : View.GONE); - repliesButton.setText(hasReplies - ? Localization.replyCount(itemBuilder.getContext(), item.getReplyCount()) : ""); - ((RelativeLayout.LayoutParams) itemThumbsUpView.getLayoutParams()).topMargin = - hasReplies ? 0 : DeviceUtils.dpToPx(6, itemBuilder.getContext()); - - - // setup comment content and click listeners to expand/ellipsize it - textEllipsizer.setStreamingService(getServiceById(item.getServiceId())); - textEllipsizer.setStreamUrl(item.getUrl()); - textEllipsizer.setContent(item.getCommentText()); - textEllipsizer.ellipsize(); - - //noinspection ClickableViewAccessibility - itemContentView.setOnTouchListener((v, event) -> { - final CharSequence text = itemContentView.getText(); - if (text instanceof Spanned buffer) { - final int action = event.getAction(); - - if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) { - final int offset = getOffsetForHorizontalLine(itemContentView, event); - final var links = buffer.getSpans(offset, offset, ClickableSpan.class); - - if (links.length != 0) { - if (action == MotionEvent.ACTION_UP) { - links[0].onClick(itemContentView); - } - // we handle events that intersect links, so return true - return true; - } - } - } - return false; - }); - - itemView.setOnClickListener(view -> { - textEllipsizer.toggle(); - if (itemBuilder.getOnCommentsSelectedListener() != null) { - itemBuilder.getOnCommentsSelectedListener().selected(item); - } - }); - - itemView.setOnLongClickListener(view -> { - if (DeviceUtils.isTv(itemBuilder.getContext())) { - openCommentAuthor(item); - } else { - final CharSequence text = itemContentView.getText(); - if (text != null) { - ShareUtils.copyToClipboard(itemBuilder.getContext(), text.toString()); - } - } - return true; - }); - } - - private void openCommentAuthor(@NonNull final CommentsInfoItem item) { - NavigationHelper.openCommentAuthorIfPresent((FragmentActivity) itemBuilder.getContext(), - item); - } - - private void openCommentReplies(@NonNull final CommentsInfoItem item) { - NavigationHelper.openCommentRepliesFragment((FragmentActivity) itemBuilder.getContext(), - item); - } - - private void allowLinkFocus() { - itemContentView.setMovementMethod(LinkMovementMethod.getInstance()); - } - - private void denyLinkFocus() { - itemContentView.setMovementMethod(null); - } - - private boolean shouldFocusLinks() { - if (itemView.isInTouchMode()) { - return false; - } - - final URLSpan[] urls = itemContentView.getUrls(); - - return urls != null && urls.length != 0; - } - - private void determineMovementMethod() { - if (shouldFocusLinks()) { - allowLinkFocus(); - } else { - denyLinkFocus(); - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java index 066d5f570..abf8d24c1 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -155,15 +155,6 @@ public final class ExtractorHelper { CommentsInfo.getInfo(NewPipe.getService(serviceId), url))); } - public static Single> getMoreCommentItems( - final int serviceId, - final CommentsInfo info, - final Page nextPage) { - checkServiceId(serviceId); - return Single.fromCallable(() -> - CommentsInfo.getMoreItems(NewPipe.getService(serviceId), info, nextPage)); - } - public static Single> getMoreCommentItems( final int serviceId, final String url, diff --git a/app/src/main/res/layout/fragment_comments.xml b/app/src/main/res/layout/fragment_comments.xml deleted file mode 100644 index 2a8c747cd..000000000 --- a/app/src/main/res/layout/fragment_comments.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/list_comment_item.xml b/app/src/main/res/layout/list_comment_item.xml deleted file mode 100644 index 631ab204b..000000000 --- a/app/src/main/res/layout/list_comment_item.xml +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - - - - - - - - - - - - -