diff --git a/app/build.gradle b/app/build.gradle index a0bc93a0d..8e4ce3725 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -179,7 +179,7 @@ dependencies { // NewPipe dependencies // You can use a local version by uncommenting a few lines in settings.gradle - implementation 'com.github.TeamNewPipe:NewPipeExtractor:85fa006214b003f21eacb76c445a167732f19981' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:79b5aa9760da52020821b68e2af41a9238943304' implementation "com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751" implementation "org.jsoup:jsoup:1.13.1" diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index 866b324ec..a77109f86 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -3,7 +3,6 @@ package org.schabi.newpipe.fragments; import android.content.Context; import android.content.res.ColorStateList; import android.os.Bundle; -import androidx.preference.PreferenceManager; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -19,6 +18,7 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround; +import androidx.preference.PreferenceManager; import androidx.viewpager.widget.ViewPager; import com.google.android.material.tabs.TabLayout; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 427cff06e..6560ab404 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -16,7 +16,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.provider.Settings; -import android.text.TextUtils; import android.text.util.Linkify; import android.util.DisplayMetrics; import android.util.Log; @@ -122,12 +121,14 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; +import static android.text.TextUtils.isEmpty; import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS; import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT; import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked; import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired; import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET; import static org.schabi.newpipe.util.AnimationUtils.animateView; +import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView; public final class VideoDetailFragment extends BaseStateFragment @@ -218,6 +219,9 @@ public final class VideoDetailFragment private TextView detailDurationView; private TextView detailPositionView; + private View detailMetaInfoSeparator; + private TextView detailMetaInfoTextView; + private LinearLayout videoDescriptionRootLayout; private TextView videoUploadDateView; private TextView videoDescriptionView; @@ -508,8 +512,8 @@ public final class VideoDetailFragment } break; case R.id.detail_uploader_root_layout: - if (TextUtils.isEmpty(currentInfo.getSubChannelUrl())) { - if (!TextUtils.isEmpty(currentInfo.getUploaderUrl())) { + if (isEmpty(currentInfo.getSubChannelUrl())) { + if (!isEmpty(currentInfo.getUploaderUrl())) { openChannel(currentInfo.getUploaderUrl(), currentInfo.getUploaderName()); } @@ -583,7 +587,7 @@ public final class VideoDetailFragment } break; case R.id.detail_uploader_root_layout: - if (TextUtils.isEmpty(currentInfo.getSubChannelUrl())) { + if (isEmpty(currentInfo.getSubChannelUrl())) { Log.w(TAG, "Can't open parent channel because we got no parent channel URL"); } else { @@ -644,6 +648,9 @@ public final class VideoDetailFragment detailDurationView = rootView.findViewById(R.id.detail_duration_view); detailPositionView = rootView.findViewById(R.id.detail_position_view); + detailMetaInfoSeparator = rootView.findViewById(R.id.detail_meta_info_separator); + detailMetaInfoTextView = rootView.findViewById(R.id.detail_meta_info_text_view); + videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout); videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view); videoDescriptionView = rootView.findViewById(R.id.detail_description_view); @@ -748,7 +755,7 @@ public final class VideoDetailFragment private void initThumbnailViews(@NonNull final StreamInfo info) { thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); - if (!TextUtils.isEmpty(info.getThumbnailUrl())) { + if (!isEmpty(info.getThumbnailUrl())) { final String infoServiceName = NewPipe.getNameOfService(info.getServiceId()); final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() { @Override @@ -763,12 +770,12 @@ public final class VideoDetailFragment ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener); } - if (!TextUtils.isEmpty(info.getSubChannelAvatarUrl())) { + if (!isEmpty(info.getSubChannelAvatarUrl())) { IMAGE_LOADER.displayImage(info.getSubChannelAvatarUrl(), subChannelThumb, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); } - if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) { + if (!isEmpty(info.getUploaderAvatarUrl())) { IMAGE_LOADER.displayImage(info.getUploaderAvatarUrl(), uploaderThumb, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); } @@ -1217,7 +1224,7 @@ public final class VideoDetailFragment } private void prepareDescription(final Description description) { - if (description == null || TextUtils.isEmpty(description.getContent()) + if (description == null || isEmpty(description.getContent()) || description == Description.emptyDescription) { return; } @@ -1462,9 +1469,9 @@ public final class VideoDetailFragment animateView(thumbnailPlayButton, true, 200); videoTitleTextView.setText(title); - if (!TextUtils.isEmpty(info.getSubChannelName())) { + if (!isEmpty(info.getSubChannelName())) { displayBothUploaderAndSubChannel(info); - } else if (!TextUtils.isEmpty(info.getUploaderName())) { + } else if (!isEmpty(info.getUploaderName())) { displayUploaderAsSubChannel(info); } else { uploaderTextView.setVisibility(View.GONE); @@ -1559,6 +1566,8 @@ public final class VideoDetailFragment prepareDescription(info.getDescription()); updateProgressInfo(info); initThumbnailViews(info); + showMetaInfoInTextView(info.getMetaInfo(), detailMetaInfoTextView, detailMetaInfoSeparator); + if (player == null || player.isPlayerStopped()) { updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl()); @@ -1610,7 +1619,7 @@ public final class VideoDetailFragment subChannelThumb.setVisibility(View.VISIBLE); - if (!TextUtils.isEmpty(info.getUploaderName())) { + if (!isEmpty(info.getUploaderName())) { uploaderTextView.setText( String.format(getString(R.string.video_detail_by), info.getUploaderName())); uploaderTextView.setVisibility(View.VISIBLE); @@ -2305,10 +2314,10 @@ public final class VideoDetailFragment private void updateOverlayData(@Nullable final String overlayTitle, @Nullable final String uploader, @Nullable final String thumbnailUrl) { - overlayTitleTextView.setText(TextUtils.isEmpty(overlayTitle) ? "" : overlayTitle); - overlayChannelTextView.setText(TextUtils.isEmpty(uploader) ? "" : uploader); + overlayTitleTextView.setText(isEmpty(title) ? "" : title); + overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader); overlayThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); - if (!TextUtils.isEmpty(thumbnailUrl)) { + if (!isEmpty(thumbnailUrl)) { IMAGE_LOADER.displayImage(thumbnailUrl, overlayThumbnailImageView, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, null); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index 02dbf176b..2dac6d11b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -39,6 +39,7 @@ import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; +import org.schabi.newpipe.extractor.MetaInfo; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.StreamingService; @@ -79,6 +80,7 @@ import io.reactivex.rxjava3.subjects.PublishSubject; import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags; import static java.util.Arrays.asList; import static org.schabi.newpipe.util.AnimationUtils.animateView; +import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView; public class SearchFragment extends BaseListFragment> implements BackPressable { @@ -129,6 +131,9 @@ public class SearchFragment extends BaseListFragment cannot be bundled without creating some containers + metaInfo = new MetaInfo[result.getMetaInfo().size()]; + metaInfo = result.getMetaInfo().toArray(metaInfo); + handleSearchSuggestion(); + showMetaInfoInTextView(result.getMetaInfo(), metaInfoTextView, metaInfoSeparator); + lastSearchedString = searchString; nextPage = result.getNextPage(); diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java index 650c5ae11..1f1b94545 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -22,9 +22,16 @@ package org.schabi.newpipe.util; import android.content.Context; import android.content.Intent; import android.os.Handler; +import android.text.method.LinkMovementMethod; import android.util.Log; +import android.view.View; +import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.Nullable; +import androidx.core.text.HtmlCompat; +import androidx.preference.PreferenceManager; + import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; @@ -32,6 +39,7 @@ import org.schabi.newpipe.extractor.Info; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage; import org.schabi.newpipe.extractor.ListInfo; +import org.schabi.newpipe.extractor.MetaInfo; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.StreamingService; @@ -60,6 +68,8 @@ import java.util.List; import io.reactivex.rxjava3.core.Maybe; import io.reactivex.rxjava3.core.Single; +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; + public final class ExtractorHelper { private static final String TAG = ExtractorHelper.class.getSimpleName(); private static final InfoCache CACHE = InfoCache.getInstance(); @@ -306,4 +316,73 @@ public final class ExtractorHelper { } }); } + + /** + * Formats the text contained in the meta info list as HTML and puts it into the text view, + * while also making the separator visible. If the list is null or empty, or the user chose not + * to see meta information, both the text view and the separator are hidden + * @param metaInfos a list of meta information, can be null or empty + * @param metaInfoTextView the text view in which to show the formatted HTML + * @param metaInfoSeparator another view to be shown or hidden accordingly to the text view + */ + public static void showMetaInfoInTextView(@Nullable final List metaInfos, + final TextView metaInfoTextView, + final View metaInfoSeparator) { + final Context context = metaInfoTextView.getContext(); + final boolean showMetaInfo = PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(context.getString(R.string.show_meta_info_key), true); + + if (!showMetaInfo || metaInfos == null || metaInfos.isEmpty()) { + metaInfoTextView.setVisibility(View.GONE); + metaInfoSeparator.setVisibility(View.GONE); + + } else { + final StringBuilder stringBuilder = new StringBuilder(); + for (final MetaInfo metaInfo : metaInfos) { + if (!isNullOrEmpty(metaInfo.getTitle())) { + stringBuilder.append("").append(metaInfo.getTitle()).append("") + .append(Localization.DOT_SEPARATOR); + } + + String content = metaInfo.getContent().getContent().trim(); + if (content.endsWith(".")) { + content = content.substring(0, content.length() - 1); // remove . at end + } + stringBuilder.append(content); + + for (int i = 0; i < metaInfo.getUrls().size(); i++) { + if (i == 0) { + stringBuilder.append(Localization.DOT_SEPARATOR); + } else { + stringBuilder.append("

"); + } + + stringBuilder + .append("") + .append(capitalizeIfAllUppercase(metaInfo.getUrlTexts().get(i).trim())) + .append(""); + } + } + + metaInfoTextView.setText(HtmlCompat.fromHtml(stringBuilder.toString(), + HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING)); + metaInfoTextView.setMovementMethod(LinkMovementMethod.getInstance()); + metaInfoTextView.setVisibility(View.VISIBLE); + metaInfoSeparator.setVisibility(View.VISIBLE); + } + } + + private static String capitalizeIfAllUppercase(final String text) { + for (int i = 0; i < text.length(); i++) { + if (Character.isLowerCase(text.charAt(i))) { + return text; // there is at least a lowercase letter -> not all uppercase + } + } + + if (text.isEmpty()) { + return text; + } else { + return text.substring(0, 1).toUpperCase() + text.substring(1).toLowerCase(); + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/Localization.java b/app/src/main/java/org/schabi/newpipe/util/Localization.java index 710827864..978f558c4 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Localization.java +++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java @@ -57,7 +57,7 @@ import java.util.Locale; public final class Localization { - private static final String DOT_SEPARATOR = " • "; + public static final String DOT_SEPARATOR = " • "; private static PrettyTime prettyTime; private Localization() { } diff --git a/app/src/main/res/layout-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index 8aee89ab0..d90c782ef 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -506,6 +506,23 @@ + + + + + + + + diff --git a/app/src/main/res/layout/fragment_video_detail.xml b/app/src/main/res/layout/fragment_video_detail.xml index 0df85fe95..758a88f19 100644 --- a/app/src/main/res/layout/fragment_video_detail.xml +++ b/app/src/main/res/layout/fragment_video_detail.xml @@ -491,6 +491,23 @@ + + + + show_play_with_kodi show_next_video show_comments + show_meta_info stream_info_selected_tab show_hold_to_append content_language diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ecb690044..d2dd8afd5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -93,6 +93,8 @@ Show comments Turn off to hide comments Turn off to prevent loading thumbnails, saving data and memory usage. Changes clear both in-memory and on-disk image cache. + Show meta info + Turn off to hide meta info boxes with additional information about the stream creator, stream content or a search request. Image cache wiped Wipe cached metadata Remove all cached webpage data diff --git a/app/src/main/res/xml/content_settings.xml b/app/src/main/res/xml/content_settings.xml index c885366ec..914fb2e59 100644 --- a/app/src/main/res/xml/content_settings.xml +++ b/app/src/main/res/xml/content_settings.xml @@ -85,6 +85,13 @@ android:title="@string/show_comments_title" app:iconSpaceReserved="false" /> + +