extends BaseA
return unknownSize;
}
- public void setSize(final int streamIndex, final long sizeInBytes) {
- streamSizes[streamIndex] = sizeInBytes;
- }
-
public void setSize(final T stream, final long sizeInBytes) {
streamSizes[streamsList.indexOf(stream)] = sizeInBytes;
}
diff --git a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java
index ea22e9368..ab74e0305 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java
@@ -41,6 +41,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
+import org.schabi.newpipe.info_list.ItemViewMode;
public final class ThemeHelper {
private ThemeHelper() {
@@ -332,7 +333,6 @@ public final class ThemeHelper {
}
}
-
/**
* Returns whether the grid layout or the list layout should be used. If the user set "auto"
* mode in settings, decides based on screen orientation (landscape) and size.
@@ -341,19 +341,8 @@ public final class ThemeHelper {
* @return true:use grid layout, false:use list layout
*/
public static boolean shouldUseGridLayout(final Context context) {
- final String listMode = PreferenceManager.getDefaultSharedPreferences(context)
- .getString(context.getString(R.string.list_view_mode_key),
- context.getString(R.string.list_view_mode_value));
-
- if (listMode.equals(context.getString(R.string.list_view_mode_list_key))) {
- return false;
- } else if (listMode.equals(context.getString(R.string.list_view_mode_grid_key))) {
- return true;
- } else /* listMode.equals("auto") */ {
- final Configuration configuration = context.getResources().getConfiguration();
- return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
- && configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
- }
+ final ItemViewMode mode = getItemViewMode(context);
+ return mode == ItemViewMode.GRID;
}
/**
@@ -367,6 +356,36 @@ public final class ThemeHelper {
context.getResources().getDimensionPixelSize(R.dimen.channel_item_grid_min_width));
}
+ /**
+ * Returns item view mode.
+ * @param context to read preference and parse string
+ * @return Returns one of ItemViewMode
+ */
+ public static ItemViewMode getItemViewMode(final Context context) {
+ final String listMode = PreferenceManager.getDefaultSharedPreferences(context)
+ .getString(context.getString(R.string.list_view_mode_key),
+ context.getString(R.string.list_view_mode_value));
+ final ItemViewMode result;
+ if (listMode.equals(context.getString(R.string.list_view_mode_list_key))) {
+ result = ItemViewMode.LIST;
+ } else if (listMode.equals(context.getString(R.string.list_view_mode_grid_key))) {
+ result = ItemViewMode.GRID;
+ } else if (listMode.equals(context.getString(R.string.list_view_mode_card_key))) {
+ result = ItemViewMode.CARD;
+ } else {
+ // Auto mode - evaluate whether to use Grid based on screen real estate.
+ final Configuration configuration = context.getResources().getConfiguration();
+ final boolean useGrid = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
+ && configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
+ if (useGrid) {
+ result = ItemViewMode.GRID;
+ } else {
+ result = ItemViewMode.LIST;
+ }
+ }
+ return result;
+ }
+
/**
* Calculates the number of grid stream info items that can fit horizontally on the screen. The
* width of a grid stream info item is obtained from the thumbnail width plus the right and left
diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java
index debeb902c..06dd3f945 100644
--- a/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java
+++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java
@@ -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);
}
}
@@ -313,8 +310,16 @@ public final class ShareUtils {
return;
}
- clipboardManager.setPrimaryClip(ClipData.newPlainText(null, text));
- Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show();
+ try {
+ clipboardManager.setPrimaryClip(ClipData.newPlainText(null, text));
+ if (Build.VERSION.SDK_INT < 33) {
+ // Android 13 has its own "copied to clipboard" dialog
+ Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show();
+ }
+ } catch (final Exception e) {
+ Log.e(TAG, "Error when trying to copy text to clipboard", e);
+ Toast.makeText(context, R.string.msg_failed_to_copy, Toast.LENGTH_SHORT).show();
+ }
}
/**
diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java
deleted file mode 100644
index 8b8eb265b..000000000
--- a/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java
+++ /dev/null
@@ -1,289 +0,0 @@
-package org.schabi.newpipe.util.external_communication;
-
-import android.content.Context;
-import android.text.SpannableStringBuilder;
-import android.text.method.LinkMovementMethod;
-import android.text.style.ClickableSpan;
-import android.text.style.URLSpan;
-import android.text.util.Linkify;
-import android.util.Log;
-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.Info;
-import org.schabi.newpipe.extractor.stream.StreamInfo;
-import org.schabi.newpipe.util.NavigationHelper;
-
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import io.noties.markwon.Markwon;
-import io.noties.markwon.linkify.LinkifyPlugin;
-import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
-import io.reactivex.rxjava3.core.Single;
-import io.reactivex.rxjava3.disposables.CompositeDisposable;
-import io.reactivex.rxjava3.schedulers.Schedulers;
-
-import static org.schabi.newpipe.util.external_communication.InternalUrlsHandler.playOnPopup;
-
-public final class TextLinkifier {
- public static final String TAG = TextLinkifier.class.getSimpleName();
-
- // 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_]+)");
-
- private TextLinkifier() {
- }
-
- /**
- * Create web links for contents with an HTML description.
- *
- * This will call {@link TextLinkifier#changeIntentsOfDescriptionLinks(TextView, CharSequence,
- * Info, CompositeDisposable)} after having linked the URLs with
- * {@link HtmlCompat#fromHtml(String, int)}.
- *
- * @param textView the TextView to set the htmlBlock linked
- * @param htmlBlock the htmlBlock 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
- */
- public static void createLinksFromHtmlBlock(@NonNull final TextView textView,
- final String htmlBlock,
- final int htmlCompatFlag,
- @Nullable final Info relatedInfo,
- final CompositeDisposable disposables) {
- changeIntentsOfDescriptionLinks(
- textView, HtmlCompat.fromHtml(htmlBlock, htmlCompatFlag), relatedInfo, disposables);
- }
-
- /**
- * Create web links for contents with a plain text description.
- *
- * This will call {@link TextLinkifier#changeIntentsOfDescriptionLinks(TextView, CharSequence,
- * Info, CompositeDisposable)} after having linked the URLs with
- * {@link TextView#setAutoLinkMask(int)} and
- * {@link TextView#setText(CharSequence, TextView.BufferType)}.
- *
- * @param textView the 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 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
- */
- public static void createLinksFromPlainText(@NonNull final TextView textView,
- final String plainTextBlock,
- @Nullable final Info relatedInfo,
- final CompositeDisposable disposables) {
- textView.setAutoLinkMask(Linkify.WEB_URLS);
- textView.setText(plainTextBlock, TextView.BufferType.SPANNABLE);
- changeIntentsOfDescriptionLinks(textView, textView.getText(), relatedInfo, disposables);
- }
-
- /**
- * Create web links for contents with a markdown description.
- *
- * This will call {@link TextLinkifier#changeIntentsOfDescriptionLinks(TextView, CharSequence,
- * Info, CompositeDisposable)} after creating an {@link Markwon} object and using
- * {@link Markwon#setMarkdown(TextView, String)}.
- *
- * @param textView the 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
- * @param disposables disposables created by the method are added here and their lifecycle
- * should be handled by the calling class
- */
- public static void createLinksFromMarkdownText(@NonNull final TextView textView,
- final String markdownBlock,
- @Nullable final Info relatedInfo,
- final CompositeDisposable disposables) {
- final Markwon markwon = Markwon.builder(textView.getContext())
- .usePlugin(LinkifyPlugin.create()).build();
- changeIntentsOfDescriptionLinks(textView, markwon.toMarkdown(markdownBlock), relatedInfo,
- disposables);
- }
-
- /**
- * Add click listeners which opens a search on hashtags in a plain text.
- *
- * This method finds all timestamps in the {@link SpannableStringBuilder} of the description
- * using a regular expression, adds for each a {@link ClickableSpan} which opens
- * {@link NavigationHelper#openSearch(Context, int, String)} and makes a search on the hashtag,
- * in the service of the content.
- *
- * @param context the context to use
- * @param spannableDescription the SpannableStringBuilder with the text of the
- * content description
- * @param relatedInfo used to search for the term in the correct service
- */
- private static void addClickListenersOnHashtags(final Context context,
- @NonNull final SpannableStringBuilder
- spannableDescription,
- final Info relatedInfo) {
- final String descriptionText = spannableDescription.toString();
- final Matcher hashtagsMatches = HASHTAGS_PATTERN.matcher(descriptionText);
-
- while (hashtagsMatches.find()) {
- final int hashtagStart = hashtagsMatches.start(1);
- final int hashtagEnd = hashtagsMatches.end(1);
- final String parsedHashtag = descriptionText.substring(hashtagStart, hashtagEnd);
-
- // don't add a ClickableSpan if there is already one, which should be a part of an URL,
- // already parsed before
- if (spannableDescription.getSpans(hashtagStart, hashtagEnd,
- ClickableSpan.class).length == 0) {
- spannableDescription.setSpan(new ClickableSpan() {
- @Override
- public void onClick(@NonNull final View view) {
- NavigationHelper.openSearch(context, relatedInfo.getServiceId(),
- parsedHashtag);
- }
- }, hashtagStart, hashtagEnd, 0);
- }
- }
- }
-
- /**
- * Add click listeners which opens the popup player on timestamps in a plain text.
- *
- * This method finds all timestamps in the {@link SpannableStringBuilder} of the description
- * using a regular expression, adds for each a {@link ClickableSpan} which opens the popup
- * player at the time indicated in the timestamps.
- *
- * @param context the context to use
- * @param spannableDescription the SpannableStringBuilder with the text of the
- * content description
- * @param relatedInfo 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(final Context context,
- @NonNull final SpannableStringBuilder
- spannableDescription,
- final Info relatedInfo,
- final CompositeDisposable disposables) {
- final String descriptionText = spannableDescription.toString();
- final Matcher timestampsMatches =
- TimestampExtractor.TIMESTAMPS_PATTERN.matcher(descriptionText);
-
- while (timestampsMatches.find()) {
- final TimestampExtractor.TimestampMatchDTO timestampMatchDTO =
- TimestampExtractor.getTimestampFromMatcher(
- timestampsMatches,
- descriptionText);
-
- if (timestampMatchDTO == null) {
- continue;
- }
-
- spannableDescription.setSpan(
- new ClickableSpan() {
- @Override
- public void onClick(@NonNull final View view) {
- playOnPopup(
- context,
- relatedInfo.getUrl(),
- relatedInfo.getService(),
- timestampMatchDTO.seconds(),
- disposables);
- }
- },
- timestampMatchDTO.timestampStart(),
- timestampMatchDTO.timestampEnd(),
- 0);
- }
- }
-
- /**
- * Change links generated by libraries in the description of a content to a custom link action
- * and add click listeners on timestamps in this description.
- *
- * Instead of using an {@link android.content.Intent#ACTION_VIEW} intent in the description of
- * a content, this method will parse the {@link CharSequence} and replace all current web links
- * with {@link ShareUtils#openUrlInBrowser(Context, String, boolean)}.
- * 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, Info,
- * 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.
- *
- * This method is required in order to intercept links and e.g. show a confirmation dialog
- * before opening a web link.
- *
- * @param textView the TextView in which the converted CharSequence will be applied
- * @param chars the 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
- */
- private static void changeIntentsOfDescriptionLinks(final TextView textView,
- final CharSequence chars,
- @Nullable final Info relatedInfo,
- final CompositeDisposable disposables) {
- disposables.add(Single.fromCallable(() -> {
- final Context context = textView.getContext();
-
- // add custom click actions on web links
- final SpannableStringBuilder textBlockLinked = new SpannableStringBuilder(chars);
- final URLSpan[] urls = textBlockLinked.getSpans(0, chars.length(), URLSpan.class);
-
- for (final URLSpan span : urls) {
- final String url = span.getURL();
- final ClickableSpan clickableSpan = new ClickableSpan() {
- public void onClick(@NonNull final View view) {
- if (!InternalUrlsHandler.handleUrlDescriptionTimestamp(
- new CompositeDisposable(), context, url)) {
- ShareUtils.openUrlInBrowser(context, url, false);
- }
- }
- };
-
- textBlockLinked.setSpan(clickableSpan, textBlockLinked.getSpanStart(span),
- textBlockLinked.getSpanEnd(span), textBlockLinked.getSpanFlags(span));
- textBlockLinked.removeSpan(span);
- }
-
- // add click actions on plain text timestamps only for description of contents,
- // unneeded for meta-info or other TextViews
- if (relatedInfo != null) {
- if (relatedInfo instanceof StreamInfo) {
- addClickListenersOnTimestamps(context, textBlockLinked, relatedInfo,
- disposables);
- }
- addClickListenersOnHashtags(context, textBlockLinked, relatedInfo);
- }
-
- return textBlockLinked;
- }).subscribeOn(Schedulers.computation())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(
- textBlockLinked -> setTextViewCharSequence(textView, textBlockLinked),
- throwable -> {
- Log.e(TAG, "Unable to linkify text", throwable);
- // this should never happen, but if it does, just fallback to it
- setTextViewCharSequence(textView, chars);
- }));
- }
-
- private static void setTextViewCharSequence(@NonNull final TextView textView,
- final CharSequence charSequence) {
- textView.setText(charSequence);
- textView.setMovementMethod(LinkMovementMethod.getInstance());
- textView.setVisibility(View.VISIBLE);
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/util/text/CommentTextOnTouchListener.java b/app/src/main/java/org/schabi/newpipe/util/text/CommentTextOnTouchListener.java
new file mode 100644
index 000000000..5018a6120
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/text/CommentTextOnTouchListener.java
@@ -0,0 +1,42 @@
+package org.schabi.newpipe.util.text;
+
+import static org.schabi.newpipe.util.text.TouchUtils.getOffsetForHorizontalLine;
+
+import android.annotation.SuppressLint;
+import android.text.Spanned;
+import android.text.style.ClickableSpan;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.TextView;
+
+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 CharSequence text = widget.getText();
+ if (text instanceof Spanned) {
+ 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[] links = buffer.getSpans(offset, offset, ClickableSpan.class);
+
+ if (links.length != 0) {
+ if (action == MotionEvent.ACTION_UP) {
+ links[0].onClick(widget);
+ }
+ // we handle events that intersect links, so return true
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/util/text/HashtagLongPressClickableSpan.java b/app/src/main/java/org/schabi/newpipe/util/text/HashtagLongPressClickableSpan.java
new file mode 100644
index 000000000..8a0363ecb
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/text/HashtagLongPressClickableSpan.java
@@ -0,0 +1,36 @@
+package org.schabi.newpipe.util.text;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+import org.schabi.newpipe.util.NavigationHelper;
+import org.schabi.newpipe.util.external_communication.ShareUtils;
+
+final class HashtagLongPressClickableSpan extends LongPressClickableSpan {
+
+ @NonNull
+ private final Context context;
+ @NonNull
+ private final String parsedHashtag;
+ private final int relatedInfoServiceId;
+
+ HashtagLongPressClickableSpan(@NonNull final Context context,
+ @NonNull final String parsedHashtag,
+ final int relatedInfoServiceId) {
+ this.context = context;
+ this.parsedHashtag = parsedHashtag;
+ this.relatedInfoServiceId = relatedInfoServiceId;
+ }
+
+ @Override
+ public void onClick(@NonNull final View view) {
+ NavigationHelper.openSearch(context, relatedInfoServiceId, parsedHashtag);
+ }
+
+ @Override
+ public void onLongClick(@NonNull final View view) {
+ ShareUtils.copyToClipboard(context, parsedHashtag);
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java b/app/src/main/java/org/schabi/newpipe/util/text/InternalUrlsHandler.java
similarity index 99%
rename from app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java
rename to app/src/main/java/org/schabi/newpipe/util/text/InternalUrlsHandler.java
index c46e6636d..b87618922 100644
--- a/app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java
+++ b/app/src/main/java/org/schabi/newpipe/util/text/InternalUrlsHandler.java
@@ -1,4 +1,4 @@
-package org.schabi.newpipe.util.external_communication;
+package org.schabi.newpipe.util.text;
import android.content.Context;
import android.util.Log;
diff --git a/app/src/main/java/org/schabi/newpipe/util/text/LongPressClickableSpan.java b/app/src/main/java/org/schabi/newpipe/util/text/LongPressClickableSpan.java
new file mode 100644
index 000000000..5c94a5850
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/text/LongPressClickableSpan.java
@@ -0,0 +1,12 @@
+package org.schabi.newpipe.util.text;
+
+import android.text.style.ClickableSpan;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+public abstract class LongPressClickableSpan extends ClickableSpan {
+
+ public abstract void onLongClick(@NonNull View view);
+
+}
diff --git a/app/src/main/java/org/schabi/newpipe/util/text/LongPressLinkMovementMethod.java b/app/src/main/java/org/schabi/newpipe/util/text/LongPressLinkMovementMethod.java
new file mode 100644
index 000000000..bd57621cb
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/text/LongPressLinkMovementMethod.java
@@ -0,0 +1,77 @@
+package org.schabi.newpipe.util.text;
+
+import static org.schabi.newpipe.util.text.TouchUtils.getOffsetForHorizontalLine;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.method.LinkMovementMethod;
+import android.text.method.MovementMethod;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+
+// Class adapted from https://stackoverflow.com/a/31786969
+
+public class LongPressLinkMovementMethod extends LinkMovementMethod {
+
+ private static final int LONG_PRESS_TIME = ViewConfiguration.getLongPressTimeout();
+
+ private static LongPressLinkMovementMethod instance;
+
+ private Handler longClickHandler;
+ private boolean isLongPressed = false;
+
+ @Override
+ public boolean onTouchEvent(@NonNull final TextView widget,
+ @NonNull final Spannable buffer,
+ @NonNull final MotionEvent event) {
+ final int action = event.getAction();
+
+ if (action == MotionEvent.ACTION_CANCEL && longClickHandler != null) {
+ longClickHandler.removeCallbacksAndMessages(null);
+ }
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
+ final int offset = getOffsetForHorizontalLine(widget, event);
+ final LongPressClickableSpan[] link = buffer.getSpans(offset, offset,
+ LongPressClickableSpan.class);
+
+ if (link.length != 0) {
+ if (action == MotionEvent.ACTION_UP) {
+ if (longClickHandler != null) {
+ longClickHandler.removeCallbacksAndMessages(null);
+ }
+ if (!isLongPressed) {
+ link[0].onClick(widget);
+ }
+ isLongPressed = false;
+ } else {
+ Selection.setSelection(buffer, buffer.getSpanStart(link[0]),
+ buffer.getSpanEnd(link[0]));
+ if (longClickHandler != null) {
+ longClickHandler.postDelayed(() -> {
+ link[0].onLongClick(widget);
+ isLongPressed = true;
+ }, LONG_PRESS_TIME);
+ }
+ }
+ return true;
+ }
+ }
+
+ return super.onTouchEvent(widget, buffer, event);
+ }
+
+ public static MovementMethod getInstance() {
+ if (instance == null) {
+ instance = new LongPressLinkMovementMethod();
+ instance.longClickHandler = new Handler(Looper.myLooper());
+ }
+
+ return instance;
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/util/text/TextLinkifier.java b/app/src/main/java/org/schabi/newpipe/util/text/TextLinkifier.java
new file mode 100644
index 000000000..e59a3dc05
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/text/TextLinkifier.java
@@ -0,0 +1,369 @@
+package org.schabi.newpipe.util.text;
+
+import android.content.Context;
+import android.text.SpannableStringBuilder;
+import android.text.style.URLSpan;
+import android.text.util.Linkify;
+import android.util.Log;
+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 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;
+
+import io.noties.markwon.Markwon;
+import io.noties.markwon.linkify.LinkifyPlugin;
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
+import io.reactivex.rxjava3.core.Single;
+import io.reactivex.rxjava3.disposables.CompositeDisposable;
+import io.reactivex.rxjava3.schedulers.Schedulers;
+
+public final class TextLinkifier {
+ public static final String TAG = TextLinkifier.class.getSimpleName();
+
+ // 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 SET_LINK_MOVEMENT_METHOD =
+ v -> v.setMovementMethod(LongPressLinkMovementMethod.getInstance());
+
+ private TextLinkifier() {
+ }
+
+ /**
+ * Create links for contents with an {@link Description} in the various possible formats.
+ *
+ * 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 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.
+ *
+ *
+ * This method will call {@link #changeLinkIntents(TextView, CharSequence, StreamingService,
+ * String, CompositeDisposable, Consumer)} after having linked the URLs with
+ * {@link HtmlCompat#fromHtml(String, int)}.
+ *
+ *
+ * @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 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 onCompletion) {
+ changeLinkIntents(
+ textView, HtmlCompat.fromHtml(htmlBlock, htmlCompatFlag), relatedInfoService,
+ relatedStreamUrl, disposables, onCompletion);
+ }
+
+ /**
+ * Create links for contents with a plain text description.
+ *
+ *
+ * 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)}.
+ *
+ *
+ * @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 fromPlainText(@NonNull final TextView textView,
+ @NonNull final String plainTextBlock,
+ @Nullable final StreamingService relatedInfoService,
+ @Nullable final String relatedStreamUrl,
+ @NonNull final CompositeDisposable disposables,
+ @Nullable final Consumer onCompletion) {
+ textView.setAutoLinkMask(Linkify.WEB_URLS);
+ textView.setText(plainTextBlock, TextView.BufferType.SPANNABLE);
+ changeLinkIntents(textView, textView.getText(), relatedInfoService,
+ relatedStreamUrl, disposables, onCompletion);
+ }
+
+ /**
+ * Create links for contents with a markdown description.
+ *
+ *
+ * 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)}.
+ *
+ *
+ * @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 fromMarkdown(@NonNull final TextView textView,
+ @NonNull final String markdownBlock,
+ @Nullable final StreamingService relatedInfoService,
+ @Nullable final String relatedStreamUrl,
+ @NonNull final CompositeDisposable disposables,
+ @Nullable final Consumer onCompletion) {
+ final Markwon markwon = Markwon.builder(textView.getContext())
+ .usePlugin(LinkifyPlugin.create()).build();
+ changeLinkIntents(textView, markwon.toMarkdown(markdownBlock),
+ relatedInfoService, relatedStreamUrl, disposables, onCompletion);
+ }
+
+ /**
+ * Change links generated by libraries in the description of a content to a custom link action
+ * and add click listeners on timestamps in this description.
+ *
+ *
+ * Instead of using an {@link android.content.Intent#ACTION_VIEW} intent in the description of
+ * a content, this method will parse the {@link CharSequence} and replace all current web links
+ * with {@link ShareUtils#openUrlInBrowser(Context, String, boolean)}.
+ *
+ *
+ *
+ * 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,
+ * 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.
+ *
+ *
+ *
+ * This method is required in order to intercept links and e.g. show a confirmation dialog
+ * before opening a web link.
+ *
+ *
+ * @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 changeLinkIntents(@NonNull final TextView textView,
+ @NonNull final CharSequence chars,
+ @Nullable final StreamingService relatedInfoService,
+ @Nullable final String relatedStreamUrl,
+ @NonNull final CompositeDisposable disposables,
+ @Nullable final Consumer onCompletion) {
+ disposables.add(Single.fromCallable(() -> {
+ final Context context = textView.getContext();
+
+ // add custom click actions on web links
+ final SpannableStringBuilder textBlockLinked =
+ new SpannableStringBuilder(chars);
+ final URLSpan[] urls = textBlockLinked.getSpans(0, chars.length(),
+ URLSpan.class);
+
+ for (final URLSpan span : urls) {
+ final String url = span.getURL();
+ final LongPressClickableSpan longPressClickableSpan =
+ new UrlLongPressClickableSpan(context, disposables, url);
+
+ textBlockLinked.setSpan(longPressClickableSpan,
+ textBlockLinked.getSpanStart(span),
+ textBlockLinked.getSpanEnd(span),
+ textBlockLinked.getSpanFlags(span));
+ textBlockLinked.removeSpan(span);
+ }
+
+ // 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,
+ relatedInfoService, relatedStreamUrl, disposables);
+ }
+ addClickListenersOnHashtags(context, textBlockLinked, relatedInfoService);
+ }
+
+ return textBlockLinked;
+ }).subscribeOn(Schedulers.computation())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ 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, onCompletion);
+ }));
+ }
+
+ /**
+ * Add click listeners which opens a search on hashtags in a plain text.
+ *
+ *
+ * This method finds all timestamps in the {@link SpannableStringBuilder} of the description
+ * using a regular expression, adds for each a {@link LongPressClickableSpan} which opens
+ * {@link NavigationHelper#openSearch(Context, int, String)} and makes a search on the hashtag,
+ * in the service of the content when pressed, and copy the hashtag to clipboard when
+ * long-pressed, if allowed by the caller method (parameter {@code addLongClickCopyListener}).
+ *
+ *
+ * @param context the {@link Context} to use
+ * @param spannableDescription the {@link SpannableStringBuilder} with the text of the
+ * content description
+ * @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 StreamingService relatedInfoService) {
+ final String descriptionText = spannableDescription.toString();
+ final Matcher hashtagsMatches = HASHTAGS_PATTERN.matcher(descriptionText);
+
+ while (hashtagsMatches.find()) {
+ final int hashtagStart = hashtagsMatches.start(1);
+ final int hashtagEnd = hashtagsMatches.end(1);
+ final String parsedHashtag = descriptionText.substring(hashtagStart, hashtagEnd);
+
+ // Don't add a LongPressClickableSpan if there is already one, which should be a part
+ // 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, serviceId),
+ hashtagStart, hashtagEnd, 0);
+ }
+ }
+ }
+
+ /**
+ * Add click listeners which opens the popup player on timestamps in a plain text.
+ *
+ *
+ * This method finds all timestamps in the {@link SpannableStringBuilder} of the description
+ * using a regular expression, adds for each a {@link LongPressClickableSpan} which opens the
+ * popup player at the time indicated in the timestamps and copy the timestamp in clipboard
+ * when long-pressed.
+ *
+ *
+ * @param context the {@link Context} to use
+ * @param spannableDescription the {@link SpannableStringBuilder} with the text of the
+ * content description
+ * @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 StreamingService relatedInfoService,
+ @NonNull final String relatedStreamUrl,
+ @NonNull final CompositeDisposable disposables) {
+ final String descriptionText = spannableDescription.toString();
+ final Matcher timestampsMatches = TimestampExtractor.TIMESTAMPS_PATTERN.matcher(
+ descriptionText);
+
+ while (timestampsMatches.find()) {
+ final TimestampExtractor.TimestampMatchDTO timestampMatchDTO =
+ TimestampExtractor.getTimestampFromMatcher(timestampsMatches, descriptionText);
+
+ if (timestampMatchDTO == null) {
+ continue;
+ }
+
+ spannableDescription.setSpan(
+ new TimestampLongPressClickableSpan(context, descriptionText, disposables,
+ relatedInfoService, relatedStreamUrl, timestampMatchDTO),
+ timestampMatchDTO.timestampStart(),
+ timestampMatchDTO.timestampEnd(),
+ 0);
+ }
+ }
+
+ private static void setTextViewCharSequence(@NonNull final TextView textView,
+ @Nullable final CharSequence charSequence,
+ @Nullable final Consumer onCompletion) {
+ textView.setText(charSequence);
+ textView.setVisibility(View.VISIBLE);
+ if (onCompletion != null) {
+ onCompletion.accept(textView);
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/TimestampExtractor.java b/app/src/main/java/org/schabi/newpipe/util/text/TimestampExtractor.java
similarity index 78%
rename from app/src/main/java/org/schabi/newpipe/util/external_communication/TimestampExtractor.java
rename to app/src/main/java/org/schabi/newpipe/util/text/TimestampExtractor.java
index a13c66402..be603f41a 100644
--- a/app/src/main/java/org/schabi/newpipe/util/external_communication/TimestampExtractor.java
+++ b/app/src/main/java/org/schabi/newpipe/util/text/TimestampExtractor.java
@@ -1,4 +1,7 @@
-package org.schabi.newpipe.util.external_communication;
+package org.schabi.newpipe.util.text;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -15,17 +18,18 @@ public final class TimestampExtractor {
}
/**
- * Get's a single timestamp from a matcher.
+ * Gets a single timestamp from a matcher.
*
- * @param timestampMatches The matcher which was created using {@link #TIMESTAMPS_PATTERN}
- * @param baseText The text where the pattern was applied to /
- * where the matcher is based upon
- * @return If a match occurred: a {@link TimestampMatchDTO} filled with information.
- * If not null
.
+ * @param timestampMatches the matcher which was created using {@link #TIMESTAMPS_PATTERN}
+ * @param baseText the text where the pattern was applied to / where the matcher is
+ * based upon
+ * @return if a match occurred, a {@link TimestampMatchDTO} filled with information, otherwise
+ * {@code null}.
*/
+ @Nullable
public static TimestampMatchDTO getTimestampFromMatcher(
- final Matcher timestampMatches,
- final String baseText) {
+ @NonNull final Matcher timestampMatches,
+ @NonNull final String baseText) {
int timestampStart = timestampMatches.start(1);
if (timestampStart == -1) {
timestampStart = timestampMatches.start(2);
diff --git a/app/src/main/java/org/schabi/newpipe/util/text/TimestampLongPressClickableSpan.java b/app/src/main/java/org/schabi/newpipe/util/text/TimestampLongPressClickableSpan.java
new file mode 100644
index 000000000..f5864794a
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/text/TimestampLongPressClickableSpan.java
@@ -0,0 +1,78 @@
+package org.schabi.newpipe.util.text;
+
+import static org.schabi.newpipe.util.text.InternalUrlsHandler.playOnPopup;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+import org.schabi.newpipe.extractor.ServiceList;
+import org.schabi.newpipe.extractor.StreamingService;
+import org.schabi.newpipe.util.external_communication.ShareUtils;
+
+import io.reactivex.rxjava3.disposables.CompositeDisposable;
+
+final class TimestampLongPressClickableSpan extends LongPressClickableSpan {
+
+ @NonNull
+ private final Context context;
+ @NonNull
+ private final String descriptionText;
+ @NonNull
+ private final CompositeDisposable disposables;
+ @NonNull
+ private final StreamingService relatedInfoService;
+ @NonNull
+ private final String relatedStreamUrl;
+ @NonNull
+ private final TimestampExtractor.TimestampMatchDTO timestampMatchDTO;
+
+ TimestampLongPressClickableSpan(
+ @NonNull final Context context,
+ @NonNull final String descriptionText,
+ @NonNull final CompositeDisposable disposables,
+ @NonNull final StreamingService relatedInfoService,
+ @NonNull final String relatedStreamUrl,
+ @NonNull final TimestampExtractor.TimestampMatchDTO timestampMatchDTO) {
+ this.context = context;
+ this.descriptionText = descriptionText;
+ this.disposables = disposables;
+ this.relatedInfoService = relatedInfoService;
+ this.relatedStreamUrl = relatedStreamUrl;
+ this.timestampMatchDTO = timestampMatchDTO;
+ }
+
+ @Override
+ public void onClick(@NonNull final View view) {
+ playOnPopup(context, relatedStreamUrl, relatedInfoService,
+ timestampMatchDTO.seconds(), disposables);
+ }
+
+ @Override
+ public void onLongClick(@NonNull final View view) {
+ ShareUtils.copyToClipboard(context, getTimestampTextToCopy(
+ relatedInfoService, relatedStreamUrl, descriptionText, timestampMatchDTO));
+ }
+
+ @NonNull
+ private static String getTimestampTextToCopy(
+ @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
+ 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
+ return descriptionText.subSequence(timestampMatchDTO.timestampStart(),
+ timestampMatchDTO.timestampEnd()).toString();
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/util/text/TouchUtils.java b/app/src/main/java/org/schabi/newpipe/util/text/TouchUtils.java
new file mode 100644
index 000000000..5c0db20a3
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/text/TouchUtils.java
@@ -0,0 +1,38 @@
+package org.schabi.newpipe.util.text;
+
+import android.text.Layout;
+import android.view.MotionEvent;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+
+public final class TouchUtils {
+
+ private TouchUtils() {
+ }
+
+ /**
+ * Get the character offset on the closest line to the position pressed by the user of a
+ * {@link TextView} from a {@link MotionEvent} which was fired on this {@link TextView}.
+ *
+ * @param textView the {@link TextView} on which the {@link MotionEvent} was fired
+ * @param event the {@link MotionEvent} which was fired
+ * @return the character offset on the closest line to the position pressed by the user
+ */
+ public static int getOffsetForHorizontalLine(@NonNull final TextView textView,
+ @NonNull final MotionEvent event) {
+
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ x -= textView.getTotalPaddingLeft();
+ y -= textView.getTotalPaddingTop();
+
+ x += textView.getScrollX();
+ y += textView.getScrollY();
+
+ final Layout layout = textView.getLayout();
+ final int line = layout.getLineForVertical(y);
+ return layout.getOffsetForHorizontal(line, x);
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/util/text/UrlLongPressClickableSpan.java b/app/src/main/java/org/schabi/newpipe/util/text/UrlLongPressClickableSpan.java
new file mode 100644
index 000000000..eb0d7425e
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/text/UrlLongPressClickableSpan.java
@@ -0,0 +1,41 @@
+package org.schabi.newpipe.util.text;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+import org.schabi.newpipe.util.external_communication.ShareUtils;
+
+import io.reactivex.rxjava3.disposables.CompositeDisposable;
+
+final class UrlLongPressClickableSpan extends LongPressClickableSpan {
+
+ @NonNull
+ private final Context context;
+ @NonNull
+ private final CompositeDisposable disposables;
+ @NonNull
+ private final String url;
+
+ UrlLongPressClickableSpan(@NonNull final Context context,
+ @NonNull final CompositeDisposable disposables,
+ @NonNull final String url) {
+ this.context = context;
+ this.disposables = disposables;
+ this.url = url;
+ }
+
+ @Override
+ public void onClick(@NonNull final View view) {
+ if (!InternalUrlsHandler.handleUrlDescriptionTimestamp(
+ disposables, context, url)) {
+ ShareUtils.openUrlInBrowser(context, url, false);
+ }
+ }
+
+ @Override
+ public void onLongClick(@NonNull final View view) {
+ ShareUtils.copyToClipboard(context, url);
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/views/NewPipeEditText.java b/app/src/main/java/org/schabi/newpipe/views/NewPipeEditText.java
index 2adc28d0e..f0993055e 100644
--- a/app/src/main/java/org/schabi/newpipe/views/NewPipeEditText.java
+++ b/app/src/main/java/org/schabi/newpipe/views/NewPipeEditText.java
@@ -13,9 +13,10 @@ import org.schabi.newpipe.util.external_communication.ShareUtils;
/**
* An {@link AppCompatEditText} which uses {@link ShareUtils#shareText(Context, String, String)}
* when sharing selected text by using the {@code Share} command of the floating actions.
+ *
*
- * This allows NewPipe to show Android share sheet instead of EMUI share sheet when sharing text
- * from {@link AppCompatEditText} on EMUI devices.
+ * This class allows NewPipe to show Android share sheet instead of EMUI share sheet when sharing
+ * text from {@link AppCompatEditText} on EMUI devices.
*
*/
public class NewPipeEditText extends AppCompatEditText {
diff --git a/app/src/main/java/org/schabi/newpipe/views/NewPipeTextView.java b/app/src/main/java/org/schabi/newpipe/views/NewPipeTextView.java
index 8fdac32db..dd3f20f40 100644
--- a/app/src/main/java/org/schabi/newpipe/views/NewPipeTextView.java
+++ b/app/src/main/java/org/schabi/newpipe/views/NewPipeTextView.java
@@ -1,6 +1,7 @@
package org.schabi.newpipe.views;
import android.content.Context;
+import android.text.method.MovementMethod;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
@@ -13,9 +14,11 @@ import org.schabi.newpipe.util.external_communication.ShareUtils;
/**
* An {@link AppCompatTextView} which uses {@link ShareUtils#shareText(Context, String, String)}
* when sharing selected text by using the {@code Share} command of the floating actions.
+ *
*
- * This allows NewPipe to show Android share sheet instead of EMUI share sheet when sharing text
- * from {@link AppCompatTextView} on EMUI devices.
+ * This class allows NewPipe to show Android share sheet instead of EMUI share sheet when sharing
+ * text from {@link AppCompatTextView} on EMUI devices and also to keep movement method set when a
+ * text change occurs, if the text cannot be selected and text links are clickable.
*
*/
public class NewPipeTextView extends AppCompatTextView {
@@ -34,6 +37,16 @@ public class NewPipeTextView extends AppCompatTextView {
super(context, attrs, defStyleAttr);
}
+ @Override
+ public void setText(final CharSequence text, final BufferType type) {
+ // We need to set again the movement method after a text change because Android resets the
+ // movement method to the default one in the case where the text cannot be selected and
+ // text links are clickable (which is the default case in NewPipe).
+ final MovementMethod movementMethod = this.getMovementMethod();
+ super.setText(text, type);
+ setMovementMethod(movementMethod);
+ }
+
@Override
public boolean onTextContextMenuItem(final int id) {
if (id == android.R.id.shareText) {
diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java
index 8b8a6ff09..edc6bb6fd 100755
--- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java
+++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java
@@ -1,5 +1,8 @@
package us.shandian.giga.service;
+import static org.schabi.newpipe.BuildConfig.APPLICATION_ID;
+import static org.schabi.newpipe.BuildConfig.DEBUG;
+
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
@@ -22,12 +25,12 @@ import android.os.IBinder;
import android.os.Message;
import android.os.Parcelable;
import android.util.Log;
-import android.util.SparseArray;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
+import androidx.collection.SparseArrayCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationCompat.Builder;
import androidx.core.app.ServiceCompat;
@@ -37,23 +40,21 @@ import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.download.DownloadActivity;
import org.schabi.newpipe.player.helper.LockManager;
+import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
+import org.schabi.newpipe.streams.io.StoredFileHelper;
+import org.schabi.newpipe.util.Localization;
+import org.schabi.newpipe.util.PendingIntentCompat;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.List;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.MissionRecoveryInfo;
-import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
-import org.schabi.newpipe.streams.io.StoredFileHelper;
-import org.schabi.newpipe.util.Localization;
-
import us.shandian.giga.postprocessing.Postprocessing;
import us.shandian.giga.service.DownloadManager.NetworkState;
-import static org.schabi.newpipe.BuildConfig.APPLICATION_ID;
-import static org.schabi.newpipe.BuildConfig.DEBUG;
-
public class DownloadManagerService extends Service {
private static final String TAG = "DownloadManagerService";
@@ -94,7 +95,7 @@ public class DownloadManagerService extends Service {
private Builder downloadDoneNotification = null;
private StringBuilder downloadDoneList = null;
- private final ArrayList mEchoObservers = new ArrayList<>(1);
+ private final List mEchoObservers = new ArrayList<>(1);
private ConnectivityManager mConnectivityManager;
private ConnectivityManager.NetworkCallback mNetworkStateListenerL = null;
@@ -107,7 +108,8 @@ public class DownloadManagerService extends Service {
private int downloadFailedNotificationID = DOWNLOADS_NOTIFICATION_ID + 1;
private Builder downloadFailedNotification = null;
- private final SparseArray mFailedDownloads = new SparseArray<>(5);
+ private final SparseArrayCompat mFailedDownloads =
+ new SparseArrayCompat<>(5);
private Bitmap icLauncher;
private Bitmap icDownloadDone;
@@ -142,7 +144,7 @@ public class DownloadManagerService extends Service {
Intent openDownloadListIntent = new Intent(this, DownloadActivity.class)
.setAction(Intent.ACTION_MAIN);
- mOpenDownloadList = PendingIntent.getActivity(this, 0,
+ mOpenDownloadList = PendingIntentCompat.getActivity(this, 0,
openDownloadListIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
@@ -276,7 +278,7 @@ public class DownloadManagerService extends Service {
}
if (msg.what != MESSAGE_ERROR)
- mFailedDownloads.delete(mFailedDownloads.indexOfValue(mission));
+ mFailedDownloads.remove(mFailedDownloads.indexOfValue(mission));
for (Callback observer : mEchoObservers)
observer.handleMessage(msg);
@@ -308,7 +310,7 @@ public class DownloadManagerService extends Service {
}
private void handlePreferenceChange(SharedPreferences prefs, @NonNull String key) {
- if (key.equals(getString(R.string.downloads_maximum_retry))) {
+ if (getString(R.string.downloads_maximum_retry).equals(key)) {
try {
String value = prefs.getString(key, getString(R.string.downloads_maximum_retry_default));
mManager.mPrefMaxRetry = value == null ? 0 : Integer.parseInt(value);
@@ -316,13 +318,13 @@ public class DownloadManagerService extends Service {
mManager.mPrefMaxRetry = 0;
}
mManager.updateMaximumAttempts();
- } else if (key.equals(getString(R.string.downloads_cross_network))) {
+ } else if (getString(R.string.downloads_cross_network).equals(key)) {
mManager.mPrefMeteredDownloads = prefs.getBoolean(key, false);
- } else if (key.equals(getString(R.string.downloads_queue_limit))) {
+ } else if (getString(R.string.downloads_queue_limit).equals(key)) {
mManager.mPrefQueueLimit = prefs.getBoolean(key, true);
- } else if (key.equals(getString(R.string.download_path_video_key))) {
+ } else if (getString(R.string.download_path_video_key).equals(key)) {
mManager.mMainStorageVideo = loadMainVideoStorage();
- } else if (key.equals(getString(R.string.download_path_audio_key))) {
+ } else if (getString(R.string.download_path_audio_key).equals(key)) {
mManager.mMainStorageAudio = loadMainAudioStorage();
}
}
@@ -460,7 +462,7 @@ public class DownloadManagerService extends Service {
}
public void notifyFailedDownload(DownloadMission mission) {
- if (!mDownloadNotificationEnable || mFailedDownloads.indexOfValue(mission) >= 0) return;
+ if (!mDownloadNotificationEnable || mFailedDownloads.containsValue(mission)) return;
int id = downloadFailedNotificationID++;
mFailedDownloads.put(id, mission);
@@ -484,7 +486,8 @@ public class DownloadManagerService extends Service {
private PendingIntent makePendingIntent(String action) {
Intent intent = new Intent(this, DownloadManagerService.class).setAction(action);
- return PendingIntent.getService(this, intent.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ return PendingIntentCompat.getService(this, intent.hashCode(), intent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
}
private void manageLock(boolean acquire) {
diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
index 808928370..bfb6a15e2 100644
--- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
+++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
@@ -48,6 +48,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
+import androidx.core.os.HandlerCompat;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.Adapter;
@@ -91,6 +92,10 @@ public class MissionAdapter extends Adapter implements Handler.Callb
private static final String UNDEFINED_PROGRESS = "--.-%";
private static final String DEFAULT_MIME_TYPE = "*/*";
private static final String UNDEFINED_ETA = "--:--";
+
+ private static final String UPDATER = "updater";
+ private static final String DELETE = "deleteFinishedDownloads";
+
private static final int HASH_NOTIFICATION_ID = 123790;
private final Context mContext;
@@ -110,9 +115,6 @@ public class MissionAdapter extends Adapter implements Handler.Callb
private final ArrayList mHidden;
private Snackbar mSnackbar;
- private final Runnable rUpdater = this::updater;
- private final Runnable rDelete = this::deleteFinishedDownloads;
-
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage, View root) {
@@ -595,12 +597,12 @@ public class MissionAdapter extends Adapter implements Handler.Callb
i.remove();
}
applyChanges();
- mHandler.removeCallbacks(rDelete);
+ mHandler.removeCallbacksAndMessages(DELETE);
});
mSnackbar.setActionTextColor(Color.YELLOW);
mSnackbar.show();
- mHandler.postDelayed(rDelete, 5000);
+ HandlerCompat.postDelayed(mHandler, this::deleteFinishedDownloads, DELETE, 5000);
} else if (!delete) {
mDownloadManager.forgetFinishedDownloads();
applyChanges();
@@ -693,7 +695,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
- Utility.copyToClipboard(mContext, result);
+ ShareUtils.copyToClipboard(mContext, result);
notificationManager.cancel(HASH_NOTIFICATION_ID);
})
);
@@ -786,15 +788,14 @@ public class MissionAdapter extends Adapter implements Handler.Callb
public void onResume() {
mDeleter.resume();
- mHandler.post(rUpdater);
+ HandlerCompat.postDelayed(mHandler, this::updater, UPDATER, 0);
}
public void onPaused() {
mDeleter.pause();
- mHandler.removeCallbacks(rUpdater);
+ mHandler.removeCallbacksAndMessages(UPDATER);
}
-
public void recoverMission(DownloadMission mission) {
ViewHolderItem h = getViewHolder(mission);
if (h == null) return;
@@ -817,7 +818,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb
updateProgress(h);
}
- mHandler.postDelayed(rUpdater, 1000);
+ HandlerCompat.postDelayed(mHandler, this::updater, UPDATER, 1000);
}
private boolean isNotFinite(double value) {
diff --git a/app/src/main/java/us/shandian/giga/ui/common/Deleter.java b/app/src/main/java/us/shandian/giga/ui/common/Deleter.java
index c554766ff..1902076d6 100644
--- a/app/src/main/java/us/shandian/giga/ui/common/Deleter.java
+++ b/app/src/main/java/us/shandian/giga/ui/common/Deleter.java
@@ -6,6 +6,8 @@ import android.graphics.Color;
import android.os.Handler;
import android.view.View;
+import androidx.core.os.HandlerCompat;
+
import com.google.android.material.snackbar.Snackbar;
import org.schabi.newpipe.R;
@@ -19,6 +21,10 @@ import us.shandian.giga.service.DownloadManager.MissionIterator;
import us.shandian.giga.ui.adapter.MissionAdapter;
public class Deleter {
+ private static final String COMMIT = "commit";
+ private static final String NEXT = "next";
+ private static final String SHOW = "show";
+
private static final int TIMEOUT = 5000;// ms
private static final int DELAY = 350;// ms
private static final int DELAY_RESUME = 400;// ms
@@ -34,10 +40,6 @@ public class Deleter {
private final Handler mHandler;
private final View mView;
- private final Runnable rShow;
- private final Runnable rNext;
- private final Runnable rCommit;
-
public Deleter(View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) {
mView = v;
mContext = c;
@@ -46,21 +48,15 @@ public class Deleter {
mIterator = i;
mHandler = h;
- // use variables to know the reference of the lambdas
- rShow = this::show;
- rNext = this::next;
- rCommit = this::commit;
-
items = new ArrayList<>(2);
}
public void append(Mission item) {
-
/* If a mission is removed from the list while the Snackbar for a previously
* removed item is still showing, commit the action for the previous item
* immediately. This prevents Snackbars from stacking up in reverse order.
*/
- mHandler.removeCallbacks(rCommit);
+ mHandler.removeCallbacksAndMessages(COMMIT);
commit();
mIterator.hide(item);
@@ -82,7 +78,7 @@ public class Deleter {
pause();
running = true;
- mHandler.postDelayed(rNext, DELAY);
+ HandlerCompat.postDelayed(mHandler, this::next, NEXT, DELAY);
}
private void next() {
@@ -95,7 +91,7 @@ public class Deleter {
snackbar.setActionTextColor(Color.YELLOW);
snackbar.show();
- mHandler.postDelayed(rCommit, TIMEOUT);
+ HandlerCompat.postDelayed(mHandler, this::commit, COMMIT, TIMEOUT);
}
private void commit() {
@@ -124,15 +120,16 @@ public class Deleter {
public void pause() {
running = false;
- mHandler.removeCallbacks(rNext);
- mHandler.removeCallbacks(rShow);
- mHandler.removeCallbacks(rCommit);
+ mHandler.removeCallbacksAndMessages(NEXT);
+ mHandler.removeCallbacksAndMessages(SHOW);
+ mHandler.removeCallbacksAndMessages(COMMIT);
if (snackbar != null) snackbar.dismiss();
}
public void resume() {
- if (running) return;
- mHandler.postDelayed(rShow, DELAY_RESUME);
+ if (!running) {
+ HandlerCompat.postDelayed(mHandler, this::show, SHOW, DELAY_RESUME);
+ }
}
public void dispose() {
diff --git a/app/src/main/java/us/shandian/giga/util/Utility.java b/app/src/main/java/us/shandian/giga/util/Utility.java
index 4cd424ab9..ecce6639e 100644
--- a/app/src/main/java/us/shandian/giga/util/Utility.java
+++ b/app/src/main/java/us/shandian/giga/util/Utility.java
@@ -192,18 +192,6 @@ public class Utility {
}
}
- public static void copyToClipboard(Context context, String str) {
- ClipboardManager cm = ContextCompat.getSystemService(context, ClipboardManager.class);
-
- if (cm == null) {
- Toast.makeText(context, R.string.permission_denied, Toast.LENGTH_LONG).show();
- return;
- }
-
- cm.setPrimaryClip(ClipData.newPlainText("text", str));
- Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show();
- }
-
public static String checksum(final StoredFileHelper source, final int algorithmId)
throws IOException {
ByteString byteString;
@@ -248,10 +236,10 @@ public class Utility {
return number < 10 ? ("0" + number) : String.valueOf(number);
}
- public static String stringifySeconds(double seconds) {
- int h = (int) Math.floor(seconds / 3600);
- int m = (int) Math.floor((seconds - (h * 3600)) / 60);
- int s = (int) (seconds - (h * 3600) - (m * 60));
+ public static String stringifySeconds(final long seconds) {
+ final int h = (int) Math.floorDiv(seconds, 3600);
+ final int m = (int) Math.floorDiv(seconds - (h * 3600L), 60);
+ final int s = (int) (seconds - (h * 3600) - (m * 60));
String str = "";
diff --git a/app/src/main/res/drawable/ic_format_list_numbered.xml b/app/src/main/res/drawable/ic_format_list_numbered.xml
deleted file mode 100644
index b11666c56..000000000
--- a/app/src/main/res/drawable/ic_format_list_numbered.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_menu_book.xml b/app/src/main/res/drawable/ic_menu_book.xml
new file mode 100644
index 000000000..4cd4fb3a4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_menu_book.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_subscriptions.xml b/app/src/main/res/drawable/ic_subscriptions.xml
new file mode 100644
index 000000000..f2ac7bec2
--- /dev/null
+++ b/app/src/main/res/drawable/ic_subscriptions.xml
@@ -0,0 +1,10 @@
+
+
+
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 a29fa8c78..9fdede902 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
@@ -319,9 +319,7 @@
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:singleLine="true"
- android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/video_item_detail_uploader_text_size"
- android:textStyle="bold"
tools:ignore="RtlHardcoded"
tools:text="Uploader" />
@@ -603,19 +601,19 @@
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
- app:layout_behavior="@string/appbar_scrolling_view_behavior"
- android:paddingBottom="48dp"/>
+ android:paddingBottom="48dp"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior" />
+ app:tabIconTint="?attr/colorAccent"
+ app:tabIndicatorGravity="top" />
@@ -627,6 +625,7 @@
android:layout_weight="3" />
+
@@ -713,22 +712,22 @@
android:layout_width="40dp"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackground"
- android:padding="10dp"
- android:scaleType="center"
+ android:contentDescription="@string/pause"
android:focusable="true"
android:focusedByDefault="true"
- android:src="@drawable/ic_play_arrow"
- tools:ignore="ContentDescription,RtlHardcoded" />
+ android:scaleType="center"
+ android:src="@drawable/ic_play_arrow" />
+ tools:ignore="RtlSymmetry" />
diff --git a/app/src/main/res/layout/fragment_video_detail.xml b/app/src/main/res/layout/fragment_video_detail.xml
index be3504878..438c618ef 100644
--- a/app/src/main/res/layout/fragment_video_detail.xml
+++ b/app/src/main/res/layout/fragment_video_detail.xml
@@ -41,9 +41,9 @@
android:id="@+id/detail_thumbnail_image_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:minHeight="200dp"
android:background="?windowBackground"
android:contentDescription="@string/detail_thumbnail_view_description"
+ android:minHeight="200dp"
android:scaleType="fitCenter"
tools:ignore="RtlHardcoded"
tools:layout_height="200dp"
@@ -306,9 +306,7 @@
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:singleLine="true"
- android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/video_item_detail_uploader_text_size"
- android:textStyle="bold"
tools:ignore="RtlHardcoded"
tools:text="Uploader" />
@@ -586,19 +584,19 @@
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
- app:layout_behavior="@string/appbar_scrolling_view_behavior"
- android:paddingBottom="48dp"/>
+ android:paddingBottom="48dp"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior" />
+ app:tabIconTint="?attr/colorAccent"
+ app:tabIndicatorGravity="top" />
@@ -668,8 +666,6 @@
android:layout_height="60dp"
android:layout_alignParentEnd="true"
android:gravity="center_vertical"
- android:paddingLeft="@dimen/video_item_search_padding"
- android:paddingRight="@dimen/video_item_search_padding"
android:theme="@style/ContrastTintTheme"
tools:ignore="RtlHardcoded">
@@ -678,7 +674,7 @@
android:layout_width="40dp"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackground"
- android:padding="10dp"
+ android:contentDescription="@string/title_activity_play_queue"
android:scaleType="center"
android:src="@drawable/ic_list"
tools:ignore="ContentDescription,RtlHardcoded" />
@@ -688,22 +684,22 @@
android:layout_width="40dp"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackground"
- android:padding="10dp"
- android:scaleType="center"
+ android:contentDescription="@string/pause"
android:focusable="true"
android:focusedByDefault="true"
- android:src="@drawable/ic_play_arrow"
- tools:ignore="ContentDescription,RtlHardcoded" />
+ android:scaleType="center"
+ android:src="@drawable/ic_play_arrow" />
+ tools:ignore="RtlSymmetry" />
diff --git a/app/src/main/res/layout/item_metadata.xml b/app/src/main/res/layout/item_metadata.xml
index 31dedd880..251b9e832 100644
--- a/app/src/main/res/layout/item_metadata.xml
+++ b/app/src/main/res/layout/item_metadata.xml
@@ -6,7 +6,7 @@
android:layout_height="wrap_content"
android:paddingVertical="6dp">
-
-
+ tools:text="10M subscribers • 100 videos" />
+
+
diff --git a/app/src/main/res/layout/list_empty_view.xml b/app/src/main/res/layout/list_empty_view.xml
index 21b3b436f..a25042aed 100644
--- a/app/src/main/res/layout/list_empty_view.xml
+++ b/app/src/main/res/layout/list_empty_view.xml
@@ -19,5 +19,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="6dp"
- android:text="@string/empty_subscription_feed_subtitle" />
+ android:gravity="center"
+ android:paddingHorizontal="16dp"
+ android:text="@string/empty_list_subtitle" />
diff --git a/app/src/main/res/layout/list_empty_view_subscriptions.xml b/app/src/main/res/layout/list_empty_view_subscriptions.xml
new file mode 100644
index 000000000..74a5eced4
--- /dev/null
+++ b/app/src/main/res/layout/list_empty_view_subscriptions.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/list_playlist_card_item.xml b/app/src/main/res/layout/list_playlist_card_item.xml
new file mode 100644
index 000000000..c7dd4f17c
--- /dev/null
+++ b/app/src/main/res/layout/list_playlist_card_item.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/list_stream_card_item.xml b/app/src/main/res/layout/list_stream_card_item.xml
new file mode 100644
index 000000000..968dca082
--- /dev/null
+++ b/app/src/main/res/layout/list_stream_card_item.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/list_stream_playlist_card_item.xml b/app/src/main/res/layout/list_stream_playlist_card_item.xml
new file mode 100644
index 000000000..9cc6b326c
--- /dev/null
+++ b/app/src/main/res/layout/list_stream_playlist_card_item.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/player.xml b/app/src/main/res/layout/player.xml
index 60cbcf7c4..b528e4e9b 100644
--- a/app/src/main/res/layout/player.xml
+++ b/app/src/main/res/layout/player.xml
@@ -212,11 +212,11 @@
android:clickable="true"
android:focusable="true"
android:paddingStart="6dp"
- android:paddingTop="5dp"
android:paddingEnd="6dp"
android:paddingBottom="3dp"
+ android:paddingTop="3dp"
android:scaleType="fitCenter"
- android:src="@drawable/ic_format_list_numbered"
+ android:src="@drawable/ic_menu_book"
android:visibility="gone"
app:tint="@color/white"
tools:ignore="ContentDescription,RtlHardcoded" />
diff --git a/app/src/main/res/layout/playlist_control.xml b/app/src/main/res/layout/playlist_control.xml
index 5a8856128..685082013 100644
--- a/app/src/main/res/layout/playlist_control.xml
+++ b/app/src/main/res/layout/playlist_control.xml
@@ -48,6 +48,8 @@
diff --git a/app/src/main/res/menu/menu_play_queue_item.xml b/app/src/main/res/menu/menu_play_queue_item.xml
index b23f8008f..00c63e1e0 100644
--- a/app/src/main/res/menu/menu_play_queue_item.xml
+++ b/app/src/main/res/menu/menu_play_queue_item.xml
@@ -16,4 +16,7 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index bd0658215..02c471d63 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -23,7 +23,7 @@
فاتح
خطأ في الشبكة
لم يتم العثور على مشغل بث. تثبيت VLC؟
- فتح في المتصفح
+ فتح في متصفح الويب
الصوت
تشغيل بواسطة كودي
البحث
@@ -114,7 +114,7 @@
تعليقك (باللغة الإنجليزية):
التفاصيل:
لم يتم العثور على نتائج
- لا شيء هنا سوى الصراصير
+ لا شيء هنا سوى الصراصير
الصوت
إعادة المحاولة
ألف
@@ -370,8 +370,7 @@
القائمة
الشبكة
تلقائي
- تحديث NewPipe متاح!
- اضغط لتنزيل
+ تحديث NewPipe متاح!
انتهى
ريثما
متوقف
@@ -549,7 +548,6 @@
نظرا لقيود مشغل ExoPlayer مدة التقديم تم ضبطها الى %d ثانية
إلغاء كتم الصوت
كتم الصوت
- مساعدة
هذا المحتوى ليس مدعومًا من قبل NewPipe.
\n
\nنأمل أن يكون مدعومًا في التحديثات القادمة.
@@ -776,4 +774,14 @@
إذا كنت تواجه مشكلة في استخدام التطبيق ، فتأكد من مراجعة هذه الإجابات للأسئلة الشائعة!
مشاهدة على الموقع
فرز
+ أنت تقوم بتشغيل أحدث إصدار من NewPipe
+ انقر للتنزيل %s
+ الوضع السريع
+ استيراد الاشتراكات أو تصديرها من القائمة المكونة من 3 نقاط
+ هذا الخيار متاح فقط إذا تم تحديد %s للسمة
+ إلغاء تعيين الصورة المصغرة الدائمة
+ فشل النسخ إلى الحافظة
+ البطاقة
+ تمت إضافة وقت (أوقات) مكررة %d
+ تحتوي قوائم التشغيل رمادية اللون بالفعل على هذا العنصر.
\ No newline at end of file
diff --git a/app/src/main/res/values-as/strings.xml b/app/src/main/res/values-as/strings.xml
new file mode 100644
index 000000000..be845845a
--- /dev/null
+++ b/app/src/main/res/values-as/strings.xml
@@ -0,0 +1,101 @@
+
+
+ কোনো ষ্ট্ৰিম প্লেয়াৰ পোৱা নগ\'ল (আপুনি ইয়াক বজাবলৈ VLC ইনষ্টল কৰিব পাৰে)।
+ ইনষ্টল
+ বাতিল কৰক
+ ঠিক আছে
+ ব্ৰাউজাৰত খোলক
+ POPUP অৱস্থাত খোলক
+ ...ৰ সৈতে খোলক
+ চেয়াৰ
+ %1$s ত প্ৰকাশ কৰা হৈছে
+ কোনো ষ্ট্ৰিম প্লেয়াৰ পোৱা নগ\'ল। VLC ইনষ্টল কৰক\?
+ ডাউনল’ড
+ ষ্ট্ৰিম কৰা ফাইল ডাউনলোড কৰক
+ সন্ধান কৰক
+ ছেটিংছ
+ %s ৰ বাবে ফলাফল দেখুৱা হৈছে
+ চেয়াৰ কৰক
+ কিছু ৰিজ’লিউচনত অডিঅ’ আঁতৰাওক
+ চাবস্ক্ৰাইব কৰা হ\'ল
+ আনচাবস্ক্ৰাইব
+ আৰম্ভ কৰিবলৈ মেগনিফাইং গ্লাছৰ চিহ্নত টিপক।
+ চাবস্ক্ৰাইব
+ চোৱা হ\'ল (চিহ্নিত কৰক)
+ আপুনি \"%1$s\" বুজাইছিল নেকি\?
+ বাহ্যিক ভিডিঅ’ প্লেয়াৰ ব্যৱহাৰ কৰক
+ বাহ্যিক অডিঅ’ প্লেয়াৰ ব্যৱহাৰ কৰক
+ Channel আনচাবস্ক্ৰাইব কৰা হ\'ল
+ subscription সলনি কৰিব পৰা নগ\'ল
+ subscription আপডেট কৰিব পৰা নগ\'ল
+ তথ্য দেখুৱাওক
+ চাবস্ক্ৰিপচন
+ বুকমাৰ্ক কৰা প্লেলিষ্ট
+ টেব নিৰ্বাচন কৰক
+ বেকগ্ৰাউণ্ড
+ পপ-আপ
+ স্থায়ী ৰিজ\'লিউচন
+ স্থায়ী পপআপ ৰিজোলিউচন
+ উচ্চ ৰিজ\'লিউচন দেখুৱাওক
+ কেৱল কিছুমান ডিভাইচেহে 2K/4K ভিডিঅ’ বজাব পাৰে
+ Kodi ৰ সৈতে বজাওক
+ Kore এপ ইনষ্টল\?
+ \"Kodi ৰ সৈতে খোলক\" বিকল্প দেখুৱাওক
+ Kodi মিডিয়া চেণ্টাৰৰ জৰিয়তে এটা ভিডিঅ\' চলাবলৈ এটা বিকল্প প্ৰদৰ্শন কৰক
+ প্লেয়াৰটো ক্ৰেচ কৰক
+ বাফাৰিং
+ নথিং
+ জাননী ৰঙিণ কৰক
+ অডিঅ\'
+ অডিঅ\' ৰ প্ৰকাৰ
+ ভিডিঅ\'ৰ প্ৰকাৰ
+ থিম
+ নিশাৰ থিম
+ পোহৰ
+ অন্ধকাৰ
+ ক\'লা
+ পপ-আপ বৈশিষ্ট্যসমূহ মনত ৰাখিব
+ পপ-আপৰ অন্তিম আকাৰ আৰু অৱস্থান মনত ৰাখিব
+ Inexact seek য়ে প্লেয়াৰটোক দ্ৰুত গতিত স্থান সলনি কৰিবলৈ অনুমতি দিয়ে। ৫, ১৫ বা ২৫ ছেকেণ্ড সলনি কৰিবলৈ বিচাৰিলে ইয়াৰ প্ৰয়োজন নহয়
+ ফাষ্ট-ফৰৱাৰ্ড/-ৰিৱাইণ্ড কৰিবলৈ বিচৰা সময়সীমা
+ প্লেবেক লোড কৰাৰ ব্যৱধানৰ আকাৰ
+ লোড ব্যৱধানৰ আকাৰ সলনি কৰক (বৰ্তমানে %s) । এটা কম মানে প্ৰাৰম্ভিক ভিডিঅ\' লোডিং দ্ৰুত কৰিব পাৰে। পৰিৱৰ্তনৰ বাবে এটা খেলুৱৈ পুনৰাৰম্ভৰ প্ৰয়োজন
+ থাম্বনেইলত থকা মূল ৰং অনুসৰি এণ্ড্ৰইডক জাননীৰ ৰং কাষ্টমাইজ কৰিবলৈ কওক (মন কৰিব যে এইটো সকলো ডিভাইচতে উপলব্ধ নহয়)
+ সক্ৰিয় প্লেয়াৰৰ queue সলনি কৰা হ’ব
+ থাম্বনেইল লোড কৰক
+ মন্তব্য দেখুৱাওক
+ বিৱৰণ দেখুৱাওক
+ মেটা তথ্য দেখুৱাওক
+ সংৰক্ষিত ছবি মচি পেলোৱা হ\'ল
+ সংৰক্ষিত কৰি থোৱা মেটাডাটা মচি পেলাওক
+ সকলো সংৰক্ষণ কৰি ৰখা ৱেবপেজৰ তথ্য আঁতৰাওক
+ সংৰক্ষণ কৰি থোৱা মেটাডাটা মচি পেলোৱা হ\'ল
+ পৰৱৰ্তী ষ্ট্ৰিম স্বয়ংক্ৰিয়ভাৱে enque কৰক
+ সজোৱা
+ ভিডিঅ\' ডাউনলোড folder
+ যোগ কৰক
+ ডাউনলোড কৰা অডিঅ\' ফাইলসমূহ ইয়াত সংৰক্ষণ কৰা হয়
+ থাম্বনেইলক ১:১ অনুপাত লৈ ক্ৰপ কৰক
+ ডাউনলোড কৰা ভিডিঅ’ ফাইলসমূহ ইয়াত সংৰক্ষণ কৰা হয়
+ ভিডিঅ\' ফাইলসমূহৰ বাবে ডাউনলোড folder বাছক
+ অডিঅ\' ডাউনলোড folder
+ অডিঅ\' ফাইলসমূহৰ বাবে ডাউনলোড folder নিৰ্বাচন কৰক
+ জাননীত দেখুওৱা ভিডিঅ’ থাম্বনেইলটো ১৬:৯ৰ পৰা ১:১ অনুপাতলৈ ক্ৰপ কৰক
+ First action button
+ Fifth action button
+ Edit each notification action below by tapping on it. Select up to three of them to be shown in the compact notification by using the checkboxes on the right
+ Second action button
+ You can select at most three actions to show in the compact notification!
+ পুনৰাবৃত্তি
+ শ্বাফেল
+ দ্ৰুত inexact seek ব্যৱহাৰ কৰক
+ এটা queue বিলুপ্তি কৰাৰ আগতে নিশ্চিতকৰণৰ বাবে সুধিব
+ এটা প্লেয়াৰ পৰা আন এটালৈ সলনি কৰিলে আপোনাৰ queue সলনি হ\'ব পাৰে
+ Fourth action button
+ Third action button
+ ভিডিঅ\'ৰ বিৱৰণ আৰু অতিৰিক্ত তথ্য লুকুৱাবলৈ বন্ধ কৰক
+ মন্তব্য লুকুৱাবলৈ বন্ধ কৰক
+ \'পৰৱৰ্তী\' আৰু \'সাদৃশ্য থকা\' ভিডিঅ\' দেখুৱাওক
+ থাম্বনেইলসমূহ লোড কৰা, তথ্য আৰু মেমৰি ব্যৱহাৰ সংৰক্ষণ কৰা ৰোধ কৰিবলে বন্ধ কৰক। পৰিবৰ্তনসমূহে ইন-মেমৰি আৰু অন-ডিস্ক কেশ্ব দুয়োটা পৰিষ্কাৰ কৰে
+ ষ্ট্ৰিমৰ সৃষ্টিকৰ্তা, ষ্ট্ৰিমৰ বিষয়বস্তু বা এটা সন্ধান অনুৰোধৰ বিষয়ে অতিৰিক্ত তথ্যৰ সৈতে মেটা তথ্যৰ বাকচসমূহ লুকুৱাবলৈ বন্ধ কৰক
+
\ No newline at end of file
diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml
index b5480ba48..0cce71fb0 100644
--- a/app/src/main/res/values-az/strings.xml
+++ b/app/src/main/res/values-az/strings.xml
@@ -1,82 +1,82 @@
- Başlamaq üçün böyüdücüyə toxun.
+ Başlamaq üçün böyüdücü güzgüyə toxun.
%1$s tarixində yayımlanıb
- Yayım oynadıcı tapılmadı. \"VLC\" yüklənilsin\?
- Yayım oynadıcı tapılmadı (Oynatmaq üçün VLC\'ni quraşdıra bilərsiniz).
+ Yayım oynadıcı tapılmadı. \"VLC\" quraşdırılsın\?
+ Yayım oynadıcı tapılmadı (Oynatmaq üçün VLC quraşdıra bilərsiniz).
Yüklə
- İmtina
+ Ləğv et
Brauzerdə aç
Paylaş
Endir
Yayım faylını endir
- Axtar
+ Axtarış
Tənzimləmələr
- Bunu nəzərdə tuturdunuz: \"%1$s\"\?
+ Bunu demək istəyirdiniz: \"%1$s\"\?
ilə paylaş
Xarici video oynadıcı istifadə et
- Bəzi qətnamələrdə səsi silir
+ Bəzi ayırdetmələrdə səsi silir
Xarici səs oynadıcı istifadə et
- Abunə Olun
+ Abunə Ol
Abunə olundu
Kanal abunəliyi ləğv edildi
Məlumat göstər
- Abunəliklər
+ Abunələr
Əlfəcinlənmiş Pleylistlər
Yeniliklər
- Arxa Fon
+ Fon
Video endirmə qovluğu
Endirilmiş video fayllar burada saxlanılır
- Video faylları üçün endirmə qovluğunu seç
+ Video fayllar üçün endirmə qovluğu seç
Səs endirmə qovluğu
Endirilmiş səs faylları burada saxlanılır
- Səs faylları üçün endirmə qovluğunu seç
- Defolt keyfiyyət
- Daha böyük keyfiyyət seçimləri göstər
+ Səs faylları üçün endirmə qovluğu seç
+ Standart ayırdetmə
+ Daha böyük ayırdetmələr göstər
\"Kodi\" ilə Oynat
Çatışmayan \"Kore\" tətbiqi yüklənilsin\?
\"Kodi ilə Oynat\" seçimini göstər
- Videonu Kodi media mərkəzi ilə oynatmaq üçün seçim göstər
+ Kodi media mərkəzindən video oynatmaq üçün seçim göstər
Səs
- Defolt səs formatı
- Defolt video formatı
- Mövzu
+ Standart səs formatı
+ Standart video formatı
+ Tema
İşıqlı
Qaranlıq
Qara
- Abunəlikdən çıxın
- Ani pəncərə rejimində aç
- Avtomatik oynatma
- Endirin
- Fasilələrdən sonra (məsələn, telefon zəngləri) oynatmağa davam etdirin
+ Abunə olma
+ Ani görüntü rejimində aç
+ Avtomatik oynat
+ Yüklə
+ Fasilələr ardınca (məsələn, telefon zəngləri) oynatmağa davam etdir
Oynatmanı davam etdir
Baxılmış videoların saxlanılması
- Məlumat təmizləmə
- Siyahılarda oynatma mövqelərini göstərin
+ Məlumat təmizlə
+ Siyahılarda oynatma mövqe göstəricilərini göstər
Siyahılardakı mövqelər
- Son oynatma mövqeyinə qaytarın
+ Son oynatma mövqeyini qaytar
Oynatmanı davam etdir
Baxış tarixçəsi
- Axtarış sorğularını yerli olaraq saxlayın
+ Axtarış sorğularını yerli olaraq saxla
Axtarış tarixçəsi
- Axtarış edərkən göstəriləcək təklifləri seçin
+ Axtarış zamanı göstərmək üçün təklifləri seç
Axtarış təklifləri
- Oynadıcının parlaqlığını nizamlamaq üçün jestlərdən istifadə edin
- Parlaqlığı jestlə nizamlama
- Oynadıcının səsini nizamlamaq üçün jestlərdən istifadə edin
- Səsi jestlə nizamla
- Avto-növbələmə
- Növbəti Yayımı Avto-növbələmə
+ Oynadıcı parlaqlığını nizamlamaq üçün jestlər istifadə et
+ Parlaqlıq jesti idarəetməsi
+ Oynadıcı səsini nizamlamaq üçün jestlər istifadə et
+ Səs səviyyəsi jesti idarəetməsi
+ Avto-növbələ
+ Növbəti Yayımı Avto-növbələ
Üst məlumat keşi silindi
Keşlənmiş bütün veb-səhifə məlumatlarını sil
Keşlənmiş üst məlumatı təmizlə
Şəkil keşi silindi
- Şərhləri gizlətmək üçün söndür
+ Şərhləri gizlətmək üçün bağla
Şərhləri göstər
- Aktiv oynadıcının növbəsi dəyişdiriləcək
+ Aktiv oynadıcı növbəsi dəyişdiriləcək
Bir oynadıcıdan digərinə keçid növbənizi dəyişdirə bilər
Növbəni təmizləməzdən əvvəl təsdiq üçün soruş
- Sürətli qeyri-dəqiq axtarışdan istifadə edin
+ Sürətli qeyri-dəqiq axtarış istifadə et
Qeyri-dəqiq axtarış oynadıcıya azaldılmış dəqiqliklə mövqeləri daha sürətli axtarmağa imkan verir. 5, 15 və ya 25 saniyəlik axtarış bununla işləmir
Sürətli irəli/geri çəkmə axtarış müddəti
Heç nə
@@ -89,21 +89,21 @@
İkinci fəaliyyət düyməsi
Birinci fəaliyyət düyməsi
Yalnız bəzi cihazlar 2K/4K videoları oynada bilir
- Defolt ani pəncərə keyfiyyəti
+ Standart ani görüntü ayırdetməsi
Əlavə Et
- Ani Pəncərə
+ Ani Görüntü
Paneli Seç
Abunəliyi yeniləmək alınmadı
- Abunəliyi dəyişdirmək alınmadı
+ Abunəliyi dəyişmək alınmadı
Nəticələr göstərilir: %s
Kanallar
%s tərəfindən
- YouTube\'un \"Məhdud Rejimi\"ni açın
- Yaş həddi səbəbiylə (məsələn, 18+) uşaqlar üçün uyğun olmayan məzmunu göstərin
+ YouTube\'un \"Məhdud Rejimi\"ni aç
+ Yaş həddi səbəbiylə (məsələn, 18+) uşaqlar üçün uyğun olmayan məzmunu göstər
Yaş məhdudiyyətli məzmunu göstər
Məzmun
- Ani pəncərədə oynadılır
- Arxa fonda oynadılır
+ Ani görüntü rejimində oynadılır
+ Fonda oynadılır
Yeniləmələr
Sazlama
Görünüş
@@ -111,37 +111,37 @@
Video və səs
Davranış
Oynadıcı
- Defolt məzmun dili
- Defolt məzmun ölkəsi
+ Cari məzmun dili
+ Cari məzmun ölkəsi
URL\'i tanımaq olmadı. Başqa tətbiqlə açılsın\?
Dəstəklənməyən URL\'i
- \"Növbəyə əlavə etmək üçün basılı saxla\" ipucusun göstər
+ \"Növbələmək üçün basılı saxla\" tövsiyəsin göstər
\"Növbəti\" və \"Bənzər\" videoları göstər
- Tarixçəni, abunəlikləri, pleylistləri və tənzimləmələri ixrac edin
- Cari tarixçənizi, abunəliklərinizi, pleylistlərinizi və (istəyə görə) tənzimləmələrinizi etibarsız edir
- reCAPTCHA kukiləri təmizləndi
- reCAPTCHA kukilərini təmizləyin
- Məlumat bazasını ixrac edin
- Məlumat bazasını idxal edin
+ Tarixçəni, abunəlikləri, pleylistləri və tənzimləmələri ixrac et
+ Cari tarixçənizi, abunəliklərinizi, pleylistlərinizi və (könüllü) tənzimləmələrinizi etibarsız edir
+ reCAPTCHA bazaları təmizləndi
+ reCAPTCHA bazalarını təmizlə
+ Məlumat bazasını ixrac et
+ Məlumat bazasını idxal et
Əsas Görünüşə Keçid
- Ani Pəncərəyə Keçid
- Arxa Fona Keçid
+ Ani Görüntüyə Keçid
+ Fona Keçid
[Naməlum]
- Yeni \"NewPipe\" versiyası üçün bildirişlər
+ Yeni \"NewPipe\" versiyaları üçün bildirişlər
Tətbiq yeniləmə bildirişi
NewPipe oynadıcısı üçün bildirişlər
Hamısı
Xəta hesabatı
- Endirmələr
- Endirmələr
+ Endirilənlər
+ Endirilənlər
Canlı
Bu video yaş məhdudiyyətlidir.
\n
\nOnu görmək istəyirsinizsə, tənzimləmələrdə \"%1$s\" seçimini aktivləşdirin.
YouTube potensial yetkin məzmunu gizlədən \"Məhdud Rejim\" təmin edir
- \"PeerTube\" serverləri
+ \"PeerTube\" nümunələri
Miniatürləri yüklə
- Siz yığcam bildirişdə göstərilməsi üçün ən çoxu üç fəaliyyət seçə bilərsiniz!
+ Yığcam bildirişdə göstərmək üçün ən çoxu üç fəaliyyət seçə bilərsiniz!
Həmişə yenilə
Axın
Yalnız qruplaşdırılmamış abunəlikləri göstər
@@ -153,7 +153,7 @@
- %d seçildi
Abunəlik seçilməyib
- Abunəlikləri seçin
+ Abunəlikləri seç
Axın emal edilir…
Axın yüklənir…
Yüklənmədi: %d
@@ -166,7 +166,7 @@
Fayl silindi
Geri qaytar
Ən yaxşı keyfiyyət
- Təmizləyin
+ Təmizlə
Qeyri-aktivdir
Sənətkarlar
Albomlar
@@ -177,48 +177,47 @@
Videolar
Pleylistlər
Xəta
- Kömək
Axtarış tarixçəsi silindi
Bütün axtarış tarixçəsi silinsin\?
- Axtarışdakı açar sözlərin tarixçəsini silir
- Axtarış tarixçəsini silin
+ Açar sözləri axtarışı tarixçəsini silir
+ Axtarış tarixçəsini sil
Oynatma mövqeləri silindi
Bütün oynatma mövqeləri silinsin\?
- Bütün oynatma mövqelərini siləcək
- Oynatma mövqelərini silin
+ Bütün oynatma mövqelərini silir
+ Oynatma mövqelərini sil
Baxış tarixçəsi silindi
Bütün baxış tarixçəsi silinsin\?
Baxış tarixçəsini təmizlə
- reCAPTCHA həll edərkən NewPipe\'ın saxladığı kukiləri silin
+ reCAPTCHA həll edərkən NewPipe saxladığı bazaları sil
%s tərəfindən yaradıldı
Yaxınlaşdır
Doldur
- Dart
+ Uyğunlaşdır
Altyazı Yoxdur
- Silin
+ Sil
Hələ ki, kanal abunəliyi yoxdur
- Kanal seçin
+ Kanal seç
Kanal Səhifəsi
- Defolt Köşk
- Köşk Səhifəsi
+ Standart Köşk
+ Köşk Səhifə
Boş Səhifə
Əsas səhifədə hansı tablar göstərilir
- Əsas səhifənin məzmunu
+ Əsas səhifə məzmunu
Yeni versiya mövcud olduqda tətbiq yeniləməsini xatırlatmaq üçün bildiriş göstər
Yeniləmələr
- Mobil internet istifadə edərkən görüntü keyfiyyətini məhdudlaşdır
+ Mobil internet istifadə edərkən ayırdetməni məhdudlaşdır
Limitsiz
1 element silindi.
- Server əlavə edin
- Sevimli \"PeerTube\" serverlərinizi seçin
- Endirilmiş faylları silin
- Endirmə tarixçənizi təmizləmək və ya endirilmiş bütün faylları silmək istəyirsiniz\?
+ Nümunə əlavə et
+ Sevimli \"PeerTube\" nümunələrinizi seçin
+ Endirilmiş faylları sil
+ Endirmə tarixçənizi təmizləmək və ya bütün endirilmiş faylları silmək istəyirsiniz\?
Endirmə tarixçəsini təmizlə
- Endirmələrə başla
- Endirmələrə fasilə verin
+ Endirmələri başlat
+ Endirmələri dayandır
Haraya endiriləcəyini soruş
Sizdən hər endirmənin harada saxlanılacağı soruşulacaq.
-\nXarici SD karta yükləmək istəyirsinizsə, sistem qovluğu seçicisini (SAF) aktiv edin
+\nXarici SD karta endirmək istəyirsinizsə, sistem qovluğu seçicisini (SAF) aktiv edin