mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 07:13:00 +00:00 
			
		
		
		
	Merge branch 'master' into dev
This commit is contained in:
		| @@ -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.utils.Utils.isBlank; | ||||
| 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.view.LayoutInflater; | ||||
| @@ -112,7 +113,10 @@ public class DescriptionFragment extends BaseFragment { | ||||
|  | ||||
|     private void disableDescriptionSelection() { | ||||
|         // 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.detailDescriptionView.setTextIsSelectable(false); | ||||
| @@ -123,27 +127,6 @@ public class DescriptionFragment extends BaseFragment { | ||||
|         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, | ||||
|                                final LinearLayout layout) { | ||||
|         addMetadataItem(inflater, layout, false, R.string.metadata_category, | ||||
| @@ -193,8 +176,8 @@ public class DescriptionFragment extends BaseFragment { | ||||
|         }); | ||||
|  | ||||
|         if (linkifyContent) { | ||||
|             TextLinkifier.createLinksFromPlainText(itemBinding.metadataContentView, content, | ||||
|                     null, descriptionDisposables); | ||||
|             TextLinkifier.fromPlainText(itemBinding.metadataContentView, content, null, null, | ||||
|                     descriptionDisposables, SET_LINK_MOVEMENT_METHOD); | ||||
|         } else { | ||||
|             itemBinding.metadataContentView.setText(content); | ||||
|         } | ||||
|   | ||||
| @@ -865,7 +865,8 @@ public final class VideoDetailFragment | ||||
|                             if (playQueue == null) { | ||||
|                                 playQueue = new SinglePlayQueue(result); | ||||
|                             } | ||||
|                             if (stack.isEmpty() || !stack.peek().getPlayQueue().equals(playQueue)) { | ||||
|                             if (stack.isEmpty() || !stack.peek().getPlayQueue() | ||||
|                                     .equalStreams(playQueue)) { | ||||
|                                 stack.push(new StackItem(serviceId, url, title, playQueue)); | ||||
|                             } | ||||
|                         } | ||||
| @@ -1779,7 +1780,7 @@ public final class VideoDetailFragment | ||||
|         // deleted/added items inside Channel/Playlist queue and makes possible to have | ||||
|         // a history of played items | ||||
|         @Nullable final StackItem stackPeek = stack.peek(); | ||||
|         if (stackPeek != null && !stackPeek.getPlayQueue().equals(queue)) { | ||||
|         if (stackPeek != null && !stackPeek.getPlayQueue().equalStreams(queue)) { | ||||
|             @Nullable final PlayQueueItem playQueueItem = queue.getItem(); | ||||
|             if (playQueueItem != null) { | ||||
|                 stack.push(new StackItem(playQueueItem.getServiceId(), playQueueItem.getUrl(), | ||||
| @@ -1845,7 +1846,7 @@ public final class VideoDetailFragment | ||||
|         // They are not equal when user watches something in popup while browsing in fragment and | ||||
|         // then changes screen orientation. In that case the fragment will set itself as | ||||
|         // a service listener and will receive initial call to onMetadataUpdate() | ||||
|         if (!queue.equals(playQueue)) { | ||||
|         if (!queue.equalStreams(playQueue)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
| @@ -2102,7 +2103,7 @@ public final class VideoDetailFragment | ||||
|         final Iterator<StackItem> iterator = stack.descendingIterator(); | ||||
|         while (iterator.hasNext()) { | ||||
|             final StackItem next = iterator.next(); | ||||
|             if (next.getPlayQueue().equals(queue)) { | ||||
|             if (next.getPlayQueue().equalStreams(queue)) { | ||||
|                 item = next; | ||||
|                 break; | ||||
|             } | ||||
| @@ -2117,7 +2118,7 @@ public final class VideoDetailFragment | ||||
|         if (isClearingQueueConfirmationRequired(activity) | ||||
|                 && playerIsNotStopped() | ||||
|                 && activeQueue != null | ||||
|                 && !activeQueue.equals(playQueue)) { | ||||
|                 && !activeQueue.equalStreams(playQueue)) { | ||||
|             showClearingQueueConfirmation(onAllow); | ||||
|         } else { | ||||
|             onAllow.run(); | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| package org.schabi.newpipe.info_list.holder; | ||||
|  | ||||
| import android.graphics.Paint; | ||||
| import android.text.Layout; | ||||
| import android.text.TextUtils; | ||||
| import android.text.method.LinkMovementMethod; | ||||
| import android.text.style.URLSpan; | ||||
| import android.text.util.Linkify; | ||||
| import android.util.Log; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| @@ -11,27 +12,36 @@ import android.widget.ImageView; | ||||
| import android.widget.RelativeLayout; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import androidx.annotation.Nullable; | ||||
| 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.error.ErrorUtil; | ||||
| 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.exceptions.ExtractionException; | ||||
| import org.schabi.newpipe.extractor.stream.Description; | ||||
| import org.schabi.newpipe.info_list.InfoItemBuilder; | ||||
| 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.Localization; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.PicassoHelper; | ||||
| 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 { | ||||
|     private static final String TAG = "CommentsMiniIIHolder"; | ||||
|     private static final String ELLIPSIS = "…"; | ||||
|  | ||||
|     private static final int COMMENT_DEFAULT_LINES = 2; | ||||
|     private static final int COMMENT_EXPANDED_LINES = 1000; | ||||
| @@ -39,13 +49,18 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { | ||||
|     private final int commentHorizontalPadding; | ||||
|     private final int commentVerticalPadding; | ||||
|  | ||||
|     private final Paint paintAtContentSize; | ||||
|     private final float ellipsisWidthPx; | ||||
|  | ||||
|     private final RelativeLayout itemRoot; | ||||
|     private final ImageView itemThumbnailView; | ||||
|     private final TextView itemContentView; | ||||
|     private final TextView itemLikesCountView; | ||||
|     private final TextView itemPublishedTime; | ||||
|  | ||||
|     private String commentText; | ||||
|     private final CompositeDisposable disposables = new CompositeDisposable(); | ||||
|     private Description commentText; | ||||
|     private StreamingService streamService; | ||||
|     private String streamUrl; | ||||
|  | ||||
|     CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId, | ||||
| @@ -62,6 +77,10 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { | ||||
|                 .getResources().getDimension(R.dimen.comments_horizontal_padding); | ||||
|         commentVerticalPadding = (int) infoItemBuilder.getContext() | ||||
|                 .getResources().getDimension(R.dimen.comments_vertical_padding); | ||||
|  | ||||
|         paintAtContentSize = new Paint(); | ||||
|         paintAtContentSize.setTextSize(itemContentView.getTextSize()); | ||||
|         ellipsisWidthPx = paintAtContentSize.measureText(ELLIPSIS); | ||||
|     } | ||||
|  | ||||
|     public CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, | ||||
| @@ -91,18 +110,20 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { | ||||
|  | ||||
|         itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item)); | ||||
|  | ||||
|         streamUrl = item.getUrl(); | ||||
|  | ||||
|         itemContentView.setLines(COMMENT_DEFAULT_LINES); | ||||
|         commentText = item.getCommentText(); | ||||
|         itemContentView.setText(commentText, TextView.BufferType.SPANNABLE); | ||||
|         itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE); | ||||
|  | ||||
|         if (itemContentView.getLineCount() == 0) { | ||||
|             itemContentView.post(this::ellipsize); | ||||
|         } else { | ||||
|             ellipsize(); | ||||
|         try { | ||||
|             streamService = NewPipe.getService(item.getServiceId()); | ||||
|         } catch (final ExtractionException e) { | ||||
|             // should never happen | ||||
|             ErrorUtil.showUiErrorSnackbar(itemBuilder.getContext(), "Getting StreamingService", e); | ||||
|             Log.w(TAG, "Cannot obtain service from comment service id, defaulting to YouTube", e); | ||||
|             streamService = ServiceList.YouTube; | ||||
|         } | ||||
|         streamUrl = item.getUrl(); | ||||
|         commentText = item.getCommentText(); | ||||
|         ellipsize(); | ||||
|  | ||||
|         //noinspection ClickableViewAccessibility | ||||
|         itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE); | ||||
|  | ||||
|         if (item.getLikeCount() >= 0) { | ||||
|             itemLikesCountView.setText( | ||||
| @@ -132,7 +153,8 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { | ||||
|             if (DeviceUtils.isTv(itemBuilder.getContext())) { | ||||
|                 openCommentAuthor(item); | ||||
|             } else { | ||||
|                 ShareUtils.copyToClipboard(itemBuilder.getContext(), commentText); | ||||
|                 ShareUtils.copyToClipboard(itemBuilder.getContext(), | ||||
|                         itemContentView.getText().toString()); | ||||
|             } | ||||
|             return true; | ||||
|         }); | ||||
| @@ -172,7 +194,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { | ||||
|         return urls != null && urls.length != 0; | ||||
|     } | ||||
|  | ||||
|     private void determineLinkFocus() { | ||||
|     private void determineMovementMethod() { | ||||
|         if (shouldFocusLinks()) { | ||||
|             allowLinkFocus(); | ||||
|         } else { | ||||
| @@ -181,63 +203,73 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { | ||||
|     } | ||||
|  | ||||
|     private void ellipsize() { | ||||
|         boolean hasEllipsis = false; | ||||
|         itemContentView.setMaxLines(COMMENT_EXPANDED_LINES); | ||||
|         linkifyCommentContentView(v -> { | ||||
|             boolean hasEllipsis = false; | ||||
|  | ||||
|         if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) { | ||||
|             final int endOfLastLine = itemContentView | ||||
|                     .getLayout() | ||||
|                     .getLineEnd(COMMENT_DEFAULT_LINES - 1); | ||||
|             int end = itemContentView.getText().toString().lastIndexOf(' ', endOfLastLine - 2); | ||||
|             if (end == -1) { | ||||
|                 end = Math.max(endOfLastLine - 2, 0); | ||||
|             if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) { | ||||
|                 // Note that converting to String removes spans (i.e. links), but that's something | ||||
|                 // we actually want since when the text is ellipsized we want all clicks on the | ||||
|                 // comment to expand the comment, not to open links. | ||||
|                 final String text = itemContentView.getText().toString(); | ||||
|  | ||||
|                 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(); | ||||
|  | ||||
|         if (hasEllipsis) { | ||||
|             denyLinkFocus(); | ||||
|         } else { | ||||
|             determineLinkFocus(); | ||||
|         } | ||||
|             itemContentView.setMaxLines(COMMENT_DEFAULT_LINES); | ||||
|             if (hasEllipsis) { | ||||
|                 denyLinkFocus(); | ||||
|             } else { | ||||
|                 determineMovementMethod(); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     private void toggleEllipsize() { | ||||
|         if (itemContentView.getText().toString().equals(commentText)) { | ||||
|             if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) { | ||||
|                 ellipsize(); | ||||
|             } | ||||
|         } else { | ||||
|         final CharSequence text = itemContentView.getText(); | ||||
|         if (text.charAt(text.length() - 1) == ELLIPSIS.charAt(0)) { | ||||
|             expand(); | ||||
|         } else if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) { | ||||
|             ellipsize(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void expand() { | ||||
|         itemContentView.setMaxLines(COMMENT_EXPANDED_LINES); | ||||
|         itemContentView.setText(commentText); | ||||
|         linkify(); | ||||
|         determineLinkFocus(); | ||||
|         linkifyCommentContentView(v -> determineMovementMethod()); | ||||
|     } | ||||
|  | ||||
|     private void linkify() { | ||||
|         LinkifyCompat.addLinks(itemContentView, Linkify.WEB_URLS); | ||||
|         LinkifyCompat.addLinks(itemContentView, TimestampExtractor.TIMESTAMPS_PATTERN, null, null, | ||||
|                 (match, url) -> { | ||||
|                     try { | ||||
|                         final var timestampMatch = TimestampExtractor | ||||
|                                 .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; | ||||
|                     } | ||||
|                 }); | ||||
|     private void linkifyCommentContentView(@Nullable final Consumer<TextView> onCompletion) { | ||||
|         disposables.clear(); | ||||
|         if (commentText != null) { | ||||
|             TextLinkifier.fromDescription(itemContentView, commentText, | ||||
|                     HtmlCompat.FROM_HTML_MODE_LEGACY, streamService, streamUrl, disposables, | ||||
|                     onCompletion); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -348,7 +348,7 @@ public final class Player implements PlaybackListener, Listener { | ||||
|         final boolean playbackSkipSilence = getPrefs().getBoolean(getContext().getString( | ||||
|                 R.string.playback_skip_silence_key), getPlaybackSkipSilence()); | ||||
|  | ||||
|         final boolean samePlayQueue = playQueue != null && playQueue.equals(newQueue); | ||||
|         final boolean samePlayQueue = playQueue != null && playQueue.equalStreamsAndIndex(newQueue); | ||||
|         final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode()); | ||||
|         final boolean playWhenReady = intent.getBooleanExtra(PLAY_WHEN_READY, true); | ||||
|         final boolean isMuted = intent.getBooleanExtra(IS_MUTED, isMuted()); | ||||
|   | ||||
| @@ -1,21 +1,27 @@ | ||||
| package org.schabi.newpipe.player.mediasource; | ||||
|  | ||||
| import androidx.annotation.NonNull; | ||||
|  | ||||
| import com.google.android.exoplayer2.MediaItem; | ||||
| import com.google.android.exoplayer2.Timeline; | ||||
| import com.google.android.exoplayer2.source.CompositeMediaSource; | ||||
| import com.google.android.exoplayer2.source.MediaPeriod; | ||||
| import com.google.android.exoplayer2.source.MediaSource; | ||||
| import com.google.android.exoplayer2.source.WrappingMediaSource; | ||||
| import com.google.android.exoplayer2.upstream.Allocator; | ||||
| import com.google.android.exoplayer2.upstream.TransferListener; | ||||
|  | ||||
| import org.schabi.newpipe.player.mediaitem.MediaItemTag; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueItem; | ||||
|  | ||||
| public class LoadedMediaSource extends WrappingMediaSource implements ManagedMediaSource { | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
|  | ||||
| public class LoadedMediaSource extends CompositeMediaSource<Integer> implements ManagedMediaSource { | ||||
|     private final MediaSource source; | ||||
|     private final PlayQueueItem stream; | ||||
|     private final MediaItem mediaItem; | ||||
|     private final long expireTimestamp; | ||||
|  | ||||
|     /** | ||||
|      * Uses a {@link WrappingMediaSource} to wrap one child {@link MediaSource} | ||||
|      * Uses a {@link CompositeMediaSource} to wrap one or more child {@link MediaSource}s | ||||
|      * containing actual media. This wrapper {@link LoadedMediaSource} holds the expiration | ||||
|      * timestamp as a {@link ManagedMediaSource} to allow explicit playlist management under | ||||
|      * {@link ManagedMediaSourcePlaylist}. | ||||
| @@ -30,7 +36,7 @@ public class LoadedMediaSource extends WrappingMediaSource implements ManagedMed | ||||
|                              @NonNull final MediaItemTag tag, | ||||
|                              @NonNull final PlayQueueItem stream, | ||||
|                              final long expireTimestamp) { | ||||
|         super(source); | ||||
|         this.source = source; | ||||
|         this.stream = stream; | ||||
|         this.expireTimestamp = expireTimestamp; | ||||
|  | ||||
| @@ -45,6 +51,51 @@ public class LoadedMediaSource extends WrappingMediaSource implements ManagedMed | ||||
|         return System.currentTimeMillis() >= expireTimestamp; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Delegates the preparation of child {@link MediaSource}s to the | ||||
|      * {@link CompositeMediaSource} wrapper. Since all {@link LoadedMediaSource}s use only | ||||
|      * a single child media, the child id of 0 is always used (sonar doesn't like null as id here). | ||||
|      * | ||||
|      * @param mediaTransferListener A data transfer listener that will be registered by the | ||||
|      *                              {@link CompositeMediaSource} for child source preparation. | ||||
|      */ | ||||
|     @Override | ||||
|     protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) { | ||||
|         super.prepareSourceInternal(mediaTransferListener); | ||||
|         prepareChildSource(0, source); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * When any child {@link MediaSource} is prepared, the refreshed {@link Timeline} can | ||||
|      * be listened to here. But since {@link LoadedMediaSource} has only a single child source, | ||||
|      * this method is called only once until {@link #releaseSourceInternal()} is called. | ||||
|      * <br><br> | ||||
|      * On refresh, the {@link CompositeMediaSource} delegate will be notified with the | ||||
|      * new {@link Timeline}, otherwise {@link #createPeriod(MediaPeriodId, Allocator, long)} | ||||
|      * will not be called and playback may be stalled. | ||||
|      * | ||||
|      * @param id            The unique id used to prepare the child source. | ||||
|      * @param mediaSource   The child source whose source info has been refreshed. | ||||
|      * @param timeline      The new timeline of the child source. | ||||
|      */ | ||||
|     @Override | ||||
|     protected void onChildSourceInfoRefreshed(final Integer id, | ||||
|                                               final MediaSource mediaSource, | ||||
|                                               final Timeline timeline) { | ||||
|         refreshSourceInfo(timeline); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator, | ||||
|                                     final long startPositionUs) { | ||||
|         return source.createPeriod(id, allocator, startPositionUs); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void releasePeriod(final MediaPeriod mediaPeriod) { | ||||
|         source.releasePeriod(mediaPeriod); | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public MediaItem getMediaItem() { | ||||
|   | ||||
| @@ -518,12 +518,10 @@ public abstract class PlayQueue implements Serializable { | ||||
|      * This method also gives a chance to track history of items in a queue in | ||||
|      * VideoDetailFragment without duplicating items from two identical queues | ||||
|      */ | ||||
|     @Override | ||||
|     public boolean equals(@Nullable final Object obj) { | ||||
|         if (!(obj instanceof PlayQueue)) { | ||||
|     public boolean equalStreams(@Nullable final PlayQueue other) { | ||||
|         if (other == null) { | ||||
|             return false; | ||||
|         } | ||||
|         final PlayQueue other = (PlayQueue) obj; | ||||
|         if (size() != other.size()) { | ||||
|             return false; | ||||
|         } | ||||
| @@ -539,9 +537,11 @@ public abstract class PlayQueue implements Serializable { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int hashCode() { | ||||
|         return streams.hashCode(); | ||||
|     public boolean equalStreamsAndIndex(@Nullable final PlayQueue other) { | ||||
|         if (equalStreams(other)) { | ||||
|             return other.getIndex() == getIndex(); | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     public boolean isDisposed() { | ||||
|   | ||||
| @@ -20,6 +20,7 @@ | ||||
| package org.schabi.newpipe.util; | ||||
|  | ||||
| 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.util.Log; | ||||
| @@ -319,8 +320,9 @@ public final class ExtractorHelper { | ||||
|             } | ||||
|  | ||||
|             metaInfoSeparator.setVisibility(View.VISIBLE); | ||||
|             TextLinkifier.createLinksFromHtmlBlock(metaInfoTextView, stringBuilder.toString(), | ||||
|                     HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING, null, disposables); | ||||
|             TextLinkifier.fromHtml(metaInfoTextView, stringBuilder.toString(), | ||||
|                     HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING, null, null, disposables, | ||||
|                     SET_LINK_MOVEMENT_METHOD); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -90,19 +90,16 @@ public final class ShareUtils { | ||||
|             // No browser set as default (doesn't work on some devices) | ||||
|             openAppChooser(context, intent, true); | ||||
|         } else { | ||||
|             if (defaultPackageName.isEmpty()) { | ||||
|                 // No app installed to open a web url | ||||
|                 Toast.makeText(context, R.string.no_app_to_open_intent, Toast.LENGTH_LONG).show(); | ||||
|                 return false; | ||||
|             } else { | ||||
|                 try { | ||||
|             try { | ||||
|                 // will be empty on Android 12+ | ||||
|                 if (!defaultPackageName.isEmpty()) { | ||||
|                     intent.setPackage(defaultPackageName); | ||||
|                     context.startActivity(intent); | ||||
|                 } catch (final ActivityNotFoundException e) { | ||||
|                     // Not a browser but an app chooser because of OEMs changes | ||||
|                     intent.setPackage(null); | ||||
|                     openAppChooser(context, intent, true); | ||||
|                 } | ||||
|                 context.startActivity(intent); | ||||
|             } catch (final ActivityNotFoundException e) { | ||||
|                 // Not a browser but an app chooser because of OEMs changes | ||||
|                 intent.setPackage(null); | ||||
|                 openAppChooser(context, intent, true); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -2,51 +2,37 @@ package org.schabi.newpipe.util.text; | ||||
|  | ||||
| import static org.schabi.newpipe.util.text.TouchUtils.getOffsetForHorizontalLine; | ||||
|  | ||||
| import android.text.Selection; | ||||
| import android.text.Spannable; | ||||
| import android.annotation.SuppressLint; | ||||
| import android.text.Spanned; | ||||
| import android.text.style.ClickableSpan; | ||||
| import android.text.style.URLSpan; | ||||
| import android.view.MotionEvent; | ||||
| import android.view.View; | ||||
| 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 static final CommentTextOnTouchListener INSTANCE = new CommentTextOnTouchListener(); | ||||
|  | ||||
|     @SuppressLint("ClickableViewAccessibility") | ||||
|     @Override | ||||
|     public boolean onTouch(final View v, final MotionEvent event) { | ||||
|         if (!(v instanceof TextView)) { | ||||
|             return false; | ||||
|         } | ||||
|         final TextView widget = (TextView) v; | ||||
|         final Object text = widget.getText(); | ||||
|         final CharSequence text = widget.getText(); | ||||
|         if (text instanceof Spanned) { | ||||
|             final Spannable buffer = (Spannable) text; | ||||
|  | ||||
|             final Spanned buffer = (Spanned) text; | ||||
|             final int action = event.getAction(); | ||||
|  | ||||
|             if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) { | ||||
|                 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 (link[0] instanceof URLSpan) { | ||||
|                             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])); | ||||
|                         links[0].onClick(widget); | ||||
|                     } | ||||
|                     // we handle events that intersect links, so return true | ||||
|                     return true; | ||||
|                 } | ||||
|             } | ||||
|   | ||||
| @@ -5,7 +5,6 @@ import android.view.View; | ||||
|  | ||||
| import androidx.annotation.NonNull; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.Info; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.external_communication.ShareUtils; | ||||
|  | ||||
| @@ -15,20 +14,19 @@ final class HashtagLongPressClickableSpan extends LongPressClickableSpan { | ||||
|     private final Context context; | ||||
|     @NonNull | ||||
|     private final String parsedHashtag; | ||||
|     @NonNull | ||||
|     private final Info relatedInfo; | ||||
|     private final int relatedInfoServiceId; | ||||
|  | ||||
|     HashtagLongPressClickableSpan(@NonNull final Context context, | ||||
|                                   @NonNull final String parsedHashtag, | ||||
|                                   @NonNull final Info relatedInfo) { | ||||
|                                   final int relatedInfoServiceId) { | ||||
|         this.context = context; | ||||
|         this.parsedHashtag = parsedHashtag; | ||||
|         this.relatedInfo = relatedInfo; | ||||
|         this.relatedInfoServiceId = relatedInfoServiceId; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onClick(@NonNull final View view) { | ||||
|         NavigationHelper.openSearch(context, relatedInfo.getServiceId(), parsedHashtag); | ||||
|         NavigationHelper.openSearch(context, relatedInfoServiceId, parsedHashtag); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|   | ||||
| @@ -12,11 +12,12 @@ import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.core.text.HtmlCompat; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.Info; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.extractor.StreamingService; | ||||
| import org.schabi.newpipe.extractor.stream.Description; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.external_communication.ShareUtils; | ||||
|  | ||||
| import java.util.function.Consumer; | ||||
| import java.util.regex.Matcher; | ||||
| 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 | ||||
|     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() { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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. | ||||
|      * | ||||
|      * <p> | ||||
|      * This method will call {@link #changeIntentsOfDescriptionLinks(TextView, CharSequence, Info, | ||||
|      * CompositeDisposable)} after having linked the URLs with | ||||
|      * This method will call {@link #changeLinkIntents(TextView, CharSequence, StreamingService, | ||||
|      * String, CompositeDisposable, Consumer)} after having linked the URLs with | ||||
|      * {@link HtmlCompat#fromHtml(String, int)}. | ||||
|      * </p> | ||||
|      * | ||||
|      * @param textView       the {@link TextView} to set the the HTML string block linked | ||||
|      * @param htmlBlock      the HTML string block to be linked | ||||
|      * @param htmlCompatFlag the int flag to be set when {@link HtmlCompat#fromHtml(String, int)} | ||||
|      *                       will be called | ||||
|      * @param relatedInfo    if given, handle timestamps to open the stream in the popup player at | ||||
|      *                       the specific time, and hashtags to search for the term in the correct | ||||
|      *                       service | ||||
|      * @param disposables    disposables created by the method are added here and their lifecycle | ||||
|      *                       should be handled by the calling class | ||||
|      * @param textView           the {@link TextView} to set the the HTML string block linked | ||||
|      * @param htmlBlock          the HTML string block to be linked | ||||
|      * @param htmlCompatFlag     the int flag to be set when {@link HtmlCompat#fromHtml(String, | ||||
|      *                           int)} will be called | ||||
|      * @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 createLinksFromHtmlBlock(@NonNull final TextView textView, | ||||
|                                                 @NonNull final String htmlBlock, | ||||
|                                                 final int htmlCompatFlag, | ||||
|                                                 @Nullable final Info relatedInfo, | ||||
|                                                 @NonNull final CompositeDisposable disposables) { | ||||
|         changeIntentsOfDescriptionLinks(textView, HtmlCompat.fromHtml(htmlBlock, htmlCompatFlag), | ||||
|                 relatedInfo, disposables); | ||||
|     public static void fromHtml(@NonNull final TextView textView, | ||||
|                                 @NonNull final String htmlBlock, | ||||
|                                 final int htmlCompatFlag, | ||||
|                                 @Nullable final StreamingService relatedInfoService, | ||||
|                                 @Nullable final String relatedStreamUrl, | ||||
|                                 @NonNull final CompositeDisposable 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. | ||||
|      * | ||||
|      * <p> | ||||
|      * This method will call {@link #changeIntentsOfDescriptionLinks(TextView, CharSequence, Info, | ||||
|      * CompositeDisposable)} after having linked the URLs with {@link TextView#setAutoLinkMask(int)} | ||||
|      * and {@link TextView#setText(CharSequence, TextView.BufferType)}. | ||||
|      * This method will call {@link #changeLinkIntents(TextView, CharSequence, StreamingService, | ||||
|      * String, CompositeDisposable, Consumer)} after having linked the URLs with | ||||
|      * {@link TextView#setAutoLinkMask(int)} and | ||||
|      * {@link TextView#setText(CharSequence, TextView.BufferType)}. | ||||
|      * </p> | ||||
|      * | ||||
|      * @param textView       the {@link TextView} to set the plain text block 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 | ||||
|      *                       the specified time, and hashtags to search for the term in the correct | ||||
|      *                       service | ||||
|      * @param disposables    disposables created by the method are added here and their lifecycle | ||||
|      *                       should be handled by the calling class | ||||
|      * @param textView           the {@link TextView} to set the plain text block linked | ||||
|      * @param plainTextBlock     the block of plain text to be linked | ||||
|      * @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 createLinksFromPlainText(@NonNull final TextView textView, | ||||
|                                                 @NonNull final String plainTextBlock, | ||||
|                                                 @Nullable final Info relatedInfo, | ||||
|                                                 @NonNull final CompositeDisposable disposables) { | ||||
|     public static void fromPlainText(@NonNull final TextView textView, | ||||
|                                      @NonNull final String plainTextBlock, | ||||
|                                      @Nullable final StreamingService relatedInfoService, | ||||
|                                      @Nullable final String relatedStreamUrl, | ||||
|                                      @NonNull final CompositeDisposable disposables, | ||||
|                                      @Nullable final Consumer<TextView> onCompletion) { | ||||
|         textView.setAutoLinkMask(Linkify.WEB_URLS); | ||||
|         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. | ||||
|      * | ||||
|      * <p> | ||||
|      * This method will call {@link #changeIntentsOfDescriptionLinks(TextView, CharSequence, Info, | ||||
|      * CompositeDisposable)} after creating a {@link Markwon} object and using | ||||
|      * This method will call {@link #changeLinkIntents(TextView, CharSequence, StreamingService, | ||||
|      * String, CompositeDisposable, Consumer)} after creating a {@link Markwon} object and using | ||||
|      * {@link Markwon#setMarkdown(TextView, String)}. | ||||
|      * </p> | ||||
|      * | ||||
|      * @param textView      the {@link TextView} to set the plain text block 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 | ||||
|      *                      the specific time, and hashtags to search for the term in the correct | ||||
|      *                      service | ||||
|      * @param disposables   disposables created by the method are added here and their lifecycle | ||||
|      *                      should be handled by the calling class | ||||
|      * @param textView           the {@link TextView} to set the plain text block linked | ||||
|      * @param markdownBlock      the block of markdown text to be linked | ||||
|      * @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 createLinksFromMarkdownText(@NonNull final TextView textView, | ||||
|                                                    final String markdownBlock, | ||||
|                                                    @Nullable final Info relatedInfo, | ||||
|                                                    final CompositeDisposable disposables) { | ||||
|     public static void fromMarkdown(@NonNull final TextView textView, | ||||
|                                     @NonNull final String markdownBlock, | ||||
|                                     @Nullable final StreamingService relatedInfoService, | ||||
|                                     @Nullable final String relatedStreamUrl, | ||||
|                                     @NonNull final CompositeDisposable disposables, | ||||
|                                     @Nullable final Consumer<TextView> onCompletion) { | ||||
|         final Markwon markwon = Markwon.builder(textView.getContext()) | ||||
|                 .usePlugin(LinkifyPlugin.create()).build(); | ||||
|         changeIntentsOfDescriptionLinks(textView, markwon.toMarkdown(markdownBlock), relatedInfo, | ||||
|                 disposables); | ||||
|         changeLinkIntents(textView, markwon.toMarkdown(markdownBlock), | ||||
|                 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 | ||||
|      * the content in the popup player at the time indicated in the timestamp, by using | ||||
|      * {@link TextLinkifier#addClickListenersOnTimestamps(Context, SpannableStringBuilder, | ||||
|      * StreamInfo, CompositeDisposable)} method and click listeners on hashtags, by using | ||||
|      * {@link TextLinkifier#addClickListenersOnHashtags(Context, SpannableStringBuilder, Info)})}, | ||||
|      * which will open a search on the current service with the hashtag. | ||||
|      * StreamingService, String, CompositeDisposable)} method and click listeners on hashtags, by | ||||
|      * using {@link TextLinkifier#addClickListenersOnHashtags(Context, SpannableStringBuilder, | ||||
|      * StreamingService)}, which will open a search on the current service with the hashtag. | ||||
|      * </p> | ||||
|      * | ||||
|      * <p> | ||||
| @@ -141,20 +209,25 @@ public final class TextLinkifier { | ||||
|      * before opening a web link. | ||||
|      * </p> | ||||
|      * | ||||
|      * @param textView    the {@link TextView} in which the converted {@link CharSequence} will be | ||||
|      *                    applied | ||||
|      * @param chars       the {@link CharSequence} to be parsed | ||||
|      * @param relatedInfo if given, handle timestamps to open the stream in the popup player at the | ||||
|      *                    specific time, and hashtags to search for the term in the correct service | ||||
|      * @param disposables disposables created by the method are added here and their lifecycle | ||||
|      *                    should be handled by the calling class | ||||
|      * @param textView           the {@link TextView} to which the converted {@link CharSequence} | ||||
|      *                           will be applied | ||||
|      * @param chars              the {@link CharSequence} to be parsed | ||||
|      * @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 | ||||
|      */ | ||||
|     private static void changeIntentsOfDescriptionLinks( | ||||
|             @NonNull final TextView textView, | ||||
|             @NonNull final CharSequence chars, | ||||
|             @Nullable final Info relatedInfo, | ||||
|             @NonNull final CompositeDisposable disposables) { | ||||
|         textView.setMovementMethod(LongPressLinkMovementMethod.getInstance()); | ||||
|     private static void changeLinkIntents(@NonNull final TextView textView, | ||||
|                                           @NonNull final CharSequence chars, | ||||
|                                           @Nullable final StreamingService relatedInfoService, | ||||
|                                           @Nullable final String relatedStreamUrl, | ||||
|                                           @NonNull final CompositeDisposable disposables, | ||||
|                                           @Nullable final Consumer<TextView> onCompletion) { | ||||
|         disposables.add(Single.fromCallable(() -> { | ||||
|                     final Context context = textView.getContext(); | ||||
|  | ||||
| @@ -176,26 +249,26 @@ public final class TextLinkifier { | ||||
|                         textBlockLinked.removeSpan(span); | ||||
|                     } | ||||
|  | ||||
|                     if (relatedInfo != null) { | ||||
|                         // add click actions on plain text timestamps only for description of | ||||
|                         // contents, unneeded for meta-info or other TextViews | ||||
|                         if (relatedInfo instanceof StreamInfo) { | ||||
|                     // add click actions on plain text timestamps only for description of contents, | ||||
|                     // unneeded for meta-info or other TextViews | ||||
|                     if (relatedInfoService != null) { | ||||
|                         if (relatedStreamUrl != null) { | ||||
|                             addClickListenersOnTimestamps(context, textBlockLinked, | ||||
|                                     (StreamInfo) relatedInfo, disposables); | ||||
|                                     relatedInfoService, relatedStreamUrl, disposables); | ||||
|                         } | ||||
|  | ||||
|                         addClickListenersOnHashtags(context, textBlockLinked, relatedInfo); | ||||
|                         addClickListenersOnHashtags(context, textBlockLinked, relatedInfoService); | ||||
|                     } | ||||
|  | ||||
|                     return textBlockLinked; | ||||
|                 }).subscribeOn(Schedulers.computation()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe( | ||||
|                         textBlockLinked -> setTextViewCharSequence(textView, textBlockLinked), | ||||
|                         textBlockLinked -> | ||||
|                                 setTextViewCharSequence(textView, textBlockLinked, onCompletion), | ||||
|                         throwable -> { | ||||
|                             Log.e(TAG, "Unable to linkify text", throwable); | ||||
|                             // 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 spannableDescription the {@link SpannableStringBuilder} with the text of the | ||||
|      *                             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( | ||||
|             @NonNull final Context context, | ||||
|             @NonNull final SpannableStringBuilder spannableDescription, | ||||
|             @NonNull final Info relatedInfo) { | ||||
|             @NonNull final StreamingService relatedInfoService) { | ||||
|         final String descriptionText = spannableDescription.toString(); | ||||
|         final Matcher hashtagsMatches = HASHTAGS_PATTERN.matcher(descriptionText); | ||||
|  | ||||
| @@ -231,8 +304,9 @@ public final class TextLinkifier { | ||||
|             // of an URL, already parsed before | ||||
|             if (spannableDescription.getSpans(hashtagStart, hashtagEnd, | ||||
|                     LongPressClickableSpan.class).length == 0) { | ||||
|                 final int serviceId = relatedInfoService.getServiceId(); | ||||
|                 spannableDescription.setSpan( | ||||
|                         new HashtagLongPressClickableSpan(context, parsedHashtag, relatedInfo), | ||||
|                         new HashtagLongPressClickableSpan(context, parsedHashtag, serviceId), | ||||
|                         hashtagStart, hashtagEnd, 0); | ||||
|             } | ||||
|         } | ||||
| @@ -251,14 +325,16 @@ public final class TextLinkifier { | ||||
|      * @param context              the {@link Context} to use | ||||
|      * @param spannableDescription the {@link SpannableStringBuilder} with the text of the | ||||
|      *                             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 | ||||
|      *                             lifecycle should be handled by the calling class | ||||
|      */ | ||||
|     private static void addClickListenersOnTimestamps( | ||||
|             @NonNull final Context context, | ||||
|             @NonNull final SpannableStringBuilder spannableDescription, | ||||
|             @NonNull final StreamInfo streamInfo, | ||||
|             @NonNull final StreamingService relatedInfoService, | ||||
|             @NonNull final String relatedStreamUrl, | ||||
|             @NonNull final CompositeDisposable disposables) { | ||||
|         final String descriptionText = spannableDescription.toString(); | ||||
|         final Matcher timestampsMatches = TimestampExtractor.TIMESTAMPS_PATTERN.matcher( | ||||
| @@ -272,8 +348,9 @@ public final class TextLinkifier { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             spannableDescription.setSpan(new TimestampLongPressClickableSpan( | ||||
|                     context, descriptionText, disposables, streamInfo, timestampMatchDTO), | ||||
|             spannableDescription.setSpan( | ||||
|                     new TimestampLongPressClickableSpan(context, descriptionText, disposables, | ||||
|                             relatedInfoService, relatedStreamUrl, timestampMatchDTO), | ||||
|                     timestampMatchDTO.timestampStart(), | ||||
|                     timestampMatchDTO.timestampEnd(), | ||||
|                     0); | ||||
| @@ -281,8 +358,12 @@ public final class TextLinkifier { | ||||
|     } | ||||
|  | ||||
|     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.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.StreamingService; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.util.external_communication.ShareUtils; | ||||
|  | ||||
| import io.reactivex.rxjava3.disposables.CompositeDisposable; | ||||
| @@ -23,7 +22,9 @@ final class TimestampLongPressClickableSpan extends LongPressClickableSpan { | ||||
|     @NonNull | ||||
|     private final CompositeDisposable disposables; | ||||
|     @NonNull | ||||
|     private final StreamInfo streamInfo; | ||||
|     private final StreamingService relatedInfoService; | ||||
|     @NonNull | ||||
|     private final String relatedStreamUrl; | ||||
|     @NonNull | ||||
|     private final TimestampExtractor.TimestampMatchDTO timestampMatchDTO; | ||||
|  | ||||
| @@ -31,41 +32,43 @@ final class TimestampLongPressClickableSpan extends LongPressClickableSpan { | ||||
|             @NonNull final Context context, | ||||
|             @NonNull final String descriptionText, | ||||
|             @NonNull final CompositeDisposable disposables, | ||||
|             @NonNull final StreamInfo streamInfo, | ||||
|             @NonNull final StreamingService relatedInfoService, | ||||
|             @NonNull final String relatedStreamUrl, | ||||
|             @NonNull final TimestampExtractor.TimestampMatchDTO timestampMatchDTO) { | ||||
|         this.context = context; | ||||
|         this.descriptionText = descriptionText; | ||||
|         this.disposables = disposables; | ||||
|         this.streamInfo = streamInfo; | ||||
|         this.relatedInfoService = relatedInfoService; | ||||
|         this.relatedStreamUrl = relatedStreamUrl; | ||||
|         this.timestampMatchDTO = timestampMatchDTO; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onClick(@NonNull final View view) { | ||||
|         playOnPopup(context, streamInfo.getUrl(), streamInfo.getService(), | ||||
|         playOnPopup(context, relatedStreamUrl, relatedInfoService, | ||||
|                 timestampMatchDTO.seconds(), disposables); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onLongClick(@NonNull final View view) { | ||||
|         ShareUtils.copyToClipboard(context, | ||||
|                 getTimestampTextToCopy(streamInfo, descriptionText, timestampMatchDTO)); | ||||
|         ShareUtils.copyToClipboard(context, getTimestampTextToCopy( | ||||
|                 relatedInfoService, relatedStreamUrl, descriptionText, timestampMatchDTO)); | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     private static String getTimestampTextToCopy( | ||||
|             @NonNull final StreamInfo relatedInfo, | ||||
|             @NonNull final StreamingService relatedInfoService, | ||||
|             @NonNull final String relatedStreamUrl, | ||||
|             @NonNull final String descriptionText, | ||||
|             @NonNull final TimestampExtractor.TimestampMatchDTO timestampMatchDTO) { | ||||
|         // TODO: use extractor methods to get timestamps when this feature will be implemented in it | ||||
|         final StreamingService streamingService = relatedInfo.getService(); | ||||
|         if (streamingService == ServiceList.YouTube) { | ||||
|             return relatedInfo.getUrl() + "&t=" + timestampMatchDTO.seconds(); | ||||
|         } else if (streamingService == ServiceList.SoundCloud | ||||
|                 || streamingService == ServiceList.MediaCCC) { | ||||
|             return relatedInfo.getUrl() + "#t=" + timestampMatchDTO.seconds(); | ||||
|         } else if (streamingService == ServiceList.PeerTube) { | ||||
|             return relatedInfo.getUrl() + "?start=" + timestampMatchDTO.seconds(); | ||||
|         if (relatedInfoService == ServiceList.YouTube) { | ||||
|             return relatedStreamUrl + "&t=" + timestampMatchDTO.seconds(); | ||||
|         } else if (relatedInfoService == ServiceList.SoundCloud | ||||
|                 || relatedInfoService == ServiceList.MediaCCC) { | ||||
|             return relatedStreamUrl + "#t=" + timestampMatchDTO.seconds(); | ||||
|         } else if (relatedInfoService == ServiceList.PeerTube) { | ||||
|             return relatedStreamUrl + "?start=" + timestampMatchDTO.seconds(); | ||||
|         } | ||||
|  | ||||
|         // Return timestamp text for other services | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Stypox
					Stypox