mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-30 23:03:00 +00:00 
			
		
		
		
	Migrate comments fragment to Jetpack Compose
This commit is contained in:
		| @@ -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); | ||||
| //        } | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -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<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 | ||||
| } | ||||
| @@ -15,17 +15,19 @@ import org.schabi.newpipe.extractor.stream.Description | ||||
| import org.schabi.newpipe.ui.theme.AppTheme | ||||
| 
 | ||||
| @Composable | ||||
| fun CommentReplies( | ||||
|     comment: CommentsInfoItem, | ||||
|     flow: Flow<PagingData<CommentsInfoItem>> | ||||
| fun CommentSection( | ||||
|     flow: Flow<PagingData<CommentsInfoItem>>, | ||||
|     parentComment: CommentsInfoItem? = null, | ||||
| ) { | ||||
|     val replies = flow.collectAsLazyPagingItems() | ||||
| 
 | ||||
|     LazyColumn { | ||||
|         if (parentComment != null) { | ||||
|             item { | ||||
|             CommentRepliesHeader(comment = comment) | ||||
|                 CommentRepliesHeader(comment = parentComment) | ||||
|                 HorizontalDivider(thickness = 1.dp) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         items(replies.itemCount) { | ||||
|             Comment(comment = replies[it]!!) | ||||
| @@ -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.<br>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) | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
|  | ||||
| import io.reactivex.rxjava3.core.Single; | ||||
| import io.reactivex.rxjava3.disposables.CompositeDisposable; | ||||
|  | ||||
| public class CommentsFragment extends BaseListInfoFragment<CommentsInfoItem, CommentsInfo> { | ||||
|     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; | ||||
|         return ComposeView(requireContext()).apply { | ||||
|             setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) | ||||
|             setContent { | ||||
|                 val flow = remember(serviceId, url) { | ||||
|                     Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) { | ||||
|                         CommentsSource(serviceId, url, null) | ||||
|                     }.flow | ||||
|                 } | ||||
|  | ||||
|     public CommentsFragment() { | ||||
|         super(UserAction.REQUESTED_COMMENTS); | ||||
|                 AppTheme { | ||||
|                     CommentSection(flow = flow) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void initViews(final View rootView, final Bundle savedInstanceState) { | ||||
|         super.initViews(rootView, savedInstanceState); | ||||
|     companion object { | ||||
|         private const val SERVICE_ID = "serviceId" | ||||
|         private const val URL = "url" | ||||
|  | ||||
|         emptyStateDesc = rootView.findViewById(R.id.empty_state_desc); | ||||
|         @JvmStatic | ||||
|         fun getInstance(serviceId: Int, url: String?) = CommentsFragment().apply { | ||||
|             arguments = bundleOf( | ||||
|                 SERVICE_ID to serviceId, | ||||
|                 URL to url | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // 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<ListExtractor.InfoItemsPage<CommentsInfoItem>> loadMoreItemsLogic() { | ||||
|         return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPage); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected Single<CommentsInfo> 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; | ||||
|         } | ||||
|  | ||||
|         itemsList.scrollToPosition(position); | ||||
|         return true; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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<Page, CommentsInfoItem>() { | ||||
|     override fun loadSingle(params: LoadParams<Page>): Single<LoadResult<Page, CommentsInfoItem>> { | ||||
|         // 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<Page, CommentsInfoItem>) = null | ||||
| } | ||||
| @@ -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) | ||||
|         return switch (infoType) { | ||||
|             case STREAM -> useMiniVariant ? new StreamMiniInfoItemHolder(this, parent) | ||||
|                     : new StreamInfoItemHolder(this, parent); | ||||
|             case CHANNEL: | ||||
|                 return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) | ||||
|             case CHANNEL -> useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) | ||||
|                     : new ChannelInfoItemHolder(this, parent); | ||||
|             case PLAYLIST: | ||||
|                 return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent) | ||||
|             case PLAYLIST -> 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()); | ||||
|         } | ||||
|             case COMMENT -> | ||||
|                     throw new IllegalArgumentException("Comments should be rendered using Compose"); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     public Context getContext() { | ||||
|   | ||||
| @@ -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<RecyclerView.ViewHolde | ||||
|             Log.d(TAG, "onCreateViewHolder() called with: " | ||||
|                     + "parent = [" + parent + "], type = [" + type + "]"); | ||||
|         } | ||||
|         switch (type) { | ||||
|         return switch (type) { | ||||
|             // #4475 and #3368 | ||||
|             // Always create a new instance otherwise the same instance | ||||
|             // is sometimes reused which causes a crash | ||||
|             case HEADER_TYPE: | ||||
|                 return new HFHolder(headerSupplier.get()); | ||||
|             case FOOTER_TYPE: | ||||
|                 return new HFHolder(PignateFooterBinding | ||||
|             case HEADER_TYPE -> new HFHolder(headerSupplier.get()); | ||||
|             case FOOTER_TYPE -> new HFHolder(PignateFooterBinding | ||||
|                     .inflate(layoutInflater, parent, false) | ||||
|                     .getRoot() | ||||
|             ); | ||||
|             case MINI_STREAM_HOLDER_TYPE: | ||||
|                 return new StreamMiniInfoItemHolder(infoItemBuilder, parent); | ||||
|             case STREAM_HOLDER_TYPE: | ||||
|                 return new StreamInfoItemHolder(infoItemBuilder, parent); | ||||
|             case GRID_STREAM_HOLDER_TYPE: | ||||
|                 return new StreamGridInfoItemHolder(infoItemBuilder, parent); | ||||
|             case CARD_STREAM_HOLDER_TYPE: | ||||
|                 return new StreamCardInfoItemHolder(infoItemBuilder, parent); | ||||
|             case MINI_CHANNEL_HOLDER_TYPE: | ||||
|                 return new ChannelMiniInfoItemHolder(infoItemBuilder, parent); | ||||
|             case CHANNEL_HOLDER_TYPE: | ||||
|                 return new ChannelInfoItemHolder(infoItemBuilder, parent); | ||||
|             case CARD_CHANNEL_HOLDER_TYPE: | ||||
|                 return new ChannelCardInfoItemHolder(infoItemBuilder, parent); | ||||
|             case GRID_CHANNEL_HOLDER_TYPE: | ||||
|                 return new ChannelGridInfoItemHolder(infoItemBuilder, parent); | ||||
|             case MINI_PLAYLIST_HOLDER_TYPE: | ||||
|                 return new PlaylistMiniInfoItemHolder(infoItemBuilder, parent); | ||||
|             case PLAYLIST_HOLDER_TYPE: | ||||
|                 return new PlaylistInfoItemHolder(infoItemBuilder, parent); | ||||
|             case GRID_PLAYLIST_HOLDER_TYPE: | ||||
|                 return new PlaylistGridInfoItemHolder(infoItemBuilder, parent); | ||||
|             case CARD_PLAYLIST_HOLDER_TYPE: | ||||
|                 return new PlaylistCardInfoItemHolder(infoItemBuilder, parent); | ||||
|             case COMMENT_HOLDER_TYPE: | ||||
|                 return new CommentInfoItemHolder(infoItemBuilder, parent); | ||||
|             default: | ||||
|                 return new FallbackViewHolder(new View(parent.getContext())); | ||||
|         } | ||||
|             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 | ||||
|   | ||||
| @@ -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(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -155,15 +155,6 @@ public final class ExtractorHelper { | ||||
|                         CommentsInfo.getInfo(NewPipe.getService(serviceId), url))); | ||||
|     } | ||||
|  | ||||
|     public static Single<InfoItemsPage<CommentsInfoItem>> 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<InfoItemsPage<CommentsInfoItem>> getMoreCommentItems( | ||||
|             final int serviceId, | ||||
|             final String url, | ||||
|   | ||||
| @@ -1,71 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent"> | ||||
|  | ||||
|     <org.schabi.newpipe.views.NewPipeRecyclerView | ||||
|         android:id="@+id/items_list" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" | ||||
|         android:scrollbars="vertical" | ||||
|         tools:listitem="@layout/list_comment_item" /> | ||||
|  | ||||
|     <ProgressBar | ||||
|         android:id="@+id/loading_progress_bar" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_centerInParent="true" | ||||
|         android:indeterminate="true" | ||||
|         android:visibility="gone" | ||||
|         tools:visibility="visible" /> | ||||
|  | ||||
|     <LinearLayout | ||||
|         android:id="@+id/empty_state_view" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_centerHorizontal="true" | ||||
|         android:orientation="vertical" | ||||
|         android:paddingTop="85dp" | ||||
|         android:visibility="gone" | ||||
|         tools:visibility="visible"> | ||||
|  | ||||
|         <org.schabi.newpipe.views.NewPipeTextView | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_gravity="center" | ||||
|             android:layout_marginBottom="10dp" | ||||
|             android:fontFamily="monospace" | ||||
|             android:text="(╯°-°)╯" | ||||
|             android:textSize="35sp" | ||||
|             tools:ignore="HardcodedText,UnusedAttribute" /> | ||||
|  | ||||
|         <org.schabi.newpipe.views.NewPipeTextView | ||||
|             android:id="@+id/empty_state_desc" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_gravity="center" | ||||
|             android:text="@string/empty_view_no_comments" | ||||
|             android:textSize="24sp" /> | ||||
|  | ||||
|     </LinearLayout> | ||||
|  | ||||
|     <!--ERROR PANEL--> | ||||
|     <include | ||||
|         android:id="@+id/error_panel" | ||||
|         layout="@layout/error_panel" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_centerHorizontal="true" | ||||
|         android:layout_marginTop="16dp" | ||||
|         android:visibility="gone" | ||||
|         tools:visibility="visible" /> | ||||
|  | ||||
|     <View | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="4dp" | ||||
|         android:layout_alignParentTop="true" | ||||
|         android:background="?attr/toolbar_shadow" | ||||
|         android:visibility="gone" /> | ||||
|  | ||||
| </RelativeLayout> | ||||
| @@ -1,104 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:id="@+id/itemRoot" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:background="?attr/selectableItemBackground" | ||||
|     android:clickable="true" | ||||
|     android:focusable="true" | ||||
|     android:padding="@dimen/comments_vertical_padding"> | ||||
|  | ||||
|     <com.google.android.material.imageview.ShapeableImageView | ||||
|         android:id="@+id/itemThumbnailView" | ||||
|         android:layout_width="42dp" | ||||
|         android:layout_height="42dp" | ||||
|         android:layout_alignParentStart="true" | ||||
|         android:layout_alignParentTop="true" | ||||
|         android:layout_marginEnd="@dimen/comment_item_avatar_right_margin" | ||||
|         android:focusable="false" | ||||
|         android:src="@drawable/placeholder_person" | ||||
|         app:shapeAppearance="@style/CircularImageView" | ||||
|         tools:ignore="RtlHardcoded" /> | ||||
|  | ||||
|     <ImageView | ||||
|         android:id="@+id/detail_pinned_view" | ||||
|         android:layout_width="@dimen/video_item_detail_pinned_image_width" | ||||
|         android:layout_height="@dimen/video_item_detail_pinned_image_height" | ||||
|         android:layout_alignParentTop="true" | ||||
|         android:layout_marginEnd="@dimen/video_item_detail_pinned_right_margin" | ||||
|         android:layout_toEndOf="@+id/itemThumbnailView" | ||||
|         android:contentDescription="@string/detail_pinned_comment_view_description" | ||||
|         android:src="@drawable/ic_pin" /> | ||||
|  | ||||
|     <org.schabi.newpipe.views.NewPipeTextView | ||||
|         android:id="@+id/itemTitleView" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_alignParentTop="true" | ||||
|         android:layout_toEndOf="@+id/detail_pinned_view" | ||||
|         android:ellipsize="end" | ||||
|         android:lines="1" | ||||
|         android:textAppearance="?android:attr/textAppearanceSmall" | ||||
|         android:textSize="@dimen/comment_item_title_text_size" | ||||
|         tools:text="Author Name, Lorem ipsum • 5 months ago" /> | ||||
|  | ||||
|     <org.schabi.newpipe.views.NewPipeTextView | ||||
|         android:id="@+id/itemCommentContentView" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_below="@id/itemTitleView" | ||||
|         android:layout_marginTop="6dp" | ||||
|         android:layout_toEndOf="@+id/itemThumbnailView" | ||||
|         android:textAppearance="?android:attr/textAppearanceLarge" | ||||
|         android:textSize="@dimen/comment_item_content_text_size" | ||||
|         tools:text="@tools:sample/lorem/random[1]" /> | ||||
|  | ||||
|     <ImageView | ||||
|         android:id="@+id/detail_thumbs_up_img_view" | ||||
|         android:layout_width="@dimen/video_item_detail_like_image_width" | ||||
|         android:layout_height="@dimen/video_item_detail_like_image_height" | ||||
|         android:layout_below="@id/itemCommentContentView" | ||||
|         android:layout_alignBottom="@+id/replies_button" | ||||
|         android:layout_toEndOf="@+id/itemThumbnailView" | ||||
|         android:contentDescription="@string/detail_likes_img_view_description" | ||||
|         android:src="@drawable/ic_thumb_up" /> | ||||
|  | ||||
|     <org.schabi.newpipe.views.NewPipeTextView | ||||
|         android:id="@+id/detail_thumbs_up_count_view" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_alignTop="@id/detail_thumbs_up_img_view" | ||||
|         android:layout_alignBottom="@id/detail_thumbs_up_img_view" | ||||
|         android:layout_marginStart="@dimen/video_item_detail_like_margin" | ||||
|         android:layout_toEndOf="@id/detail_thumbs_up_img_view" | ||||
|         android:gravity="center" | ||||
|         android:lines="1" | ||||
|         android:textAppearance="?android:attr/textAppearanceMedium" | ||||
|         android:textSize="@dimen/video_item_detail_likes_text_size" | ||||
|         tools:text="12M" /> | ||||
|  | ||||
|     <ImageView | ||||
|         android:id="@+id/detail_heart_image_view" | ||||
|         android:layout_width="@dimen/video_item_detail_heart_image_size" | ||||
|         android:layout_height="@dimen/video_item_detail_heart_image_size" | ||||
|         android:layout_alignTop="@id/detail_thumbs_up_img_view" | ||||
|         android:layout_alignBottom="@id/detail_thumbs_up_img_view" | ||||
|         android:layout_marginStart="@dimen/video_item_detail_heart_margin" | ||||
|         android:layout_toEndOf="@+id/detail_thumbs_up_count_view" | ||||
|         android:contentDescription="@string/detail_heart_img_view_description" | ||||
|         android:src="@drawable/ic_heart" /> | ||||
|  | ||||
|     <Button | ||||
|         android:id="@+id/replies_button" | ||||
|         style="?android:attr/borderlessButtonStyle" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_below="@id/itemCommentContentView" | ||||
|         android:layout_alignParentEnd="true" | ||||
|         android:layout_marginStart="@dimen/video_item_detail_heart_margin" | ||||
|         android:minHeight="0dp" | ||||
|         tools:text="543 replies" /> | ||||
|  | ||||
| </RelativeLayout> | ||||
		Reference in New Issue
	
	Block a user
	 Isira Seneviratne
					Isira Seneviratne