1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2024-12-23 16:40:32 +00:00

Update replies fragment to use the comment composable as well

This commit is contained in:
Isira Seneviratne 2024-06-18 07:59:24 +05:30
parent 1620668966
commit 341cc37ce7
11 changed files with 163 additions and 123 deletions

View File

@ -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}"

View File

@ -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) {

View File

@ -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)

View File

@ -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<PagingData<CommentsInfoItem>>,
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.<br>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())
}
}

View File

@ -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<CommentsInfoItem, CommentRepliesInfo>(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<CommentsInfoItem>(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<View> {
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<Any>) {
super.writeTo(objectsToSave)
objectsToSave.add(commentsInfoItem)
}
@Throws(Exception::class)
override fun readFrom(savedObjects: Queue<Any>) {
super.readFrom(savedObjects)
commentsInfoItem = savedObjects.poll() as CommentsInfoItem
}
/*//////////////////////////////////////////////////////////////////////////
// Data loading
////////////////////////////////////////////////////////////////////////// */
override fun loadResult(forceLoad: Boolean): Single<CommentRepliesInfo> {
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<InfoItemsPage<CommentsInfoItem?>>? {
// commentsInfoItem.getUrl() should contain the url of the original
// ListInfo<CommentsInfoItem>, 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"
}
}

View File

@ -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()

View File

@ -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<CommentsInfoItem> {
/**
* 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
}
}

View File

@ -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<Page, CommentsInfoItem>() {
override fun loadSingle(params: LoadParams<Page>): Single<LoadResult<Page, CommentsInfoItem>> {
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<Page, CommentsInfoItem>) = null
}

View File

@ -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 <reified T : Parcelable> Bundle.parcelableArrayList(key: String?): ArrayList<T>? {
return BundleCompat.getParcelableArrayList(this, key, T::class.java)
}
inline fun <reified T : Serializable> Bundle.serializable(key: String?): T? {
return getSerializable(this, key, T::class.java)
}
fun <T : Serializable> getSerializable(bundle: Bundle, key: String?, clazz: Class<T>): 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))
}
}

View File

@ -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();

View File

@ -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()