Merge pull request #10091 from TeamNewPipe/feat/playlist_description

Add playlist description to playlist fragment
This commit is contained in:
Stypox 2023-12-29 10:58:13 +01:00 committed by GitHub
commit 8345f348f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 264 additions and 109 deletions

View File

@ -1,7 +1,9 @@
package org.schabi.newpipe.fragments.list.playlist; package org.schabi.newpipe.fragments.list.playlist;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
import static org.schabi.newpipe.ktx.ViewUtils.animate; import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling; import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
@ -37,6 +39,7 @@ import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.ServiceList; import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper; import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.info_list.dialog.InfoItemDialog; import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
@ -48,9 +51,10 @@ import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
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.image.PicassoHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.PlayButtonHelper; import org.schabi.newpipe.util.PlayButtonHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.text.TextEllipsizer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -321,6 +325,29 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
headerBinding.playlistStreamCount.setText(Localization headerBinding.playlistStreamCount.setText(Localization
.localizeStreamCount(getContext(), result.getStreamCount())); .localizeStreamCount(getContext(), result.getStreamCount()));
final Description description = result.getDescription();
if (description != null && description != Description.EMPTY_DESCRIPTION
&& !isBlank(description.getContent())) {
final TextEllipsizer ellipsizer = new TextEllipsizer(
headerBinding.playlistDescription, 5, getServiceById(result.getServiceId()));
ellipsizer.setStateChangeListener(isEllipsized ->
headerBinding.playlistDescriptionReadMore.setText(
Boolean.TRUE.equals(isEllipsized) ? R.string.show_more : R.string.show_less
));
ellipsizer.setOnContentChanged(canBeEllipsized -> {
headerBinding.playlistDescriptionReadMore.setVisibility(
Boolean.TRUE.equals(canBeEllipsized) ? View.VISIBLE : View.GONE);
if (Boolean.TRUE.equals(canBeEllipsized)) {
ellipsizer.ellipsize();
}
});
ellipsizer.setContent(description);
headerBinding.playlistDescriptionReadMore.setOnClickListener(v -> ellipsizer.toggle());
} else {
headerBinding.playlistDescription.setVisibility(View.GONE);
headerBinding.playlistDescriptionReadMore.setVisibility(View.GONE);
}
if (!result.getErrors().isEmpty()) { if (!result.getErrors().isEmpty()) {
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.REQUESTED_PLAYLIST, showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
result.getUrl(), result)); result.getUrl(), result));

View File

