Skip to content

Commit 2db2918

Browse files
authored
Merge pull request TeamNewPipe#7725 from AudricV/add-long-press-actions-on-hashtags-and-links-in-descriptions
Add long press action on hashtags and web links in descriptions
2 parents 262b3a2 + 22c201b commit 2db2918

20 files changed

Lines changed: 513 additions & 217 deletions

app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static android.text.TextUtils.isEmpty;
44
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
55
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
6+
import static org.schabi.newpipe.util.Localization.getAppLocale;
67

78
import android.os.Bundle;
89
import android.view.LayoutInflater;
@@ -28,7 +29,7 @@
2829
import org.schabi.newpipe.util.Localization;
2930
import org.schabi.newpipe.util.NavigationHelper;
3031
import org.schabi.newpipe.util.external_communication.ShareUtils;
31-
import org.schabi.newpipe.util.external_communication.TextLinkifier;
32+
import org.schabi.newpipe.util.text.TextLinkifier;
3233

3334
import icepick.State;
3435
import io.reactivex.rxjava3.disposables.CompositeDisposable;
@@ -134,7 +135,8 @@ private void loadDescriptionContent() {
134135
TextLinkifier.createLinksFromMarkdownText(binding.detailDescriptionView,
135136
description.getContent(), streamInfo, descriptionDisposables);
136137
break;
137-
case Description.PLAIN_TEXT: default:
138+
case Description.PLAIN_TEXT:
139+
default:
138140
TextLinkifier.createLinksFromPlainText(binding.detailDescriptionView,
139141
description.getContent(), streamInfo, descriptionDisposables);
140142
break;
@@ -144,30 +146,30 @@ private void loadDescriptionContent() {
144146

145147
private void setupMetadata(final LayoutInflater inflater,
146148
final LinearLayout layout) {
147-
addMetadataItem(inflater, layout, false,
148-
R.string.metadata_category, streamInfo.getCategory());
149+
addMetadataItem(inflater, layout, false, R.string.metadata_category,
150+
streamInfo.getCategory());
149151

150-
addMetadataItem(inflater, layout, false,
151-
R.string.metadata_licence, streamInfo.getLicence());
152+
addMetadataItem(inflater, layout, false, R.string.metadata_licence,
153+
streamInfo.getLicence());
152154

153155
addPrivacyMetadataItem(inflater, layout);
154156

155157
if (streamInfo.getAgeLimit() != NO_AGE_LIMIT) {
156-
addMetadataItem(inflater, layout, false,
157-
R.string.metadata_age_limit, String.valueOf(streamInfo.getAgeLimit()));
158+
addMetadataItem(inflater, layout, false, R.string.metadata_age_limit,
159+
String.valueOf(streamInfo.getAgeLimit()));
158160
}
159161

160162
if (streamInfo.getLanguageInfo() != null) {
161-
addMetadataItem(inflater, layout, false,
162-
R.string.metadata_language, streamInfo.getLanguageInfo().getDisplayLanguage());
163+
addMetadataItem(inflater, layout, false, R.string.metadata_language,
164+
streamInfo.getLanguageInfo().getDisplayLanguage(getAppLocale(getContext())));
163165
}
164166

165-
addMetadataItem(inflater, layout, true,
166-
R.string.metadata_support, streamInfo.getSupportInfo());
167-
addMetadataItem(inflater, layout, true,
168-
R.string.metadata_host, streamInfo.getHost());
169-
addMetadataItem(inflater, layout, true,
170-
R.string.metadata_thumbnail_url, streamInfo.getThumbnailUrl());
167+
addMetadataItem(inflater, layout, true, R.string.metadata_support,
168+
streamInfo.getSupportInfo());
169+
addMetadataItem(inflater, layout, true, R.string.metadata_host,
170+
streamInfo.getHost());
171+
addMetadataItem(inflater, layout, true, R.string.metadata_thumbnail_url,
172+
streamInfo.getThumbnailUrl());
171173

172174
addTagsMetadataItem(inflater, layout);
173175
}
@@ -191,12 +193,14 @@ private void addMetadataItem(final LayoutInflater inflater,
191193
});
192194

193195
if (linkifyContent) {
194-
TextLinkifier.createLinksFromPlainText(itemBinding.metadataContentView, content, null,
195-
descriptionDisposables);
196+
TextLinkifier.createLinksFromPlainText(itemBinding.metadataContentView, content,
197+
null, descriptionDisposables);
196198
} else {
197199
itemBinding.metadataContentView.setText(content);
198200
}
199201

202+
itemBinding.metadataContentView.setClickable(true);
203+
200204
layout.addView(itemBinding.getRoot());
201205
}
202206

