Skip to content

Commit

Permalink
Add ability to copy hashtags, URLs and timestamps in descriptions on …
Browse files Browse the repository at this point in the history
…long-press

This commit adds the ability to copy to clipboard hashtags, URLs and timestamps
when long-pressing them.

Some changes in our TextView class related to text setting have been required
and metadata items are now using a NewPipeTextView instead of a standard
TextView.

Six new classes have been added:

- a custom LinkMovementMethod class;
- a custom ClickableSpan class, LongPressClickableSpan, in order to set a long
  press event;
- a class to avoid code duplication in CommentTextOnTouchListener, TouchUtils;
- three implementations of LongPressClickableSpan used when linkifying text:
  - HashtagLongPressClickableSpan for hashtags;
  - TimestampLongPressClickableSpan for timestamps;
  - UrlLongPressClickableSpan for URLs.
  • Loading branch information
AudricV authored and Stypox committed Nov 29, 2022
1 parent 2984649 commit aaae49a
Show file tree
Hide file tree
Showing 16 changed files with 507 additions and 209 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static android.text.TextUtils.isEmpty;
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.util.Localization.getAppLocale;

import android.os.Bundle;
import android.view.LayoutInflater;
Expand Down Expand Up @@ -134,7 +135,8 @@ private void loadDescriptionContent() {
TextLinkifier.createLinksFromMarkdownText(binding.detailDescriptionView,
description.getContent(), streamInfo, descriptionDisposables);
break;
case Description.PLAIN_TEXT: default:
case Description.PLAIN_TEXT:
default:
TextLinkifier.createLinksFromPlainText(binding.detailDescriptionView,
description.getContent(), streamInfo, descriptionDisposables);
break;
Expand All @@ -144,30 +146,30 @@ private void loadDescriptionContent() {

private void setupMetadata(final LayoutInflater inflater,
final LinearLayout layout) {
addMetadataItem(inflater, layout, false,
R.string.metadata_category, streamInfo.getCategory());
addMetadataItem(inflater, layout, false, R.string.metadata_category,
streamInfo.getCategory());

addMetadataItem(inflater, layout, false,
R.string.metadata_licence, streamInfo.getLicence());
addMetadataItem(inflater, layout, false, R.string.metadata_licence,
streamInfo.getLicence());

addPrivacyMetadataItem(inflater, layout);

if (streamInfo.getAgeLimit() != NO_AGE_LIMIT) {
addMetadataItem(inflater, layout, false,
R.string.metadata_age_limit, String.valueOf(streamInfo.getAgeLimit()));
addMetadataItem(inflater, layout, false, R.string.metadata_age_limit,
String.valueOf(streamInfo.getAgeLimit()));
}

if (streamInfo.getLanguageInfo() != null) {
addMetadataItem(inflater, layout, false,
R.string.metadata_language, streamInfo.getLanguageInfo().getDisplayLanguage());
addMetadataItem(inflater, layout, false, R.string.metadata_language,
streamInfo.getLanguageInfo().getDisplayLanguage(getAppLocale(getContext())));
}

addMetadataItem(inflater, layout, true,
R.string.metadata_support, streamInfo.getSupportInfo());
addMetadataItem(inflater, layout, true,
R.string.metadata_host, streamInfo.getHost());
addMetadataItem(inflater, layout, true,
R.string.metadata_thumbnail_url, streamInfo.getThumbnailUrl());
addMetadataItem(inflater, layout, true, R.string.metadata_support,
streamInfo.getSupportInfo());
addMetadataItem(inflater, layout, true, R.string.metadata_host,
streamInfo.getHost());
addMetadataItem(inflater, layout, true, R.string.metadata_thumbnail_url,
streamInfo.getThumbnailUrl());

addTagsMetadataItem(inflater, layout);
}
Expand All @@ -191,12 +193,14 @@ private void addMetadataItem(final LayoutInflater inflater,
});

if (linkifyContent) {
TextLinkifier.createLinksFromPlainText(itemBinding.metadataContentView, content, null,
descriptionDisposables);
TextLinkifier.createLinksFromPlainText(itemBinding.metadataContentView, content,
null, descriptionDisposables);
} else {
itemBinding.metadataContentView.setText(content);
}

itemBinding.metadataContentView.setClickable(true);

layout.addView(itemBinding.getRoot());
}

Expand Down Expand Up @@ -245,14 +249,15 @@ private void addPrivacyMetadataItem(final LayoutInflater inflater, final LinearL
case INTERNAL:
contentRes = R.string.metadata_privacy_internal;
break;
case OTHER: default:
case OTHER:
default:
contentRes = 0;
break;
}

