1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2024-12-24 00:50: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 { composeOptions {
kotlinCompilerExtensionVersion = "1.5.13" kotlinCompilerExtensionVersion = "1.5.14"
} }
} }
@ -289,11 +289,15 @@ dependencies {
implementation "org.ocpsoft.prettytime:prettytime:5.0.8.Final" implementation "org.ocpsoft.prettytime:prettytime:5.0.8.Final"
// Jetpack Compose // 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.compose.material3:material3'
implementation 'androidx.activity:activity-compose' implementation 'androidx.activity:activity-compose'
implementation 'androidx.compose.ui:ui-tooling-preview' 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 **/ /** Debugging **/
// Memory leak detection // Memory leak detection
debugImplementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}" 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.error.ErrorUtil;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService; 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.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance; import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
import org.schabi.newpipe.fragments.BackPressable; 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 // the root comment is the comment for which the user opened the replies page
@Nullable final CommentRepliesFragment repliesFragment = final var repliesFragment = (CommentRepliesFragment)
(CommentRepliesFragment) fm.findFragmentByTag(CommentRepliesFragment.TAG); fm.findFragmentByTag(CommentRepliesFragment.TAG);
@Nullable final CommentsInfoItem rootComment = final var rootComment = repliesFragment == null ? null : repliesFragment.getComment();
repliesFragment == null ? null : repliesFragment.getCommentsInfoItem();
// sometimes this function pops the backstack, other times it's handled by the system // sometimes this function pops the backstack, other times it's handled by the system
if (popBackStack) { if (popBackStack) {

View File

@ -73,6 +73,7 @@ fun Comment(comment: CommentsInfoItem) {
color = MaterialTheme.colorScheme.secondary color = MaterialTheme.colorScheme.secondary
) )
// TODO: Handle HTML and Markdown formats.
Text( Text(
text = comment.commentText.content, text = comment.commentText.content,
// If the comment is expanded, we display all its 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 = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable @Composable
fun CommentPreview() { private fun CommentPreview() {
val comment = CommentsInfoItem(1, "", "") val comment = CommentsInfoItem(
comment.commentText = Description("Hello world!\n\nThis line should be hidden by default.", Description.PLAIN_TEXT) commentText = Description("Hello world!\n\nThis line should be hidden by default.", Description.PLAIN_TEXT),
comment.uploaderName = "Test" uploaderName = "Test",
comment.likeCount = 100 )
comment.isHeartedByUploader = true
comment.isPinned = true
AppTheme { AppTheme {
Comment(comment) 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.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import icepick.State import androidx.fragment.app.Fragment
import io.reactivex.rxjava3.core.Single import androidx.paging.Pager
import androidx.paging.PagingConfig
import io.reactivex.rxjava3.disposables.CompositeDisposable 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.extractor.comments.CommentsInfoItem
import org.schabi.newpipe.fragments.list.BaseListInfoFragment import org.schabi.newpipe.ktx.serializable
import org.schabi.newpipe.info_list.ItemViewMode
import org.schabi.newpipe.ui.theme.AppTheme 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) { class CommentRepliesFragment : Fragment() {
/**
* @return the comment to which the replies are shown
*/
@State
lateinit var commentsInfoItem: CommentsInfoItem // the comment to show replies of
private val disposables = CompositeDisposable() private val disposables = CompositeDisposable()
lateinit var comment: CommentsInfoItem
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( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): 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
}
AppTheme {
CommentReplies(comment = comment, flow = flow, disposables = disposables)
}
}
}
} }
override fun onDestroyView() { override fun onDestroyView() {
disposables.clear()
super.onDestroyView() super.onDestroyView()
} disposables.clear()
override fun getListHeaderSupplier(): Supplier<View> {
return Supplier {
ComposeView(requireContext()).apply {
setContent {
AppTheme {
CommentRepliesHeader(commentsInfoItem, 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
} }
companion object { companion object {
@JvmField @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( @Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
name = "Light mode", @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
uiMode = Configuration.UI_MODE_NIGHT_NO
)
@Preview(
name = "Dark mode",
uiMode = Configuration.UI_MODE_NIGHT_YES
)
@Composable @Composable
fun CommentRepliesHeaderPreview() { fun CommentRepliesHeaderPreview() {
val disposables = CompositeDisposable() 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 package org.schabi.newpipe.ktx
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import androidx.core.os.BundleCompat import androidx.core.os.BundleCompat
import java.io.Serializable
import kotlin.reflect.safeCast
inline fun <reified T : Parcelable> Bundle.parcelableArrayList(key: String?): ArrayList<T>? { inline fun <reified T : Parcelable> Bundle.parcelableArrayList(key: String?): ArrayList<T>? {
return BundleCompat.getParcelableArrayList(this, key, T::class.java) 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.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
@ -503,8 +504,11 @@ public final class NavigationHelper {
public static void openCommentRepliesFragment(@NonNull final FragmentActivity activity, public static void openCommentRepliesFragment(@NonNull final FragmentActivity activity,
@NonNull final CommentsInfoItem comment) { @NonNull final CommentsInfoItem comment) {
final var bundle = new Bundle();
bundle.putSerializable(CommentRepliesFragment.COMMENT_KEY, comment);
defaultTransaction(activity.getSupportFragmentManager()) defaultTransaction(activity.getSupportFragmentManager())
.replace(R.id.fragment_holder, new CommentRepliesFragment(comment), .replace(R.id.fragment_holder, CommentRepliesFragment.class, bundle,
CommentRepliesFragment.TAG) CommentRepliesFragment.TAG)
.addToBackStack(CommentRepliesFragment.TAG) .addToBackStack(CommentRepliesFragment.TAG)
.commit(); .commit();

View File

@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.9.23' ext.kotlin_version = '1.9.24'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()