@ -1,10 +1,7 @@
package org.schabi.newpipe.info_list.holder; package org.schabi.newpipe.info_list.holder;
import static android.text.TextUtils.isEmpty;
import static org.schabi.newpipe.util.ServiceHelper.getServiceById; import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
import android.graphics.Paint;
import android.text.Layout;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.text.style.URLSpan; import android.text.style.URLSpan;
import android.view.View; import android.view.View;
@ -15,42 +12,28 @@ import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.text.HtmlCompat;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
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.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.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.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.ImageStrategy; import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper; import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.text.CommentTextOnTouchListener; import org.schabi.newpipe.util.text.CommentTextOnTouchListener;
import org.schabi.newpipe.util.text.TextLinkifier; import org.schabi.newpipe.util.text.TextEllipsizer;
import java.util.function.Consumer;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
public class CommentInfoItemHolder extends InfoItemHolder { public class CommentInfoItemHolder extends InfoItemHolder {
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 final int commentHorizontalPadding; private final int commentHorizontalPadding;
private final int commentVerticalPadding; private final int commentVerticalPadding;
private final Paint paintAtContentSize;
private final float ellipsisWidthPx;
private final RelativeLayout itemRoot; private final RelativeLayout itemRoot;
private final ImageView itemThumbnailView; private final ImageView itemThumbnailView;
private final TextView itemContentView; private final TextView itemContentView;
@ -61,13 +44,8 @@ public class CommentInfoItemHolder extends InfoItemHolder {
private final ImageView itemPinnedView; private final ImageView itemPinnedView;
private final Button repliesButton; private final Button repliesButton;
private final CompositeDisposable disposables = new CompositeDisposable(); @NonNull
@Nullable private final TextEllipsizer textEllipsizer;
private Description commentText;
@Nullable
private StreamingService streamService;
@Nullable
private String streamUrl;
public CommentInfoItemHolder(final InfoItemBuilder infoItemBuilder, public CommentInfoItemHolder(final InfoItemBuilder infoItemBuilder,
final ViewGroup parent) { final ViewGroup parent) {
@ -88,9 +66,14 @@ public class CommentInfoItemHolder extends InfoItemHolder {
commentVerticalPadding = (int) infoItemBuilder.getContext() commentVerticalPadding = (int) infoItemBuilder.getContext()
.getResources().getDimension(R.dimen.comments_vertical_padding); .getResources().getDimension(R.dimen.comments_vertical_padding);
paintAtContentSize = new Paint(); textEllipsizer = new TextEllipsizer(itemContentView, COMMENT_DEFAULT_LINES, null);
paintAtContentSize.setTextSize(itemContentView.getTextSize()); textEllipsizer.setStateChangeListener(isEllipsized -> {
ellipsisWidthPx = paintAtContentSize.measureText(ELLIPSIS); if (Boolean.TRUE.equals(isEllipsized)) {
denyLinkFocus();
} else {
determineMovementMethod();
}
});
} }
@Override @Override
@ -139,16 +122,16 @@ public class CommentInfoItemHolder extends InfoItemHolder {
// setup comment content and click listeners to expand/ellipsize it // setup comment content and click listeners to expand/ellipsize it
streamService = getServiceById(item.getServiceId()); textEllipsizer.setStreamingService(getServiceById(item.getServiceId()));
streamUrl = item.getUrl(); textEllipsizer.setStreamUrl(item.getUrl());
commentText = item.getCommentText(); textEllipsizer.setContent(item.getCommentText());
ellipsize(); textEllipsizer.ellipsize();
//noinspection ClickableViewAccessibility //noinspection ClickableViewAccessibility
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE); itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE);
itemView.setOnClickListener(view -> { itemView.setOnClickListener(view -> {
toggleEllipsize(); textEllipsizer.toggle();
if (itemBuilder.getOnCommentsSelectedListener() != null) { if (itemBuilder.getOnCommentsSelectedListener() != null) {
itemBuilder.getOnCommentsSelectedListener().selected(item); itemBuilder.getOnCommentsSelectedListener().selected(item);
} }
@ -202,76 +185,4 @@ public class CommentInfoItemHolder extends InfoItemHolder {
denyLinkFocus(); denyLinkFocus();
} }
} }
private void ellipsize() {
itemContentView.setMaxLines(COMMENT_EXPANDED_LINES);
linkifyCommentContentView(v -> {
boolean hasEllipsis = false;
final CharSequence charSeqText = itemContentView.getText();
if (charSeqText != null && 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 = charSeqText.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;
}
itemContentView.setMaxLines(COMMENT_DEFAULT_LINES);
if (hasEllipsis) {
denyLinkFocus();
} else {
determineMovementMethod();
}
});
}
private void toggleEllipsize() {
final CharSequence text = itemContentView.getText();
if (!isEmpty(text) && 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);
linkifyCommentContentView(v -> determineMovementMethod());
}
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);
}
}
} }

View File

