mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-12-23 00:20:32 +00:00
Update NewPipeExtractor and properly linkify comments
This commit is contained in:
parent
2db29187f4
commit
489df0ed7d
@ -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,33 +12,43 @@ 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;
|
||||||
|
|
||||||
private final int commentHorizontalPadding;
|
private final int commentHorizontalPadding;
|
||||||
private final int commentVerticalPadding;
|
private final int commentVerticalPadding;
|
||||||
|
private final float ellipsisWidthPx;
|
||||||
|
|
||||||
private final RelativeLayout itemRoot;
|
private final RelativeLayout itemRoot;
|
||||||
private final ImageView itemThumbnailView;
|
private final ImageView itemThumbnailView;
|
||||||
@ -45,7 +56,9 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
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 +75,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);
|
||||||
|
|
||||||
|
final Paint paint = new Paint();
|
||||||
|
paint.setTextSize(itemContentView.getTextSize());
|
||||||
|
ellipsisWidthPx = paint.measureText(ELLIPSIS);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder,
|
public CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder,
|
||||||
@ -91,18 +108,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 +151,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 +192,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 +201,51 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void ellipsize() {
|
private void ellipsize() {
|
||||||
boolean hasEllipsis = false;
|
linkifyCommentContentView(v -> {
|
||||||
|
boolean hasEllipsis = false;
|
||||||
|
|
||||||
if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
|
if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
|
||||||
final int endOfLastLine = itemContentView
|
final int endOfLastLine = itemContentView
|
||||||
.getLayout()
|
.getLayout()
|
||||||
.getLineEnd(COMMENT_DEFAULT_LINES - 1);
|
.getLineEnd(COMMENT_DEFAULT_LINES - 1);
|
||||||
int end = itemContentView.getText().toString().lastIndexOf(' ', endOfLastLine - 2);
|
int end = itemContentView.getText().toString().lastIndexOf(' ', endOfLastLine - 2);
|
||||||
if (end == -1) {
|
if (end == -1) {
|
||||||
end = Math.max(endOfLastLine - 2, 0);
|
end = Math.max(endOfLastLine - 2, 0);
|
||||||
|
}
|
||||||
|
final String newVal = itemContentView.getText().subSequence(0, end) + " …";
|
||||||
|
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
|
||||||
|
Loading…
Reference in New Issue
Block a user