if (contentRes != 0) {
addMetadataItem(inflater, layout, false,
R.string.metadata_privacy, getString(contentRes));
addMetadataItem(inflater, layout, false, R.string.metadata_privacy,
getString(contentRes));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.schabi.newpipe.util;

import android.text.Layout;
import static org.schabi.newpipe.util.TouchUtils.getOffsetForHorizontalLine;

import android.text.Selection;
import android.text.Spannable;
import android.text.Spanned;
Expand Down Expand Up @@ -30,23 +31,9 @@ public boolean onTouch(final View v, final MotionEvent event) {

final int action = event.getAction();

if (action == MotionEvent.ACTION_UP
|| action == MotionEvent.ACTION_DOWN) {
int x = (int) event.getX();
int y = (int) event.getY();

x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();

x += widget.getScrollX();
y += widget.getScrollY();

final Layout layout = widget.getLayout();
final int line = layout.getLineForVertical(y);
final int off = layout.getOffsetForHorizontal(line, x);

final ClickableSpan[] link = buffer.getSpans(off, off,
ClickableSpan.class);
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
final int offset = getOffsetForHorizontalLine(widget, event);
final ClickableSpan[] link = buffer.getSpans(offset, offset, ClickableSpan.class);

if (link.length != 0) {
if (action == MotionEvent.ACTION_UP) {
Expand All @@ -58,8 +45,7 @@ public boolean onTouch(final View v, final MotionEvent event) {
}
}
} else if (action == MotionEvent.ACTION_DOWN) {
Selection.setSelection(buffer,
buffer.getSpanStart(link[0]),
Selection.setSelection(buffer, buffer.getSpanStart(link[0]),
buffer.getSpanEnd(link[0]));
}
return true;
Expand Down
38 changes: 38 additions & 0 deletions app/src/main/java/org/schabi/newpipe/util/TouchUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.schabi.newpipe.util;

import android.text.Layout;
import android.view.MotionEvent;
import android.widget.TextView;

import androidx.annotation.NonNull;

public final class TouchUtils {

private TouchUtils() {
}

/**
* Get the character offset on the closest line to the position pressed by the user of a
* {@link TextView} from a {@link MotionEvent} which was fired on this {@link TextView}.
*
* @param textView the {@link TextView} on which the {@link MotionEvent} was fired
* @param event the {@link MotionEvent} which was fired
* @return the character offset on the closest line to the position pressed by the user
*/
public static int getOffsetForHorizontalLine(@NonNull final TextView textView,
@NonNull final MotionEvent event) {

int x = (int) event.getX();
int y = (int) event.getY();

x -= textView.getTotalPaddingLeft();
y -= textView.getTotalPaddingTop();

x += textView.getScrollX();
y += textView.getScrollY();

final Layout layout = textView.getLayout();
final int line = layout.getLineForVertical(y);
return layout.getOffsetForHorizontal(line, x);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.schabi.newpipe.util.external_communication;

import android.content.Context;
import android.view.View;

import androidx.annotation.NonNull;

import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.views.LongPressClickableSpan;

final class HashtagLongPressClickableSpan extends LongPressClickableSpan {

@NonNull
private final Context context;
@NonNull
private final String parsedHashtag;
@NonNull
private final Info relatedInfo;

HashtagLongPressClickableSpan(@NonNull final Context context,
@NonNull final String parsedHashtag,
@NonNull final Info relatedInfo) {
this.context = context;
this.parsedHashtag = parsedHashtag;
this.relatedInfo = relatedInfo;
}

@Override
public void onClick(@NonNull final View view) {
NavigationHelper.openSearch(context, relatedInfo.getServiceId(), parsedHashtag);
}

@Override
public void onLongClick(@NonNull final View view) {
ShareUtils.copyToClipboard(context, parsedHashtag);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -313,10 +313,15 @@ public static void copyToClipboard(@NonNull final Context context, final String
return;
}

clipboardManager.setPrimaryClip(ClipData.newPlainText(null, text));
if (Build.VERSION.SDK_INT < 33) {
// Android 13 has its own "copied to clipboard" dialog
Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show();
try {
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, text));
if (Build.VERSION.SDK_INT < 33) {
// Android 13 has its own "copied to clipboard" dialog
Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show();
}
} catch (final Exception e) {
Log.e(TAG, "Error when trying to copy text to clipboard", e);
Toast.makeText(context, R.string.msg_failed_to_copy, Toast.LENGTH_SHORT).show();
}
}

Expand Down
Loading

0 comments on commit aaae49a

Please sign in to comment.