@ -0,0 +1,193 @@
package org.schabi.newpipe.util.text;
import android.graphics.Paint;
import android.text.Layout;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.text.HtmlCompat;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.stream.Description;
import java.util.function.Consumer;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
/**
* <p>Class to ellipsize text inside a {@link TextView}.</p>
* This class provides all utils to automatically ellipsize and expand a text
*/
public final class TextEllipsizer {
private static final int EXPANDED_LINES = Integer.MAX_VALUE;
private static final String ELLIPSIS = "";
@NonNull private final CompositeDisposable disposable = new CompositeDisposable();
@NonNull private final TextView view;
private final int maxLines;
@NonNull private Description content;
@Nullable private StreamingService streamingService;
@Nullable private String streamUrl;
private boolean isEllipsized = false;
@Nullable private Boolean canBeEllipsized = null;
@NonNull private final Paint paintAtContentSize = new Paint();
private final float ellipsisWidthPx;
@Nullable private Consumer<Boolean> stateChangeListener = null;
@Nullable private Consumer<Boolean> onContentChanged;
public TextEllipsizer(@NonNull final TextView view,
final int maxLines,
@Nullable final StreamingService streamingService) {
this.view = view;
this.maxLines = maxLines;
this.content = Description.EMPTY_DESCRIPTION;
this.streamingService = streamingService;
paintAtContentSize.setTextSize(view.getTextSize());
ellipsisWidthPx = paintAtContentSize.measureText(ELLIPSIS);
}
public void setOnContentChanged(@Nullable final Consumer<Boolean> onContentChanged) {
this.onContentChanged = onContentChanged;
}
public void setContent(@NonNull final Description content) {
this.content = content;
canBeEllipsized = null;
linkifyContentView(v -> {
final int currentMaxLines = view.getMaxLines();
view.setMaxLines(EXPANDED_LINES);
canBeEllipsized = view.getLineCount() > maxLines;
view.setMaxLines(currentMaxLines);
if (onContentChanged != null) {
onContentChanged.accept(canBeEllipsized);
}
});
}
public void setStreamUrl(@Nullable final String streamUrl) {
this.streamUrl = streamUrl;
}
public void setStreamingService(@NonNull final StreamingService streamingService) {
this.streamingService = streamingService;
}
/**
* Expand the {@link TextEllipsizer#content} to its full length.
*/
public void expand() {
view.setMaxLines(EXPANDED_LINES);
linkifyContentView(v -> isEllipsized = false);
}
/**
* Shorten the {@link TextEllipsizer#content} to the given number of
* {@link TextEllipsizer#maxLines maximum lines} and add trailing '{@code }'
* if the text was shorted.
*/
public void ellipsize() {
// expand text to see whether it is necessary to ellipsize the text
view.setMaxLines(EXPANDED_LINES);
linkifyContentView(v -> {
final CharSequence charSeqText = view.getText();
if (charSeqText != null && view.getLineCount() > maxLines) {
// 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 = charSeqText.toString();
final Layout layout = view.getLayout();
final float lineWidth = layout.getLineWidth(maxLines - 1);
final float layoutWidth = layout.getWidth();
final int lineStart = layout.getLineStart(maxLines - 1);
final int lineEnd = layout.getLineEnd(maxLines - 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;
view.setText(newVal);
isEllipsized = true;
} else {
isEllipsized = false;
}
view.setMaxLines(maxLines);
});
}
/**
* Toggle the view between the ellipsized and expanded state.
*/
public void toggle() {
if (isEllipsized) {
expand();
} else {
ellipsize();
}
}
/**
* Whether the {@link #view} can be ellipsized.
* This is only the case when the {@link #content} has more lines
* than allowed via {@link #maxLines}.
* @return {@code true} if the {@link #content} has more lines than allowed via
* {@link #maxLines} and thus can be shortened, {@code false} if the {@code content} fits into
* the {@link #view} without being shortened and {@code null} if the initialization is not
* completed yet.
*/
@Nullable
public Boolean canBeEllipsized() {
return canBeEllipsized;
}
private void linkifyContentView(final Consumer<View> consumer) {
final boolean oldState = isEllipsized;
disposable.clear();
TextLinkifier.fromDescription(view, content,
HtmlCompat.FROM_HTML_MODE_LEGACY, streamingService, streamUrl, disposable,
v -> {
consumer.accept(v);
notifyStateChangeListener(oldState);
});
}
/**
* Add a listener which is called when the given content is changed,
* either from <em>ellipsized</em> to <em>full</em> or vice versa.
* @param listener The listener to be called, or {@code null} to remove it.
* The Boolean parameter is the new state.
* <em>Ellipsized</em> content is represented as {@code true},
* normal or <em>full</em> content by {@code false}.
*/
public void setStateChangeListener(@Nullable final Consumer<Boolean> listener) {
this.stateChangeListener = listener;
}
private void notifyStateChangeListener(final boolean oldState) {
if (oldState != isEllipsized && stateChangeListener != null) {
stateChangeListener.accept(isEllipsized);
}
}
}

View File

@ -80,10 +80,32 @@
tools:text="234 videos" /> tools:text="234 videos" />
</RelativeLayout> </RelativeLayout>
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/playlist_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/playlist_meta"
android:paddingHorizontal="@dimen/video_item_search_padding"
android:paddingTop="6dp"
android:maxLines="5"
tools:text="This is a multiline playlist description. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blandit Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blandit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blandit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blandit" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/playlist_description_read_more"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/playlist_description"
android:gravity="end"
android:paddingHorizontal="@dimen/video_item_search_padding"
android:paddingTop="6dp"
android:text="@string/show_more"
android:layout_marginBottom="6dp"
android:textColor="?attr/colorPrimary" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/playlist_meta"> android:layout_below="@id/playlist_description_read_more">
<include <include
android:id="@+id/playlist_control" android:id="@+id/playlist_control"

View File

@ -842,4 +842,6 @@
<item quantity="one">%s reply</item> <item quantity="one">%s reply</item>
<item quantity="other">%s replies</item> <item quantity="other">%s replies</item>
</plurals> </plurals>
<string name="show_more">Show more</string>
<string name="show_less">Show less</string>
</resources> </resources>