mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-30 23:03:00 +00:00 
			
		
		
		
	Merge pull request #9631 from TeamNewPipe/update-npe
Update NewPipeExtractor and properly linkify comments
This commit is contained in:
		| @@ -187,7 +187,7 @@ dependencies { | |||||||
|     // name and the commit hash with the commit hash of the (pushed) commit you want to test |     // name and the commit hash with the commit hash of the (pushed) commit you want to test | ||||||
|     // This works thanks to JitPack: https://jitpack.io/ |     // This works thanks to JitPack: https://jitpack.io/ | ||||||
|     implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751' |     implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751' | ||||||
|     implementation 'com.github.TeamNewPipe:NewPipeExtractor:2211a24b6934a8a8cdf5547ea1b52daa4cb5de6c' |     implementation 'com.github.TeamNewPipe:NewPipeExtractor:ff94e9f30bc5d7831734cc85ecebe7d30ac9c040' | ||||||
|     implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0' |     implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0' | ||||||
|  |  | ||||||
| /** Checkstyle **/ | /** Checkstyle **/ | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import static android.text.TextUtils.isEmpty; | |||||||
| import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT; | import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT; | ||||||
| import static org.schabi.newpipe.extractor.utils.Utils.isBlank; | import static org.schabi.newpipe.extractor.utils.Utils.isBlank; | ||||||
| import static org.schabi.newpipe.util.Localization.getAppLocale; | import static org.schabi.newpipe.util.Localization.getAppLocale; | ||||||
|  | import static org.schabi.newpipe.util.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD; | ||||||
|  |  | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
| import android.view.LayoutInflater; | import android.view.LayoutInflater; | ||||||
| @@ -112,7 +113,10 @@ public class DescriptionFragment extends BaseFragment { | |||||||
|  |  | ||||||
|     private void disableDescriptionSelection() { |     private void disableDescriptionSelection() { | ||||||
|         // show description content again, otherwise some links are not clickable |         // show description content again, otherwise some links are not clickable | ||||||
|         loadDescriptionContent(); |         TextLinkifier.fromDescription(binding.detailDescriptionView, | ||||||
|  |                 streamInfo.getDescription(), HtmlCompat.FROM_HTML_MODE_LEGACY, | ||||||
|  |                 streamInfo.getService(), streamInfo.getUrl(), | ||||||
|  |                 descriptionDisposables, SET_LINK_MOVEMENT_METHOD); | ||||||
|  |  | ||||||
|         binding.detailDescriptionNoteView.setVisibility(View.GONE); |         binding.detailDescriptionNoteView.setVisibility(View.GONE); | ||||||
|         binding.detailDescriptionView.setTextIsSelectable(false); |         binding.detailDescriptionView.setTextIsSelectable(false); | ||||||
| @@ -123,27 +127,6 @@ public class DescriptionFragment extends BaseFragment { | |||||||
|         binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_select_all); |         binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_select_all); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void loadDescriptionContent() { |  | ||||||
|         final Description description = streamInfo.getDescription(); |  | ||||||
|         switch (description.getType()) { |  | ||||||
|             case Description.HTML: |  | ||||||
|                 TextLinkifier.createLinksFromHtmlBlock(binding.detailDescriptionView, |  | ||||||
|                         description.getContent(), HtmlCompat.FROM_HTML_MODE_LEGACY, streamInfo, |  | ||||||
|                         descriptionDisposables); |  | ||||||
|                 break; |  | ||||||
|             case Description.MARKDOWN: |  | ||||||
|                 TextLinkifier.createLinksFromMarkdownText(binding.detailDescriptionView, |  | ||||||
|                         description.getContent(), streamInfo, descriptionDisposables); |  | ||||||
|                 break; |  | ||||||
|             case Description.PLAIN_TEXT: |  | ||||||
|             default: |  | ||||||
|                 TextLinkifier.createLinksFromPlainText(binding.detailDescriptionView, |  | ||||||
|                         description.getContent(), streamInfo, descriptionDisposables); |  | ||||||
|                 break; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     private void setupMetadata(final LayoutInflater inflater, |     private void setupMetadata(final LayoutInflater inflater, | ||||||
|                                final LinearLayout layout) { |                                final LinearLayout layout) { | ||||||
|         addMetadataItem(inflater, layout, false, R.string.metadata_category, |         addMetadataItem(inflater, layout, false, R.string.metadata_category, | ||||||
| @@ -193,8 +176,8 @@ public class DescriptionFragment extends BaseFragment { | |||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         if (linkifyContent) { |         if (linkifyContent) { | ||||||
|             TextLinkifier.createLinksFromPlainText(itemBinding.metadataContentView, content, |             TextLinkifier.fromPlainText(itemBinding.metadataContentView, content, null, null, | ||||||
|                     null, descriptionDisposables); |                     descriptionDisposables, SET_LINK_MOVEMENT_METHOD); | ||||||
|         } else { |         } else { | ||||||
|             itemBinding.metadataContentView.setText(content); |             itemBinding.metadataContentView.setText(content); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -1,9 +1,10 @@ | |||||||
| package org.schabi.newpipe.info_list.holder; | package org.schabi.newpipe.info_list.holder; | ||||||
|  |  | ||||||
|  | import android.graphics.Paint; | ||||||
|  | import android.text.Layout; | ||||||
| import android.text.TextUtils; | import android.text.TextUtils; | ||||||
| import android.text.method.LinkMovementMethod; | import android.text.method.LinkMovementMethod; | ||||||
| import android.text.style.URLSpan; | import android.text.style.URLSpan; | ||||||
| import android.text.util.Linkify; |  | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.view.ViewGroup; | import android.view.ViewGroup; | ||||||
| @@ -11,27 +12,36 @@ import android.widget.ImageView; | |||||||
| import android.widget.RelativeLayout; | import android.widget.RelativeLayout; | ||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
|  |  | ||||||
|  | import androidx.annotation.Nullable; | ||||||
| import androidx.appcompat.app.AppCompatActivity; | import androidx.appcompat.app.AppCompatActivity; | ||||||
| import androidx.core.text.util.LinkifyCompat; | import androidx.core.text.HtmlCompat; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.error.ErrorUtil; | import org.schabi.newpipe.error.ErrorUtil; | ||||||
| import org.schabi.newpipe.extractor.InfoItem; | import org.schabi.newpipe.extractor.InfoItem; | ||||||
|  | import org.schabi.newpipe.extractor.NewPipe; | ||||||
|  | import org.schabi.newpipe.extractor.ServiceList; | ||||||
|  | import org.schabi.newpipe.extractor.StreamingService; | ||||||
| import org.schabi.newpipe.extractor.comments.CommentsInfoItem; | import org.schabi.newpipe.extractor.comments.CommentsInfoItem; | ||||||
|  | import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||||
|  | import org.schabi.newpipe.extractor.stream.Description; | ||||||
| import org.schabi.newpipe.info_list.InfoItemBuilder; | import org.schabi.newpipe.info_list.InfoItemBuilder; | ||||||
| import org.schabi.newpipe.local.history.HistoryRecordManager; | import org.schabi.newpipe.local.history.HistoryRecordManager; | ||||||
| import org.schabi.newpipe.util.text.CommentTextOnTouchListener; |  | ||||||
| import org.schabi.newpipe.util.DeviceUtils; | import org.schabi.newpipe.util.DeviceUtils; | ||||||
| import org.schabi.newpipe.util.Localization; | import org.schabi.newpipe.util.Localization; | ||||||
| import org.schabi.newpipe.util.NavigationHelper; | import org.schabi.newpipe.util.NavigationHelper; | ||||||
| import org.schabi.newpipe.util.PicassoHelper; | import org.schabi.newpipe.util.PicassoHelper; | ||||||
| import org.schabi.newpipe.util.external_communication.ShareUtils; | import org.schabi.newpipe.util.external_communication.ShareUtils; | ||||||
| import org.schabi.newpipe.util.text.TimestampExtractor; | import org.schabi.newpipe.util.text.CommentTextOnTouchListener; | ||||||
|  | import org.schabi.newpipe.util.text.TextLinkifier; | ||||||
|  |  | ||||||
| import java.util.Objects; | import java.util.function.Consumer; | ||||||
|  |  | ||||||
|  | import io.reactivex.rxjava3.disposables.CompositeDisposable; | ||||||
|  |  | ||||||
| public class CommentsMiniInfoItemHolder extends InfoItemHolder { | public class CommentsMiniInfoItemHolder extends InfoItemHolder { | ||||||
|     private static final String TAG = "CommentsMiniIIHolder"; |     private static final String TAG = "CommentsMiniIIHolder"; | ||||||
|  |     private static final String ELLIPSIS = "…"; | ||||||
|  |  | ||||||
|     private static final int COMMENT_DEFAULT_LINES = 2; |     private static final int COMMENT_DEFAULT_LINES = 2; | ||||||
|     private static final int COMMENT_EXPANDED_LINES = 1000; |     private static final int COMMENT_EXPANDED_LINES = 1000; | ||||||
| @@ -39,13 +49,18 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { | |||||||
|     private final int commentHorizontalPadding; |     private final int commentHorizontalPadding; | ||||||
|     private final int commentVerticalPadding; |     private final int commentVerticalPadding; | ||||||
|  |  | ||||||
|  |     private final Paint paintAtContentSize; | ||||||
|  |     private final float ellipsisWidthPx; | ||||||
|  |  | ||||||
|     private final RelativeLayout itemRoot; |     private final RelativeLayout itemRoot; | ||||||
|     private final ImageView itemThumbnailView; |     private final ImageView itemThumbnailView; | ||||||
|     private final TextView itemContentView; |     private final TextView itemContentView; | ||||||
|     private final TextView itemLikesCountView; |     private final TextView itemLikesCountView; | ||||||
|     private final TextView itemPublishedTime; |     private final TextView itemPublishedTime; | ||||||
|  |  | ||||||
|     private String commentText; |     private final CompositeDisposable disposables = new CompositeDisposable(); | ||||||
|  |     private Description commentText; | ||||||
|  |     private StreamingService streamService; | ||||||
|     private String streamUrl; |     private String streamUrl; | ||||||
|  |  | ||||||
|     CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId, |     CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId, | ||||||
| @@ -62,6 +77,10 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { | |||||||
|                 .getResources().getDimension(R.dimen.comments_horizontal_padding); |                 .getResources().getDimension(R.dimen.comments_horizontal_padding); | ||||||
|         commentVerticalPadding = (int) infoItemBuilder.getContext() |         commentVerticalPadding = (int) infoItemBuilder.getContext() | ||||||
|                 .getResources().getDimension(R.dimen.comments_vertical_padding); |                 .getResources().getDimension(R.dimen.comments_vertical_padding); | ||||||
|  |  | ||||||
|  |         paintAtContentSize = new Paint(); | ||||||
|  |         paintAtContentSize.setTextSize(itemContentView.getTextSize()); | ||||||
|  |         ellipsisWidthPx = paintAtContentSize.measureText(ELLIPSIS); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, |     public CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, | ||||||
| @@ -91,18 +110,20 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { | |||||||
|  |  | ||||||
|         itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item)); |         itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item)); | ||||||
|  |  | ||||||
|         streamUrl = item.getUrl(); |         try { | ||||||
|  |             streamService = NewPipe.getService(item.getServiceId()); | ||||||
|         itemContentView.setLines(COMMENT_DEFAULT_LINES); |         } catch (final ExtractionException e) { | ||||||
|         commentText = item.getCommentText(); |             // should never happen | ||||||
|         itemContentView.setText(commentText, TextView.BufferType.SPANNABLE); |             ErrorUtil.showUiErrorSnackbar(itemBuilder.getContext(), "Getting StreamingService", e); | ||||||
|         itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE); |             Log.w(TAG, "Cannot obtain service from comment service id, defaulting to YouTube", e); | ||||||
|  |             streamService = ServiceList.YouTube; | ||||||
|         if (itemContentView.getLineCount() == 0) { |  | ||||||
|             itemContentView.post(this::ellipsize); |  | ||||||
|         } else { |  | ||||||
|             ellipsize(); |  | ||||||
|         } |         } | ||||||
|  |         streamUrl = item.getUrl(); | ||||||
|  |         commentText = item.getCommentText(); | ||||||
|  |         ellipsize(); | ||||||
|  |  | ||||||
|  |         //noinspection ClickableViewAccessibility | ||||||
|  |         itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE); | ||||||
|  |  | ||||||
|         if (item.getLikeCount() >= 0) { |         if (item.getLikeCount() >= 0) { | ||||||
|             itemLikesCountView.setText( |             itemLikesCountView.setText( | ||||||
| @@ -132,7 +153,8 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { | |||||||
|             if (DeviceUtils.isTv(itemBuilder.getContext())) { |             if (DeviceUtils.isTv(itemBuilder.getContext())) { | ||||||
|                 openCommentAuthor(item); |                 openCommentAuthor(item); | ||||||
|             } else { |             } else { | ||||||
|                 ShareUtils.copyToClipboard(itemBuilder.getContext(), commentText); |                 ShareUtils.copyToClipboard(itemBuilder.getContext(), | ||||||
|  |                         itemContentView.getText().toString()); | ||||||
|             } |             } | ||||||
|             return true; |             return true; | ||||||
|         }); |         }); | ||||||
| @@ -172,7 +194,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { | |||||||
|         return urls != null && urls.length != 0; |         return urls != null && urls.length != 0; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void determineLinkFocus() { |     private void determineMovementMethod() { | ||||||
|         if (shouldFocusLinks()) { |         if (shouldFocusLinks()) { | ||||||
|             allowLinkFocus(); |             allowLinkFocus(); | ||||||
|         } else { |         } else { | ||||||
| @@ -181,63 +203,73 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void ellipsize() { |     private void ellipsize() { | ||||||
|         boolean hasEllipsis = false; |         itemContentView.setMaxLines(COMMENT_EXPANDED_LINES); | ||||||
|  |         linkifyCommentContentView(v -> { | ||||||
|  |             boolean hasEllipsis = false; | ||||||
|  |  | ||||||
|         if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) { |             if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) { | ||||||
|             final int endOfLastLine = itemContentView |                 // Note that converting to String removes spans (i.e. links), but that's something | ||||||
|                     .getLayout() |                 // we actually want since when the text is ellipsized we want all clicks on the | ||||||
|                     .getLineEnd(COMMENT_DEFAULT_LINES - 1); |                 // comment to expand the comment, not to open links. | ||||||
|             int end = itemContentView.getText().toString().lastIndexOf(' ', endOfLastLine - 2); |                 final String text = itemContentView.getText().toString(); | ||||||
|             if (end == -1) { |  | ||||||
|                 end = Math.max(endOfLastLine - 2, 0); |                 final Layout layout = itemContentView.getLayout(); | ||||||
|  |                 final float lineWidth = layout.getLineWidth(COMMENT_DEFAULT_LINES - 1); | ||||||
|  |                 final float layoutWidth = layout.getWidth(); | ||||||
|  |                 final int lineStart = layout.getLineStart(COMMENT_DEFAULT_LINES - 1); | ||||||
|  |                 final int lineEnd = layout.getLineEnd(COMMENT_DEFAULT_LINES - 1); | ||||||
|  |  | ||||||
|  |                 // remove characters up until there is enough space for the ellipsis | ||||||
|  |                 // (also summing 2 more pixels, just to be sure to avoid float rounding errors) | ||||||
|  |                 int end = lineEnd; | ||||||
|  |                 float removedCharactersWidth = 0.0f; | ||||||
|  |                 while (lineWidth - removedCharactersWidth + ellipsisWidthPx + 2.0f > layoutWidth | ||||||
|  |                         && end >= lineStart) { | ||||||
|  |                     end -= 1; | ||||||
|  |                     // recalculate each time to account for ligatures or other similar things | ||||||
|  |                     removedCharactersWidth = paintAtContentSize.measureText( | ||||||
|  |                             text.substring(end, lineEnd)); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 // remove trailing spaces and newlines | ||||||
|  |                 while (end > 0 && Character.isWhitespace(text.charAt(end - 1))) { | ||||||
|  |                     end -= 1; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 final String newVal = text.substring(0, end) + ELLIPSIS; | ||||||
|  |                 itemContentView.setText(newVal); | ||||||
|  |                 hasEllipsis = true; | ||||||
|             } |             } | ||||||
|             final String newVal = itemContentView.getText().subSequence(0, end) + " …"; |  | ||||||
|             itemContentView.setText(newVal); |  | ||||||
|             hasEllipsis = true; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         linkify(); |             itemContentView.setMaxLines(COMMENT_DEFAULT_LINES); | ||||||
|  |             if (hasEllipsis) { | ||||||
|         if (hasEllipsis) { |                 denyLinkFocus(); | ||||||
|             denyLinkFocus(); |             } else { | ||||||
|         } else { |                 determineMovementMethod(); | ||||||
|             determineLinkFocus(); |             } | ||||||
|         } |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void toggleEllipsize() { |     private void toggleEllipsize() { | ||||||
|         if (itemContentView.getText().toString().equals(commentText)) { |         final CharSequence text = itemContentView.getText(); | ||||||
|             if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) { |         if (text.charAt(text.length() - 1) == ELLIPSIS.charAt(0)) { | ||||||
|                 ellipsize(); |  | ||||||
|             } |  | ||||||
|         } else { |  | ||||||
|             expand(); |             expand(); | ||||||
|  |         } else if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) { | ||||||
|  |             ellipsize(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void expand() { |     private void expand() { | ||||||
|         itemContentView.setMaxLines(COMMENT_EXPANDED_LINES); |         itemContentView.setMaxLines(COMMENT_EXPANDED_LINES); | ||||||
|         itemContentView.setText(commentText); |         linkifyCommentContentView(v -> determineMovementMethod()); | ||||||
|         linkify(); |  | ||||||
|         determineLinkFocus(); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void linkify() { |     private void linkifyCommentContentView(@Nullable final Consumer<TextView> onCompletion) { | ||||||
|         LinkifyCompat.addLinks(itemContentView, Linkify.WEB_URLS); |         disposables.clear(); | ||||||
|         LinkifyCompat.addLinks(itemContentView, TimestampExtractor.TIMESTAMPS_PATTERN, null, null, |         if (commentText != null) { | ||||||
|                 (match, url) -> { |             TextLinkifier.fromDescription(itemContentView, commentText, | ||||||
|                     try { |                     HtmlCompat.FROM_HTML_MODE_LEGACY, streamService, streamUrl, disposables, | ||||||
|                         final var timestampMatch = TimestampExtractor |                     onCompletion); | ||||||
|                                 .getTimestampFromMatcher(match, commentText); |         } | ||||||
|                         if (timestampMatch == null) { |  | ||||||
|                             return url; |  | ||||||
|                         } |  | ||||||
|                         return streamUrl + url.replace(Objects.requireNonNull(match.group(0)), |  | ||||||
|                                 "#timestamp=" + timestampMatch.seconds()); |  | ||||||
|                     } catch (final Exception ex) { |  | ||||||
|                         Log.e(TAG, "Unable to process url='" + url + "' as timestampLink", ex); |  | ||||||
|                         return url; |  | ||||||
|                     } |  | ||||||
|                 }); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ | |||||||
| package org.schabi.newpipe.util; | package org.schabi.newpipe.util; | ||||||
|  |  | ||||||
| import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; | import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; | ||||||
|  | import static org.schabi.newpipe.util.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD; | ||||||
|  |  | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
| @@ -319,8 +320,9 @@ public final class ExtractorHelper { | |||||||
|             } |             } | ||||||
|  |  | ||||||
|             metaInfoSeparator.setVisibility(View.VISIBLE); |             metaInfoSeparator.setVisibility(View.VISIBLE); | ||||||
|             TextLinkifier.createLinksFromHtmlBlock(metaInfoTextView, stringBuilder.toString(), |             TextLinkifier.fromHtml(metaInfoTextView, stringBuilder.toString(), | ||||||
|                     HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING, null, disposables); |                     HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING, null, null, disposables, | ||||||
|  |                     SET_LINK_MOVEMENT_METHOD); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,51 +2,37 @@ package org.schabi.newpipe.util.text; | |||||||
|  |  | ||||||
| import static org.schabi.newpipe.util.text.TouchUtils.getOffsetForHorizontalLine; | import static org.schabi.newpipe.util.text.TouchUtils.getOffsetForHorizontalLine; | ||||||
|  |  | ||||||
| import android.text.Selection; | import android.annotation.SuppressLint; | ||||||
| import android.text.Spannable; |  | ||||||
| import android.text.Spanned; | import android.text.Spanned; | ||||||
| import android.text.style.ClickableSpan; | import android.text.style.ClickableSpan; | ||||||
| import android.text.style.URLSpan; |  | ||||||
| import android.view.MotionEvent; | import android.view.MotionEvent; | ||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.util.external_communication.ShareUtils; |  | ||||||
|  |  | ||||||
| import io.reactivex.rxjava3.disposables.CompositeDisposable; |  | ||||||
|  |  | ||||||
| public class CommentTextOnTouchListener implements View.OnTouchListener { | public class CommentTextOnTouchListener implements View.OnTouchListener { | ||||||
|     public static final CommentTextOnTouchListener INSTANCE = new CommentTextOnTouchListener(); |     public static final CommentTextOnTouchListener INSTANCE = new CommentTextOnTouchListener(); | ||||||
|  |  | ||||||
|  |     @SuppressLint("ClickableViewAccessibility") | ||||||
|     @Override |     @Override | ||||||
|     public boolean onTouch(final View v, final MotionEvent event) { |     public boolean onTouch(final View v, final MotionEvent event) { | ||||||
|         if (!(v instanceof TextView)) { |         if (!(v instanceof TextView)) { | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|         final TextView widget = (TextView) v; |         final TextView widget = (TextView) v; | ||||||
|         final Object text = widget.getText(); |         final CharSequence text = widget.getText(); | ||||||
|         if (text instanceof Spanned) { |         if (text instanceof Spanned) { | ||||||
|             final Spannable buffer = (Spannable) text; |             final Spanned buffer = (Spanned) text; | ||||||
|  |  | ||||||
|             final int action = event.getAction(); |             final int action = event.getAction(); | ||||||
|  |  | ||||||
|             if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) { |             if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) { | ||||||
|                 final int offset = getOffsetForHorizontalLine(widget, event); |                 final int offset = getOffsetForHorizontalLine(widget, event); | ||||||
|                 final ClickableSpan[] link = buffer.getSpans(offset, offset, ClickableSpan.class); |                 final ClickableSpan[] links = buffer.getSpans(offset, offset, ClickableSpan.class); | ||||||
|  |  | ||||||
|                 if (link.length != 0) { |                 if (links.length != 0) { | ||||||
|                     if (action == MotionEvent.ACTION_UP) { |                     if (action == MotionEvent.ACTION_UP) { | ||||||
|                         if (link[0] instanceof URLSpan) { |                         links[0].onClick(widget); | ||||||
|                             final String url = ((URLSpan) link[0]).getURL(); |  | ||||||
|                             if (!InternalUrlsHandler.handleUrlCommentsTimestamp( |  | ||||||
|                                     new CompositeDisposable(), v.getContext(), url)) { |  | ||||||
|                                 ShareUtils.openUrlInBrowser(v.getContext(), url, false); |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                     } else if (action == MotionEvent.ACTION_DOWN) { |  | ||||||
|                         Selection.setSelection(buffer, buffer.getSpanStart(link[0]), |  | ||||||
|                                 buffer.getSpanEnd(link[0])); |  | ||||||
|                     } |                     } | ||||||
|  |                     // we handle events that intersect links, so return true | ||||||
|                     return true; |                     return true; | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -5,7 +5,6 @@ import android.view.View; | |||||||
|  |  | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.extractor.Info; |  | ||||||
| import org.schabi.newpipe.util.NavigationHelper; | import org.schabi.newpipe.util.NavigationHelper; | ||||||
| import org.schabi.newpipe.util.external_communication.ShareUtils; | import org.schabi.newpipe.util.external_communication.ShareUtils; | ||||||
|  |  | ||||||
| @@ -15,20 +14,19 @@ final class HashtagLongPressClickableSpan extends LongPressClickableSpan { | |||||||
|     private final Context context; |     private final Context context; | ||||||
|     @NonNull |     @NonNull | ||||||
|     private final String parsedHashtag; |     private final String parsedHashtag; | ||||||
|     @NonNull |     private final int relatedInfoServiceId; | ||||||
|     private final Info relatedInfo; |  | ||||||
|  |  | ||||||
|     HashtagLongPressClickableSpan(@NonNull final Context context, |     HashtagLongPressClickableSpan(@NonNull final Context context, | ||||||
|                                   @NonNull final String parsedHashtag, |                                   @NonNull final String parsedHashtag, | ||||||
|                                   @NonNull final Info relatedInfo) { |                                   final int relatedInfoServiceId) { | ||||||
|         this.context = context; |         this.context = context; | ||||||
|         this.parsedHashtag = parsedHashtag; |         this.parsedHashtag = parsedHashtag; | ||||||
|         this.relatedInfo = relatedInfo; |         this.relatedInfoServiceId = relatedInfoServiceId; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onClick(@NonNull final View view) { |     public void onClick(@NonNull final View view) { | ||||||
|         NavigationHelper.openSearch(context, relatedInfo.getServiceId(), parsedHashtag); |         NavigationHelper.openSearch(context, relatedInfoServiceId, parsedHashtag); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|   | |||||||
| @@ -12,11 +12,12 @@ import androidx.annotation.NonNull; | |||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.core.text.HtmlCompat; | import androidx.core.text.HtmlCompat; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.extractor.Info; | import org.schabi.newpipe.extractor.StreamingService; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | import org.schabi.newpipe.extractor.stream.Description; | ||||||
| import org.schabi.newpipe.util.NavigationHelper; | import org.schabi.newpipe.util.NavigationHelper; | ||||||
| import org.schabi.newpipe.util.external_communication.ShareUtils; | import org.schabi.newpipe.util.external_communication.ShareUtils; | ||||||
|  |  | ||||||
|  | import java.util.function.Consumer; | ||||||
| import java.util.regex.Matcher; | import java.util.regex.Matcher; | ||||||
| import java.util.regex.Pattern; | import java.util.regex.Pattern; | ||||||
|  |  | ||||||
| @@ -33,88 +34,155 @@ public final class TextLinkifier { | |||||||
|     // Looks for hashtags with characters from any language (\p{L}), numbers, or underscores |     // Looks for hashtags with characters from any language (\p{L}), numbers, or underscores | ||||||
|     private static final Pattern HASHTAGS_PATTERN = Pattern.compile("(#[\\p{L}0-9_]+)"); |     private static final Pattern HASHTAGS_PATTERN = Pattern.compile("(#[\\p{L}0-9_]+)"); | ||||||
|  |  | ||||||
|  |     public static final Consumer<TextView> SET_LINK_MOVEMENT_METHOD = | ||||||
|  |             v -> v.setMovementMethod(LongPressLinkMovementMethod.getInstance()); | ||||||
|  |  | ||||||
|     private TextLinkifier() { |     private TextLinkifier() { | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Create links for contents with an {@link Description} in the various possible formats. | ||||||
|  |      * <p> | ||||||
|  |      * This will call one of these three functions based on the format: {@link #fromHtml}, | ||||||
|  |      * {@link #fromMarkdown} or {@link #fromPlainText}. | ||||||
|  |      * | ||||||
|  |      * @param textView           the TextView to set the htmlBlock linked | ||||||
|  |      * @param description        the htmlBlock to be linked | ||||||
|  |      * @param htmlCompatFlag     the int flag to be set if {@link HtmlCompat#fromHtml(String, int)} | ||||||
|  |      *                           will be called (not used for formats different than HTML) | ||||||
|  |      * @param relatedInfoService if given, handle hashtags to search for the term in the correct | ||||||
|  |      *                           service | ||||||
|  |      * @param relatedStreamUrl   if given, used alongside {@code relatedInfoService} to handle | ||||||
|  |      *                           timestamps to open the stream in the popup player at the specific | ||||||
|  |      *                           time | ||||||
|  |      * @param disposables        disposables created by the method are added here and their | ||||||
|  |      *                           lifecycle should be handled by the calling class | ||||||
|  |      * @param onCompletion       will be run when setting text to the textView completes; use {@link | ||||||
|  |      *                           #SET_LINK_MOVEMENT_METHOD} to make links clickable and focusable | ||||||
|  |      */ | ||||||
|  |     public static void fromDescription(@NonNull final TextView textView, | ||||||
|  |                                        @NonNull final Description description, | ||||||
|  |                                        final int htmlCompatFlag, | ||||||
|  |                                        @Nullable final StreamingService relatedInfoService, | ||||||
|  |                                        @Nullable final String relatedStreamUrl, | ||||||
|  |                                        @NonNull final CompositeDisposable disposables, | ||||||
|  |                                        @Nullable final Consumer<TextView> onCompletion) { | ||||||
|  |         switch (description.getType()) { | ||||||
|  |             case Description.HTML: | ||||||
|  |                 TextLinkifier.fromHtml(textView, description.getContent(), htmlCompatFlag, | ||||||
|  |                         relatedInfoService, relatedStreamUrl, disposables, onCompletion); | ||||||
|  |                 break; | ||||||
|  |             case Description.MARKDOWN: | ||||||
|  |                 TextLinkifier.fromMarkdown(textView, description.getContent(), | ||||||
|  |                         relatedInfoService, relatedStreamUrl, disposables, onCompletion); | ||||||
|  |                 break; | ||||||
|  |             case Description.PLAIN_TEXT: default: | ||||||
|  |                 TextLinkifier.fromPlainText(textView, description.getContent(), | ||||||
|  |                         relatedInfoService, relatedStreamUrl, disposables, onCompletion); | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Create links for contents with an HTML description. |      * Create links for contents with an HTML description. | ||||||
|      * |      * | ||||||
|      * <p> |      * <p> | ||||||
|      * This method will call {@link #changeIntentsOfDescriptionLinks(TextView, CharSequence, Info, |      * This method will call {@link #changeLinkIntents(TextView, CharSequence, StreamingService, | ||||||
|      * CompositeDisposable)} after having linked the URLs with |      * String, CompositeDisposable, Consumer)} after having linked the URLs with | ||||||
|      * {@link HtmlCompat#fromHtml(String, int)}. |      * {@link HtmlCompat#fromHtml(String, int)}. | ||||||
|      * </p> |      * </p> | ||||||
|      * |      * | ||||||
|      * @param textView       the {@link TextView} to set the the HTML string block linked |      * @param textView           the {@link TextView} to set the the HTML string block linked | ||||||
|      * @param htmlBlock      the HTML string block to be linked |      * @param htmlBlock          the HTML string block to be linked | ||||||
|      * @param htmlCompatFlag the int flag to be set when {@link HtmlCompat#fromHtml(String, int)} |      * @param htmlCompatFlag     the int flag to be set when {@link HtmlCompat#fromHtml(String, | ||||||
|      *                       will be called |      *                           int)} will be called | ||||||
|      * @param relatedInfo    if given, handle timestamps to open the stream in the popup player at |      * @param relatedInfoService if given, handle hashtags to search for the term in the correct | ||||||
|      *                       the specific time, and hashtags to search for the term in the correct |      *                           service | ||||||
|      *                       service |      * @param relatedStreamUrl   if given, used alongside {@code relatedInfoService} to handle | ||||||
|      * @param disposables    disposables created by the method are added here and their lifecycle |      *                           timestamps to open the stream in the popup player at the specific | ||||||
|      *                       should be handled by the calling class |      *                           time | ||||||
|  |      * @param disposables        disposables created by the method are added here and their | ||||||
|  |      *                           lifecycle should be handled by the calling class | ||||||
|  |      * @param onCompletion       will be run when setting text to the textView completes; use {@link | ||||||
|  |      *                           #SET_LINK_MOVEMENT_METHOD} to make links clickable and focusable | ||||||
|      */ |      */ | ||||||
|     public static void createLinksFromHtmlBlock(@NonNull final TextView textView, |     public static void fromHtml(@NonNull final TextView textView, | ||||||
|                                                 @NonNull final String htmlBlock, |                                 @NonNull final String htmlBlock, | ||||||
|                                                 final int htmlCompatFlag, |                                 final int htmlCompatFlag, | ||||||
|                                                 @Nullable final Info relatedInfo, |                                 @Nullable final StreamingService relatedInfoService, | ||||||
|                                                 @NonNull final CompositeDisposable disposables) { |                                 @Nullable final String relatedStreamUrl, | ||||||
|         changeIntentsOfDescriptionLinks(textView, HtmlCompat.fromHtml(htmlBlock, htmlCompatFlag), |                                 @NonNull final CompositeDisposable disposables, | ||||||
|                 relatedInfo, disposables); |                                 @Nullable final Consumer<TextView> onCompletion) { | ||||||
|  |         changeLinkIntents( | ||||||
|  |                 textView, HtmlCompat.fromHtml(htmlBlock, htmlCompatFlag), relatedInfoService, | ||||||
|  |                 relatedStreamUrl, disposables, onCompletion); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Create links for contents with a plain text description. |      * Create links for contents with a plain text description. | ||||||
|      * |      * | ||||||
|      * <p> |      * <p> | ||||||
|      * This method will call {@link #changeIntentsOfDescriptionLinks(TextView, CharSequence, Info, |      * This method will call {@link #changeLinkIntents(TextView, CharSequence, StreamingService, | ||||||
|      * CompositeDisposable)} after having linked the URLs with {@link TextView#setAutoLinkMask(int)} |      * String, CompositeDisposable, Consumer)} after having linked the URLs with | ||||||
|      * and {@link TextView#setText(CharSequence, TextView.BufferType)}. |      * {@link TextView#setAutoLinkMask(int)} and | ||||||
|  |      * {@link TextView#setText(CharSequence, TextView.BufferType)}. | ||||||
|      * </p> |      * </p> | ||||||
|      * |      * | ||||||
|      * @param textView       the {@link TextView} to set the plain text block linked |      * @param textView           the {@link TextView} to set the plain text block linked | ||||||
|      * @param plainTextBlock the block of plain text to be linked |      * @param plainTextBlock     the block of plain text to be linked | ||||||
|      * @param relatedInfo    if given, handle timestamps to open the stream in the popup player, at |      * @param relatedInfoService if given, handle hashtags to search for the term in the correct | ||||||
|      *                       the specified time, and hashtags to search for the term in the correct |      *                           service | ||||||
|      *                       service |      * @param relatedStreamUrl   if given, used alongside {@code relatedInfoService} to handle | ||||||
|      * @param disposables    disposables created by the method are added here and their lifecycle |      *                           timestamps to open the stream in the popup player at the specific | ||||||
|      *                       should be handled by the calling class |      *                           time | ||||||
|  |      * @param disposables        disposables created by the method are added here and their | ||||||
|  |      *                           lifecycle should be handled by the calling class | ||||||
|  |      * @param onCompletion       will be run when setting text to the textView completes; use {@link | ||||||
|  |      *                           #SET_LINK_MOVEMENT_METHOD} to make links clickable and focusable | ||||||
|      */ |      */ | ||||||
|     public static void createLinksFromPlainText(@NonNull final TextView textView, |     public static void fromPlainText(@NonNull final TextView textView, | ||||||
|                                                 @NonNull final String plainTextBlock, |                                      @NonNull final String plainTextBlock, | ||||||
|                                                 @Nullable final Info relatedInfo, |                                      @Nullable final StreamingService relatedInfoService, | ||||||
|                                                 @NonNull final CompositeDisposable disposables) { |                                      @Nullable final String relatedStreamUrl, | ||||||
|  |                                      @NonNull final CompositeDisposable disposables, | ||||||
|  |                                      @Nullable final Consumer<TextView> onCompletion) { | ||||||
|         textView.setAutoLinkMask(Linkify.WEB_URLS); |         textView.setAutoLinkMask(Linkify.WEB_URLS); | ||||||
|         textView.setText(plainTextBlock, TextView.BufferType.SPANNABLE); |         textView.setText(plainTextBlock, TextView.BufferType.SPANNABLE); | ||||||
|         changeIntentsOfDescriptionLinks(textView, textView.getText(), relatedInfo, disposables); |         changeLinkIntents(textView, textView.getText(), relatedInfoService, | ||||||
|  |                 relatedStreamUrl, disposables, onCompletion); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Create links for contents with a markdown description. |      * Create links for contents with a markdown description. | ||||||
|      * |      * | ||||||
|      * <p> |      * <p> | ||||||
|      * This method will call {@link #changeIntentsOfDescriptionLinks(TextView, CharSequence, Info, |      * This method will call {@link #changeLinkIntents(TextView, CharSequence, StreamingService, | ||||||
|      * CompositeDisposable)} after creating a {@link Markwon} object and using |      * String, CompositeDisposable, Consumer)} after creating a {@link Markwon} object and using | ||||||
|      * {@link Markwon#setMarkdown(TextView, String)}. |      * {@link Markwon#setMarkdown(TextView, String)}. | ||||||
|      * </p> |      * </p> | ||||||
|      * |      * | ||||||
|      * @param textView      the {@link TextView} to set the plain text block linked |      * @param textView           the {@link TextView} to set the plain text block linked | ||||||
|      * @param markdownBlock the block of markdown text to be linked |      * @param markdownBlock      the block of markdown text to be linked | ||||||
|      * @param relatedInfo   if given, handle timestamps to open the stream in the popup player at |      * @param relatedInfoService if given, handle hashtags to search for the term in the correct | ||||||
|      *                      the specific time, and hashtags to search for the term in the correct |      *                           service | ||||||
|      *                      service |      * @param relatedStreamUrl   if given, used alongside {@code relatedInfoService} to handle | ||||||
|      * @param disposables   disposables created by the method are added here and their lifecycle |      *                           timestamps to open the stream in the popup player at the specific | ||||||
|      *                      should be handled by the calling class |      *                           time | ||||||
|  |      * @param disposables        disposables created by the method are added here and their | ||||||
|  |      *                           lifecycle should be handled by the calling class | ||||||
|  |      * @param onCompletion       will be run when setting text to the textView completes; use {@link | ||||||
|  |      *                           #SET_LINK_MOVEMENT_METHOD} to make links clickable and focusable | ||||||
|      */ |      */ | ||||||
|     public static void createLinksFromMarkdownText(@NonNull final TextView textView, |     public static void fromMarkdown(@NonNull final TextView textView, | ||||||
|                                                    final String markdownBlock, |                                     @NonNull final String markdownBlock, | ||||||
|                                                    @Nullable final Info relatedInfo, |                                     @Nullable final StreamingService relatedInfoService, | ||||||
|                                                    final CompositeDisposable disposables) { |                                     @Nullable final String relatedStreamUrl, | ||||||
|  |                                     @NonNull final CompositeDisposable disposables, | ||||||
|  |                                     @Nullable final Consumer<TextView> onCompletion) { | ||||||
|         final Markwon markwon = Markwon.builder(textView.getContext()) |         final Markwon markwon = Markwon.builder(textView.getContext()) | ||||||
|                 .usePlugin(LinkifyPlugin.create()).build(); |                 .usePlugin(LinkifyPlugin.create()).build(); | ||||||
|         changeIntentsOfDescriptionLinks(textView, markwon.toMarkdown(markdownBlock), relatedInfo, |         changeLinkIntents(textView, markwon.toMarkdown(markdownBlock), | ||||||
|                 disposables); |                 relatedInfoService, relatedStreamUrl, disposables, onCompletion); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -131,9 +199,9 @@ public final class TextLinkifier { | |||||||
|      * This method will also add click listeners on timestamps in this description, which will play |      * This method will also add click listeners on timestamps in this description, which will play | ||||||
|      * the content in the popup player at the time indicated in the timestamp, by using |      * the content in the popup player at the time indicated in the timestamp, by using | ||||||
|      * {@link TextLinkifier#addClickListenersOnTimestamps(Context, SpannableStringBuilder, |      * {@link TextLinkifier#addClickListenersOnTimestamps(Context, SpannableStringBuilder, | ||||||
|      * StreamInfo, CompositeDisposable)} method and click listeners on hashtags, by using |      * StreamingService, String, CompositeDisposable)} method and click listeners on hashtags, by | ||||||
|      * {@link TextLinkifier#addClickListenersOnHashtags(Context, SpannableStringBuilder, Info)})}, |      * using {@link TextLinkifier#addClickListenersOnHashtags(Context, SpannableStringBuilder, | ||||||
|      * which will open a search on the current service with the hashtag. |      * StreamingService)}, which will open a search on the current service with the hashtag. | ||||||
|      * </p> |      * </p> | ||||||
|      * |      * | ||||||
|      * <p> |      * <p> | ||||||
| @@ -141,20 +209,25 @@ public final class TextLinkifier { | |||||||
|      * before opening a web link. |      * before opening a web link. | ||||||
|      * </p> |      * </p> | ||||||
|      * |      * | ||||||
|      * @param textView    the {@link TextView} in which the converted {@link CharSequence} will be |      * @param textView           the {@link TextView} to which the converted {@link CharSequence} | ||||||
|      *                    applied |      *                           will be applied | ||||||
|      * @param chars       the {@link CharSequence} to be parsed |      * @param chars              the {@link CharSequence} to be parsed | ||||||
|      * @param relatedInfo if given, handle timestamps to open the stream in the popup player at the |      * @param relatedInfoService if given, handle hashtags to search for the term in the correct | ||||||
|      *                    specific time, and hashtags to search for the term in the correct service |      *                           service | ||||||
|      * @param disposables disposables created by the method are added here and their lifecycle |      * @param relatedStreamUrl   if given, used alongside {@code relatedInfoService} to handle | ||||||
|      *                    should be handled by the calling class |      *                           timestamps to open the stream in the popup player at the specific | ||||||
|  |      *                           time | ||||||
|  |      * @param disposables        disposables created by the method are added here and their | ||||||
|  |      *                           lifecycle should be handled by the calling class | ||||||
|  |      * @param onCompletion       will be run when setting text to the textView completes; use {@link | ||||||
|  |      *                           #SET_LINK_MOVEMENT_METHOD} to make links clickable and focusable | ||||||
|      */ |      */ | ||||||
|     private static void changeIntentsOfDescriptionLinks( |     private static void changeLinkIntents(@NonNull final TextView textView, | ||||||
|             @NonNull final TextView textView, |                                           @NonNull final CharSequence chars, | ||||||
|             @NonNull final CharSequence chars, |                                           @Nullable final StreamingService relatedInfoService, | ||||||
|             @Nullable final Info relatedInfo, |                                           @Nullable final String relatedStreamUrl, | ||||||
|             @NonNull final CompositeDisposable disposables) { |                                           @NonNull final CompositeDisposable disposables, | ||||||
|         textView.setMovementMethod(LongPressLinkMovementMethod.getInstance()); |                                           @Nullable final Consumer<TextView> onCompletion) { | ||||||
|         disposables.add(Single.fromCallable(() -> { |         disposables.add(Single.fromCallable(() -> { | ||||||
|                     final Context context = textView.getContext(); |                     final Context context = textView.getContext(); | ||||||
|  |  | ||||||
| @@ -176,26 +249,26 @@ public final class TextLinkifier { | |||||||
|                         textBlockLinked.removeSpan(span); |                         textBlockLinked.removeSpan(span); | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|                     if (relatedInfo != null) { |                     // add click actions on plain text timestamps only for description of contents, | ||||||
|                         // add click actions on plain text timestamps only for description of |                     // unneeded for meta-info or other TextViews | ||||||
|                         // contents, unneeded for meta-info or other TextViews |                     if (relatedInfoService != null) { | ||||||
|                         if (relatedInfo instanceof StreamInfo) { |                         if (relatedStreamUrl != null) { | ||||||
|                             addClickListenersOnTimestamps(context, textBlockLinked, |                             addClickListenersOnTimestamps(context, textBlockLinked, | ||||||
|                                     (StreamInfo) relatedInfo, disposables); |                                     relatedInfoService, relatedStreamUrl, disposables); | ||||||
|                         } |                         } | ||||||
|  |                         addClickListenersOnHashtags(context, textBlockLinked, relatedInfoService); | ||||||
|                         addClickListenersOnHashtags(context, textBlockLinked, relatedInfo); |  | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|                     return textBlockLinked; |                     return textBlockLinked; | ||||||
|                 }).subscribeOn(Schedulers.computation()) |                 }).subscribeOn(Schedulers.computation()) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .subscribe( |                 .subscribe( | ||||||
|                         textBlockLinked -> setTextViewCharSequence(textView, textBlockLinked), |                         textBlockLinked -> | ||||||
|  |                                 setTextViewCharSequence(textView, textBlockLinked, onCompletion), | ||||||
|                         throwable -> { |                         throwable -> { | ||||||
|                             Log.e(TAG, "Unable to linkify text", throwable); |                             Log.e(TAG, "Unable to linkify text", throwable); | ||||||
|                             // this should never happen, but if it does, just fallback to it |                             // this should never happen, but if it does, just fallback to it | ||||||
|                             setTextViewCharSequence(textView, chars); |                             setTextViewCharSequence(textView, chars, onCompletion); | ||||||
|                         })); |                         })); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -213,12 +286,12 @@ public final class TextLinkifier { | |||||||
|      * @param context              the {@link Context} to use |      * @param context              the {@link Context} to use | ||||||
|      * @param spannableDescription the {@link SpannableStringBuilder} with the text of the |      * @param spannableDescription the {@link SpannableStringBuilder} with the text of the | ||||||
|      *                             content description |      *                             content description | ||||||
|      * @param relatedInfo          used to search for the term in the correct service |      * @param relatedInfoService   used to search for the term in the correct service | ||||||
|      */ |      */ | ||||||
|     private static void addClickListenersOnHashtags( |     private static void addClickListenersOnHashtags( | ||||||
|             @NonNull final Context context, |             @NonNull final Context context, | ||||||
|             @NonNull final SpannableStringBuilder spannableDescription, |             @NonNull final SpannableStringBuilder spannableDescription, | ||||||
|             @NonNull final Info relatedInfo) { |             @NonNull final StreamingService relatedInfoService) { | ||||||
|         final String descriptionText = spannableDescription.toString(); |         final String descriptionText = spannableDescription.toString(); | ||||||
|         final Matcher hashtagsMatches = HASHTAGS_PATTERN.matcher(descriptionText); |         final Matcher hashtagsMatches = HASHTAGS_PATTERN.matcher(descriptionText); | ||||||
|  |  | ||||||
| @@ -231,8 +304,9 @@ public final class TextLinkifier { | |||||||
|             // of an URL, already parsed before |             // of an URL, already parsed before | ||||||
|             if (spannableDescription.getSpans(hashtagStart, hashtagEnd, |             if (spannableDescription.getSpans(hashtagStart, hashtagEnd, | ||||||
|                     LongPressClickableSpan.class).length == 0) { |                     LongPressClickableSpan.class).length == 0) { | ||||||
|  |                 final int serviceId = relatedInfoService.getServiceId(); | ||||||
|                 spannableDescription.setSpan( |                 spannableDescription.setSpan( | ||||||
|                         new HashtagLongPressClickableSpan(context, parsedHashtag, relatedInfo), |                         new HashtagLongPressClickableSpan(context, parsedHashtag, serviceId), | ||||||
|                         hashtagStart, hashtagEnd, 0); |                         hashtagStart, hashtagEnd, 0); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @@ -251,14 +325,16 @@ public final class TextLinkifier { | |||||||
|      * @param context              the {@link Context} to use |      * @param context              the {@link Context} to use | ||||||
|      * @param spannableDescription the {@link SpannableStringBuilder} with the text of the |      * @param spannableDescription the {@link SpannableStringBuilder} with the text of the | ||||||
|      *                             content description |      *                             content description | ||||||
|      * @param streamInfo           what to open in the popup player when timestamps are clicked |      * @param relatedInfoService   the service of the {@code relatedStreamUrl} | ||||||
|  |      * @param relatedStreamUrl     what to open in the popup player when timestamps are clicked | ||||||
|      * @param disposables          disposables created by the method are added here and their |      * @param disposables          disposables created by the method are added here and their | ||||||
|      *                             lifecycle should be handled by the calling class |      *                             lifecycle should be handled by the calling class | ||||||
|      */ |      */ | ||||||
|     private static void addClickListenersOnTimestamps( |     private static void addClickListenersOnTimestamps( | ||||||
|             @NonNull final Context context, |             @NonNull final Context context, | ||||||
|             @NonNull final SpannableStringBuilder spannableDescription, |             @NonNull final SpannableStringBuilder spannableDescription, | ||||||
|             @NonNull final StreamInfo streamInfo, |             @NonNull final StreamingService relatedInfoService, | ||||||
|  |             @NonNull final String relatedStreamUrl, | ||||||
|             @NonNull final CompositeDisposable disposables) { |             @NonNull final CompositeDisposable disposables) { | ||||||
|         final String descriptionText = spannableDescription.toString(); |         final String descriptionText = spannableDescription.toString(); | ||||||
|         final Matcher timestampsMatches = TimestampExtractor.TIMESTAMPS_PATTERN.matcher( |         final Matcher timestampsMatches = TimestampExtractor.TIMESTAMPS_PATTERN.matcher( | ||||||
| @@ -272,8 +348,9 @@ public final class TextLinkifier { | |||||||
|                 continue; |                 continue; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             spannableDescription.setSpan(new TimestampLongPressClickableSpan( |             spannableDescription.setSpan( | ||||||
|                     context, descriptionText, disposables, streamInfo, timestampMatchDTO), |                     new TimestampLongPressClickableSpan(context, descriptionText, disposables, | ||||||
|  |                             relatedInfoService, relatedStreamUrl, timestampMatchDTO), | ||||||
|                     timestampMatchDTO.timestampStart(), |                     timestampMatchDTO.timestampStart(), | ||||||
|                     timestampMatchDTO.timestampEnd(), |                     timestampMatchDTO.timestampEnd(), | ||||||
|                     0); |                     0); | ||||||
| @@ -281,8 +358,12 @@ public final class TextLinkifier { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private static void setTextViewCharSequence(@NonNull final TextView textView, |     private static void setTextViewCharSequence(@NonNull final TextView textView, | ||||||
|                                                 @Nullable final CharSequence charSequence) { |                                                 @Nullable final CharSequence charSequence, | ||||||
|  |                                                 @Nullable final Consumer<TextView> onCompletion) { | ||||||
|         textView.setText(charSequence); |         textView.setText(charSequence); | ||||||
|         textView.setVisibility(View.VISIBLE); |         textView.setVisibility(View.VISIBLE); | ||||||
|  |         if (onCompletion != null) { | ||||||
|  |             onCompletion.accept(textView); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -9,7 +9,6 @@ import androidx.annotation.NonNull; | |||||||
|  |  | ||||||
| import org.schabi.newpipe.extractor.ServiceList; | import org.schabi.newpipe.extractor.ServiceList; | ||||||
| import org.schabi.newpipe.extractor.StreamingService; | import org.schabi.newpipe.extractor.StreamingService; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; |  | ||||||
| import org.schabi.newpipe.util.external_communication.ShareUtils; | import org.schabi.newpipe.util.external_communication.ShareUtils; | ||||||
|  |  | ||||||
| import io.reactivex.rxjava3.disposables.CompositeDisposable; | import io.reactivex.rxjava3.disposables.CompositeDisposable; | ||||||
| @@ -23,7 +22,9 @@ final class TimestampLongPressClickableSpan extends LongPressClickableSpan { | |||||||
|     @NonNull |     @NonNull | ||||||
|     private final CompositeDisposable disposables; |     private final CompositeDisposable disposables; | ||||||
|     @NonNull |     @NonNull | ||||||
|     private final StreamInfo streamInfo; |     private final StreamingService relatedInfoService; | ||||||
|  |     @NonNull | ||||||
|  |     private final String relatedStreamUrl; | ||||||
|     @NonNull |     @NonNull | ||||||
|     private final TimestampExtractor.TimestampMatchDTO timestampMatchDTO; |     private final TimestampExtractor.TimestampMatchDTO timestampMatchDTO; | ||||||
|  |  | ||||||
| @@ -31,41 +32,43 @@ final class TimestampLongPressClickableSpan extends LongPressClickableSpan { | |||||||
|             @NonNull final Context context, |             @NonNull final Context context, | ||||||
|             @NonNull final String descriptionText, |             @NonNull final String descriptionText, | ||||||
|             @NonNull final CompositeDisposable disposables, |             @NonNull final CompositeDisposable disposables, | ||||||
|             @NonNull final StreamInfo streamInfo, |             @NonNull final StreamingService relatedInfoService, | ||||||
|  |             @NonNull final String relatedStreamUrl, | ||||||
|             @NonNull final TimestampExtractor.TimestampMatchDTO timestampMatchDTO) { |             @NonNull final TimestampExtractor.TimestampMatchDTO timestampMatchDTO) { | ||||||
|         this.context = context; |         this.context = context; | ||||||
|         this.descriptionText = descriptionText; |         this.descriptionText = descriptionText; | ||||||
|         this.disposables = disposables; |         this.disposables = disposables; | ||||||
|         this.streamInfo = streamInfo; |         this.relatedInfoService = relatedInfoService; | ||||||
|  |         this.relatedStreamUrl = relatedStreamUrl; | ||||||
|         this.timestampMatchDTO = timestampMatchDTO; |         this.timestampMatchDTO = timestampMatchDTO; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onClick(@NonNull final View view) { |     public void onClick(@NonNull final View view) { | ||||||
|         playOnPopup(context, streamInfo.getUrl(), streamInfo.getService(), |         playOnPopup(context, relatedStreamUrl, relatedInfoService, | ||||||
|                 timestampMatchDTO.seconds(), disposables); |                 timestampMatchDTO.seconds(), disposables); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onLongClick(@NonNull final View view) { |     public void onLongClick(@NonNull final View view) { | ||||||
|         ShareUtils.copyToClipboard(context, |         ShareUtils.copyToClipboard(context, getTimestampTextToCopy( | ||||||
|                 getTimestampTextToCopy(streamInfo, descriptionText, timestampMatchDTO)); |                 relatedInfoService, relatedStreamUrl, descriptionText, timestampMatchDTO)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @NonNull |     @NonNull | ||||||
|     private static String getTimestampTextToCopy( |     private static String getTimestampTextToCopy( | ||||||
|             @NonNull final StreamInfo relatedInfo, |             @NonNull final StreamingService relatedInfoService, | ||||||
|  |             @NonNull final String relatedStreamUrl, | ||||||
|             @NonNull final String descriptionText, |             @NonNull final String descriptionText, | ||||||
|             @NonNull final TimestampExtractor.TimestampMatchDTO timestampMatchDTO) { |             @NonNull final TimestampExtractor.TimestampMatchDTO timestampMatchDTO) { | ||||||
|         // TODO: use extractor methods to get timestamps when this feature will be implemented in it |         // TODO: use extractor methods to get timestamps when this feature will be implemented in it | ||||||
|         final StreamingService streamingService = relatedInfo.getService(); |         if (relatedInfoService == ServiceList.YouTube) { | ||||||
|         if (streamingService == ServiceList.YouTube) { |             return relatedStreamUrl + "&t=" + timestampMatchDTO.seconds(); | ||||||
|             return relatedInfo.getUrl() + "&t=" + timestampMatchDTO.seconds(); |         } else if (relatedInfoService == ServiceList.SoundCloud | ||||||
|         } else if (streamingService == ServiceList.SoundCloud |                 || relatedInfoService == ServiceList.MediaCCC) { | ||||||
|                 || streamingService == ServiceList.MediaCCC) { |             return relatedStreamUrl + "#t=" + timestampMatchDTO.seconds(); | ||||||
|             return relatedInfo.getUrl() + "#t=" + timestampMatchDTO.seconds(); |         } else if (relatedInfoService == ServiceList.PeerTube) { | ||||||
|         } else if (streamingService == ServiceList.PeerTube) { |             return relatedStreamUrl + "?start=" + timestampMatchDTO.seconds(); | ||||||
|             return relatedInfo.getUrl() + "?start=" + timestampMatchDTO.seconds(); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Return timestamp text for other services |         // Return timestamp text for other services | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Stypox
					Stypox