@@ -245,14 +249,15 @@ private void addPrivacyMetadataItem(final LayoutInflater inflater, final LinearL
245249
case INTERNAL:
246250
contentRes = R.string.metadata_privacy_internal;
247251
break;
248-
case OTHER: default:
252+
case OTHER:
253+
default:
249254
contentRes = 0;
250255
break;
251256
}
252257

253258
if (contentRes != 0) {
254-
addMetadataItem(inflater, layout, false,
255-
R.string.metadata_privacy, getString(contentRes));
259+
addMetadataItem(inflater, layout, false, R.string.metadata_privacy,
260+
getString(contentRes));
256261
}
257262
}
258263
}

app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@
2020
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
2121
import org.schabi.newpipe.info_list.InfoItemBuilder;
2222
import org.schabi.newpipe.local.history.HistoryRecordManager;
23-
import org.schabi.newpipe.util.CommentTextOnTouchListener;
23+
import org.schabi.newpipe.util.text.CommentTextOnTouchListener;
2424
import org.schabi.newpipe.util.DeviceUtils;
2525
import org.schabi.newpipe.util.Localization;
2626
import org.schabi.newpipe.util.NavigationHelper;
2727
import org.schabi.newpipe.util.PicassoHelper;
2828
import org.schabi.newpipe.util.external_communication.ShareUtils;
29-
import org.schabi.newpipe.util.external_communication.TimestampExtractor;
29+
import org.schabi.newpipe.util.text.TimestampExtractor;
3030

3131
import java.util.Objects;
3232

app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
import org.schabi.newpipe.extractor.stream.StreamInfo;
5252
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
5353
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
54-
import org.schabi.newpipe.util.external_communication.TextLinkifier;
54+
import org.schabi.newpipe.util.text.TextLinkifier;
5555

5656
import java.util.Collections;
5757
import java.util.List;

app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -313,10 +313,15 @@ public static void copyToClipboard(@NonNull final Context context, final String
313313
return;
314314
}
315315

316-
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, text));
317-
if (Build.VERSION.SDK_INT < 33) {
318-
// Android 13 has its own "copied to clipboard" dialog
319-
Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show();
316+
try {
317+
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, text));
318+
if (Build.VERSION.SDK_INT < 33) {
319+
// Android 13 has its own "copied to clipboard" dialog
320+
Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show();
321+
}
322+
} catch (final Exception e) {
323+
Log.e(TAG, "Error when trying to copy text to clipboard", e);
324+
Toast.makeText(context, R.string.msg_failed_to_copy, Toast.LENGTH_SHORT).show();
320325
}
321326
}
322327

app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java renamed to app/src/main/java/org/schabi/newpipe/util/text/CommentTextOnTouchListener.java

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
package org.schabi.newpipe.util;
1+
package org.schabi.newpipe.util.text;
2+
3+
import static org.schabi.newpipe.util.text.TouchUtils.getOffsetForHorizontalLine;
24

3-
import android.text.Layout;
45
import android.text.Selection;
56
import android.text.Spannable;
67
import android.text.Spanned;
@@ -11,7 +12,6 @@
1112
import android.widget.TextView;
1213

1314
import org.schabi.newpipe.util.external_communication.ShareUtils;
14-
import org.schabi.newpipe.util.external_communication.InternalUrlsHandler;
1515

1616
import io.reactivex.rxjava3.disposables.CompositeDisposable;
1717

@@ -30,23 +30,9 @@ public boolean onTouch(final View v, final MotionEvent event) {
3030

3131
final int action = event.getAction();
3232

33-
if (action == MotionEvent.ACTION_UP
34-
|| action == MotionEvent.ACTION_DOWN) {
35-
int x = (int) event.getX();
36-
int y = (int) event.getY();
37-
38-
x -= widget.getTotalPaddingLeft();
39-
y -= widget.getTotalPaddingTop();
40-
41-
x += widget.getScrollX();
42-
y += widget.getScrollY();
43-
44-
final Layout layout = widget.getLayout();
45-
final int line = layout.getLineForVertical(y);
46-
final int off = layout.getOffsetForHorizontal(line, x);
47-
48-
final ClickableSpan[] link = buffer.getSpans(off, off,
49-
ClickableSpan.class);
33+
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
34+
final int offset = getOffsetForHorizontalLine(widget, event);
35+
final ClickableSpan[] link = buffer.getSpans(offset, offset, ClickableSpan.class);
5036

5137
if (link.length != 0) {
5238
if (action == MotionEvent.ACTION_UP) {
@@ -58,8 +44,7 @@ public boolean onTouch(final View v, final MotionEvent event) {
5844
}
5945
}
6046
} else if (action == MotionEvent.ACTION_DOWN) {
61-
Selection.setSelection(buffer,
62-
buffer.getSpanStart(link[0]),
47+
Selection.setSelection(buffer, buffer.getSpanStart(link[0]),
6348
buffer.getSpanEnd(link[0]));
6449
}
6550
return true;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package org.schabi.newpipe.util.text;
2+
3+
import android.content.Context;
4+
import android.view.View;
5+
6+
import androidx.annotation.NonNull;
7+
8+
import org.schabi.newpipe.extractor.Info;
9+
import org.schabi.newpipe.util.NavigationHelper;
10+
import org.schabi.newpipe.util.external_communication.ShareUtils;
11+
12+
final class HashtagLongPressClickableSpan extends LongPressClickableSpan {
13+
14+
@NonNull
15+
private final Context context;
16+
@NonNull
17+
private final String parsedHashtag;
18+
@NonNull
19+
private final Info relatedInfo;
20+
21+
HashtagLongPressClickableSpan(@NonNull final Context context,
22+
@NonNull final String parsedHashtag,
23+
@NonNull final Info relatedInfo) {
24+
this.context = context;
25+
this.parsedHashtag = parsedHashtag;
26+
this.relatedInfo = relatedInfo;
27+
}
28+
29+
@Override
30+
public void onClick(@NonNull final View view) {
31+
NavigationHelper.openSearch(context, relatedInfo.getServiceId(), parsedHashtag);
32+
}
33+
34+
@Override
35+
public void onLongClick(@NonNull final View view) {
36+
ShareUtils.copyToClipboard(context, parsedHashtag);
37+
}
38+
}

app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java renamed to app/src/main/java/org/schabi/newpipe/util/text/InternalUrlsHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package org.schabi.newpipe.util.external_communication;
1+
package org.schabi.newpipe.util.text;
22

33
import android.content.Context;
44
import android.util.Log;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.schabi.newpipe.util.text;
2+
3+
import android.text.style.ClickableSpan;
4+
import android.view.View;
5+
6+
import androidx.annotation.NonNull;
7+
8+
public abstract class LongPressClickableSpan extends ClickableSpan {
9+
10+
public abstract void onLongClick(@NonNull View view);
11+
12+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package org.schabi.newpipe.util.text;
2+
3+
import static org.schabi.newpipe.util.text.TouchUtils.getOffsetForHorizontalLine;
4+
5+
import android.os.Handler;
6+
import android.os.Looper;
7+
import android.text.Selection;
8+
import android.text.Spannable;
9+
import android.text.method.LinkMovementMethod;
10+
import android.text.method.MovementMethod;
11+
import android.view.MotionEvent;
12+
import android.view.ViewConfiguration;
13+
import android.widget.TextView;
14+
15+
import androidx.annotation.NonNull;
16+
17+
// Class adapted from https://stackoverflow.com/a/31786969
18+
19+
public class LongPressLinkMovementMethod extends LinkMovementMethod {
20+
21+
private static final int LONG_PRESS_TIME = ViewConfiguration.getLongPressTimeout();
22+
23+
private static LongPressLinkMovementMethod instance;
24+
25+
private Handler longClickHandler;
26+
private boolean isLongPressed = false;
27+
28+
@Override
29+
public boolean onTouchEvent(@NonNull final TextView widget,
30+
@NonNull final Spannable buffer,
31+
@NonNull final MotionEvent event) {
32+
final int action = event.getAction();
33+
34+
if (action == MotionEvent.ACTION_CANCEL && longClickHandler != null) {
35+
longClickHandler.removeCallbacksAndMessages(null);
36+
}
37+
38+
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
39+
final int offset = getOffsetForHorizontalLine(widget, event);
40+
final LongPressClickableSpan[] link = buffer.getSpans(offset, offset,
41+
LongPressClickableSpan.class);
42+
43+
if (link.length != 0) {
44+
if (action == MotionEvent.ACTION_UP) {
45+
if (longClickHandler != null) {
46+
longClickHandler.removeCallbacksAndMessages(null);
47+
}
48+
if (!isLongPressed) {
49+
link[0].onClick(widget);
50+
}
51+
isLongPressed = false;
52+
} else {
53+
Selection.setSelection(buffer, buffer.getSpanStart(link[0]),
54+
buffer.getSpanEnd(link[0]));
55+
if (longClickHandler != null) {
56+
longClickHandler.postDelayed(() -> {
57+
link[0].onLongClick(widget);
58+
isLongPressed = true;
59+
}, LONG_PRESS_TIME);
60+
}
61+
}
62+
return true;
63+
}
64+
}
65+
66+
return super.onTouchEvent(widget, buffer, event);
67+
}
68+
69+
public static MovementMethod getInstance() {
70+
if (instance == null) {
71+
instance = new LongPressLinkMovementMethod();
72+
instance.longClickHandler = new Handler(Looper.myLooper());
73+
}
74+
75+
return instance;
76+
}
77+
}

0 commit comments

Comments
 (0)