getThumbnails() {
+ return thumbnails;
}
@NonNull
diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemBuilder.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemBuilder.java
index e7aeb9638..066f92c26 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemBuilder.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemBuilder.java
@@ -6,7 +6,7 @@ import android.view.MotionEvent;
import android.view.View;
import org.schabi.newpipe.util.Localization;
-import org.schabi.newpipe.util.PicassoHelper;
+import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
public class PlayQueueItemBuilder {
@@ -33,7 +33,7 @@ public class PlayQueueItemBuilder {
holder.itemDurationView.setVisibility(View.GONE);
}
- PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(holder.itemThumbnailView);
+ PicassoHelper.loadThumbnail(item.getThumbnails()).into(holder.itemThumbnailView);
holder.itemRoot.setOnClickListener(view -> {
if (onItemClickListener != null) {
diff --git a/app/src/main/java/org/schabi/newpipe/player/seekbarpreview/SeekbarPreviewThumbnailHolder.java b/app/src/main/java/org/schabi/newpipe/player/seekbarpreview/SeekbarPreviewThumbnailHolder.java
index 50ffa2f2a..26065de15 100644
--- a/app/src/main/java/org/schabi/newpipe/player/seekbarpreview/SeekbarPreviewThumbnailHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/player/seekbarpreview/SeekbarPreviewThumbnailHolder.java
@@ -14,7 +14,7 @@ import androidx.collection.SparseArrayCompat;
import com.google.common.base.Stopwatch;
import org.schabi.newpipe.extractor.stream.Frameset;
-import org.schabi.newpipe.util.PicassoHelper;
+import org.schabi.newpipe.util.image.PicassoHelper;
import java.util.Comparator;
import java.util.List;
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
index 92e38a6a2..03f90a344 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
@@ -740,7 +740,7 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
String videoUrl = player.getVideoUrl();
videoUrl += ("&t=" + seconds);
ShareUtils.shareText(context, currentItem.getTitle(),
- videoUrl, currentItem.getThumbnailUrl());
+ videoUrl, currentItem.getThumbnails());
}
}
};
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
index 119c43b95..b51aaa638 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
@@ -226,7 +226,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
final PlayQueueItem currentItem = player.getCurrentItem();
if (currentItem != null) {
ShareUtils.shareText(context, currentItem.getTitle(),
- player.getVideoUrlAtCurrentTime(), currentItem.getThumbnailUrl());
+ player.getVideoUrlAtCurrentTime(), currentItem.getThumbnails());
}
}));
binding.share.setOnLongClickListener(v -> {
diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java
index ec3b1b2d7..ec2bed67a 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java
@@ -12,7 +12,9 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.localization.Localization;
-import org.schabi.newpipe.util.PicassoHelper;
+import org.schabi.newpipe.util.image.ImageStrategy;
+import org.schabi.newpipe.util.image.PicassoHelper;
+import org.schabi.newpipe.util.image.PreferredImageQuality;
import java.io.IOException;
@@ -35,9 +37,11 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
.getPreferredContentCountry(requireContext());
initialLanguage = defaultPreferences.getString(getString(R.string.app_language_key), "en");
- findPreference(getString(R.string.download_thumbnail_key)).setOnPreferenceChangeListener(
+ final Preference imageQualityPreference = requirePreference(R.string.image_quality_key);
+ imageQualityPreference.setOnPreferenceChangeListener(
(preference, newValue) -> {
- PicassoHelper.setShouldLoadImages((Boolean) newValue);
+ ImageStrategy.setPreferredImageQuality(PreferredImageQuality
+ .fromPreferenceKey(requireContext(), (String) newValue));
try {
PicassoHelper.clearCache(preference.getContext());
Toast.makeText(preference.getContext(),
diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java
index 0f4c9765e..d78ade49d 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java
@@ -10,7 +10,7 @@ import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
-import org.schabi.newpipe.util.PicassoHelper;
+import org.schabi.newpipe.util.image.PicassoHelper;
import java.util.Optional;
diff --git a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java
index 3776d78f6..32e33d55b 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java
@@ -23,7 +23,7 @@ public class MainSettingsFragment extends BasePreferenceFragment {
setHasOptionsMenu(true); // Otherwise onCreateOptionsMenu is not called
// Check if the app is updatable
- if (!ReleaseVersionUtil.isReleaseApk()) {
+ if (!ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
getPreferenceScreen().removePreference(
findPreference(getString(R.string.update_pref_screen_key)));
diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java
index 0f25be630..37335421d 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java
@@ -19,7 +19,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.local.subscription.SubscriptionManager;
-import org.schabi.newpipe.util.PicassoHelper;
+import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.List;
diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java
index e8491d52c..147d20a36 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java
@@ -25,7 +25,7 @@ import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
-import org.schabi.newpipe.util.PicassoHelper;
+import org.schabi.newpipe.util.image.PicassoHelper;
import java.util.List;
import java.util.Vector;
diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java b/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java
index 215caaa38..b7bafde75 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java
@@ -128,6 +128,20 @@ public final class SettingMigrations {
}
};
+ public static final Migration MIGRATION_5_6 = new Migration(5, 6) {
+ @Override
+ protected void migrate(@NonNull final Context context) {
+ final boolean loadImages = sp.getBoolean("download_thumbnail_key", true);
+
+ sp.edit()
+ .putString(context.getString(R.string.image_quality_key),
+ context.getString(loadImages
+ ? R.string.image_quality_default
+ : R.string.image_quality_none_key))
+ .apply();
+ }
+ };
+
/**
* List of all implemented migrations.
*
@@ -140,12 +154,13 @@ public final class SettingMigrations {
MIGRATION_2_3,
MIGRATION_3_4,
MIGRATION_4_5,
+ MIGRATION_5_6,
};
/**
* Version number for preferences. Must be incremented every time a migration is necessary.
*/
- private static final int VERSION = 5;
+ private static final int VERSION = 6;
public static void runMigrationsIfNeeded(@NonNull final Context context,
diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java
index 3ee6668bf..529e53442 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java
@@ -266,7 +266,7 @@ public class SettingsActivity extends AppCompatActivity implements
*/
private void ensureSearchRepresentsApplicationState() {
// Check if the update settings are available
- if (!ReleaseVersionUtil.isReleaseApk()) {
+ if (!ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
SettingsResourceRegistry.getInstance()
.getEntryByPreferencesResId(R.xml.update_settings)
.setSearchable(false);
diff --git a/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java
index aae9cfca5..a1f563724 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java
@@ -13,6 +13,7 @@ import androidx.preference.ListPreference;
import com.google.android.material.snackbar.Snackbar;
import org.schabi.newpipe.R;
+import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.PermissionHelper;
import java.util.LinkedList;
@@ -26,7 +27,7 @@ public class VideoAudioSettingsFragment extends BasePreferenceFragment {
addPreferencesFromResourceRegistry();
updateSeekOptions();
-
+ updateResolutionOptions();
listener = (sharedPreferences, key) -> {
// on M and above, if user chooses to minimise to popup player on exit
@@ -48,10 +49,84 @@ public class VideoAudioSettingsFragment extends BasePreferenceFragment {
}
} else if (getString(R.string.use_inexact_seek_key).equals(key)) {
updateSeekOptions();
+ } else if (getString(R.string.show_higher_resolutions_key).equals(key)) {
+ updateResolutionOptions();
}
};
}
+ /**
+ * Update default resolution, default popup resolution & mobile data resolution options.
+ *
+ * Show high resolutions when "Show higher resolution" option is enabled.
+ * Set default resolution to "best resolution" when "Show higher resolution" option
+ * is disabled.
+ */
+ private void updateResolutionOptions() {
+ final Resources resources = getResources();
+ final boolean showHigherResolutions = getPreferenceManager().getSharedPreferences()
+ .getBoolean(resources.getString(R.string.show_higher_resolutions_key), false);
+
+ // get sorted resolution lists
+ final List resolutionListDescriptions = ListHelper.getSortedResolutionList(
+ resources,
+ R.array.resolution_list_description,
+ R.array.high_resolution_list_descriptions,
+ showHigherResolutions);
+ final List resolutionListValues = ListHelper.getSortedResolutionList(
+ resources,
+ R.array.resolution_list_values,
+ R.array.high_resolution_list_values,
+ showHigherResolutions);
+ final List limitDataUsageResolutionValues = ListHelper.getSortedResolutionList(
+ resources,
+ R.array.limit_data_usage_values_list,
+ R.array.high_resolution_limit_data_usage_values_list,
+ showHigherResolutions);
+ final List limitDataUsageResolutionDescriptions = ListHelper
+ .getSortedResolutionList(resources,
+ R.array.limit_data_usage_description_list,
+ R.array.high_resolution_list_descriptions,
+ showHigherResolutions);
+
+ // get resolution preferences
+ final ListPreference defaultResolution = findPreference(
+ getString(R.string.default_resolution_key));
+ final ListPreference defaultPopupResolution = findPreference(
+ getString(R.string.default_popup_resolution_key));
+ final ListPreference mobileDataResolution = findPreference(
+ getString(R.string.limit_mobile_data_usage_key));
+
+ // update resolution preferences with new resolutions, entries & values for each
+ defaultResolution.setEntries(resolutionListDescriptions.toArray(new String[0]));
+ defaultResolution.setEntryValues(resolutionListValues.toArray(new String[0]));
+ defaultPopupResolution.setEntries(resolutionListDescriptions.toArray(new String[0]));
+ defaultPopupResolution.setEntryValues(resolutionListValues.toArray(new String[0]));
+ mobileDataResolution.setEntries(
+ limitDataUsageResolutionDescriptions.toArray(new String[0]));
+ mobileDataResolution.setEntryValues(limitDataUsageResolutionValues.toArray(new String[0]));
+
+ // if "Show higher resolution" option is disabled,
+ // set default resolution to "best resolution"
+ if (!showHigherResolutions) {
+ if (ListHelper.isHighResolutionSelected(defaultResolution.getValue(),
+ R.array.high_resolution_list_values,
+ resources)) {
+ defaultResolution.setValueIndex(0);
+ }
+ if (ListHelper.isHighResolutionSelected(defaultPopupResolution.getValue(),
+ R.array.high_resolution_list_values,
+ resources)) {
+ defaultPopupResolution.setValueIndex(0);
+ }
+ if (ListHelper.isHighResolutionSelected(mobileDataResolution.getValue(),
+ R.array.high_resolution_limit_data_usage_values_list,
+ resources)) {
+ mobileDataResolution.setValueIndex(0);
+ }
+ }
+ }
+
/**
* Update fast-forward/-rewind seek duration options
* according to language and inexact seek setting.
diff --git a/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java b/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java
index 3e92f297e..7dfddef20 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java
@@ -5,35 +5,22 @@ import static org.schabi.newpipe.player.notification.NotificationConstants.ACTIO
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
-import android.content.res.ColorStateList;
+import android.os.Build;
import android.util.AttributeSet;
-import android.view.LayoutInflater;
import android.view.View;
-import android.view.ViewGroup;
import android.widget.CheckBox;
-import android.widget.ImageView;
-import android.widget.RadioButton;
-import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.content.res.AppCompatResources;
-import androidx.core.widget.TextViewCompat;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
-import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
-import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
import org.schabi.newpipe.player.notification.NotificationConstants;
-import org.schabi.newpipe.util.DeviceUtils;
-import org.schabi.newpipe.util.ThemeHelper;
-import org.schabi.newpipe.views.FocusOverlayView;
+import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
@@ -45,8 +32,9 @@ public class NotificationActionsPreference extends Preference {
}
- @Nullable private NotificationSlot[] notificationSlots = null;
- @Nullable private List compactSlots = null;
+ private NotificationSlot[] notificationSlots;
+ private List compactSlots;
+
////////////////////////////////////////////////////////////////////////////
// Lifecycle
@@ -56,6 +44,11 @@ public class NotificationActionsPreference extends Preference {
public void onBindViewHolder(@NonNull final PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ ((TextView) holder.itemView.findViewById(R.id.summary))
+ .setText(R.string.notification_actions_summary_android13);
+ }
+
holder.itemView.setClickable(false);
setupActions(holder.itemView);
}
@@ -75,13 +68,29 @@ public class NotificationActionsPreference extends Preference {
////////////////////////////////////////////////////////////////////////////
private void setupActions(@NonNull final View view) {
- compactSlots = NotificationConstants.getCompactSlotsFromPreferences(getContext(),
- getSharedPreferences(), 5);
+ compactSlots = new ArrayList<>(NotificationConstants.getCompactSlotsFromPreferences(
+ getContext(), getSharedPreferences()));
notificationSlots = IntStream.range(0, 5)
- .mapToObj(i -> new NotificationSlot(i, view))
+ .mapToObj(i -> new NotificationSlot(getContext(), getSharedPreferences(), i, view,
+ compactSlots.contains(i), this::onToggleCompactSlot))
.toArray(NotificationSlot[]::new);
}
+ private void onToggleCompactSlot(final int i, final CheckBox checkBox) {
+ if (checkBox.isChecked()) {
+ compactSlots.remove((Integer) i);
+ } else if (compactSlots.size() < 3) {
+ compactSlots.add(i);
+ } else {
+ Toast.makeText(getContext(),
+ R.string.notification_actions_at_most_three,
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ checkBox.toggle();
+ }
+
////////////////////////////////////////////////////////////////////////////
// Saving
@@ -99,143 +108,10 @@ public class NotificationActionsPreference extends Preference {
for (int i = 0; i < 5; i++) {
editor.putInt(getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
- notificationSlots[i].selectedAction);
+ notificationSlots[i].getSelectedAction());
}
editor.apply();
}
}
-
-
- ////////////////////////////////////////////////////////////////////////////
- // Notification action
- ////////////////////////////////////////////////////////////////////////////
-
- private static final int[] SLOT_ITEMS = {
- R.id.notificationAction0,
- R.id.notificationAction1,
- R.id.notificationAction2,
- R.id.notificationAction3,
- R.id.notificationAction4,
- };
-
- private static final int[] SLOT_TITLES = {
- R.string.notification_action_0_title,
- R.string.notification_action_1_title,
- R.string.notification_action_2_title,
- R.string.notification_action_3_title,
- R.string.notification_action_4_title,
- };
-
- private class NotificationSlot {
-
- final int i;
- @NotificationConstants.Action int selectedAction;
-
- ImageView icon;
- TextView summary;
-
- NotificationSlot(final int actionIndex, final View parentView) {
- this.i = actionIndex;
-
- final View view = parentView.findViewById(SLOT_ITEMS[i]);
- setupSelectedAction(view);
- setupTitle(view);
- setupCheckbox(view);
- }
-
- void setupTitle(final View view) {
- ((TextView) view.findViewById(R.id.notificationActionTitle))
- .setText(SLOT_TITLES[i]);
- view.findViewById(R.id.notificationActionClickableArea).setOnClickListener(
- v -> openActionChooserDialog());
- }
-
- void setupCheckbox(final View view) {
- final CheckBox compactSlotCheckBox = view.findViewById(R.id.notificationActionCheckBox);
- compactSlotCheckBox.setChecked(compactSlots.contains(i));
- view.findViewById(R.id.notificationActionCheckBoxClickableArea).setOnClickListener(
- v -> {
- if (compactSlotCheckBox.isChecked()) {
- compactSlots.remove((Integer) i);
- } else if (compactSlots.size() < 3) {
- compactSlots.add(i);
- } else {
- Toast.makeText(getContext(),
- R.string.notification_actions_at_most_three,
- Toast.LENGTH_SHORT).show();
- return;
- }
-
- compactSlotCheckBox.toggle();
- });
- }
-
- void setupSelectedAction(final View view) {
- icon = view.findViewById(R.id.notificationActionIcon);
- summary = view.findViewById(R.id.notificationActionSummary);
- selectedAction = getSharedPreferences().getInt(
- getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
- NotificationConstants.SLOT_DEFAULTS[i]);
- updateInfo();
- }
-
- void updateInfo() {
- if (NotificationConstants.ACTION_ICONS[selectedAction] == 0) {
- icon.setImageDrawable(null);
- } else {
- icon.setImageDrawable(AppCompatResources.getDrawable(getContext(),
- NotificationConstants.ACTION_ICONS[selectedAction]));
- }
-
- summary.setText(NotificationConstants.getActionName(getContext(), selectedAction));
- }
-
- void openActionChooserDialog() {
- final LayoutInflater inflater = LayoutInflater.from(getContext());
- final SingleChoiceDialogViewBinding binding =
- SingleChoiceDialogViewBinding.inflate(inflater);
-
- final AlertDialog alertDialog = new AlertDialog.Builder(getContext())
- .setTitle(SLOT_TITLES[i])
- .setView(binding.getRoot())
- .setCancelable(true)
- .create();
-
- final View.OnClickListener radioButtonsClickListener = v -> {
- selectedAction = NotificationConstants.SLOT_ALLOWED_ACTIONS[i][v.getId()];
- updateInfo();
- alertDialog.dismiss();
- };
-
- for (int id = 0; id < NotificationConstants.SLOT_ALLOWED_ACTIONS[i].length; ++id) {
- final int action = NotificationConstants.SLOT_ALLOWED_ACTIONS[i][id];
- final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater)
- .getRoot();
-
- // if present set action icon with correct color
- final int iconId = NotificationConstants.ACTION_ICONS[action];
- if (iconId != 0) {
- radioButton.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, iconId, 0);
-
- final var color = ColorStateList.valueOf(ThemeHelper
- .resolveColorFromAttr(getContext(), android.R.attr.textColorPrimary));
- TextViewCompat.setCompoundDrawableTintList(radioButton, color);
- }
-
- radioButton.setText(NotificationConstants.getActionName(getContext(), action));
- radioButton.setChecked(action == selectedAction);
- radioButton.setId(id);
- radioButton.setLayoutParams(new RadioGroup.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
- radioButton.setOnClickListener(radioButtonsClickListener);
- binding.list.addView(radioButton);
- }
- alertDialog.show();
-
- if (DeviceUtils.isTv(getContext())) {
- FocusOverlayView.setupFocusObserver(alertDialog);
- }
- }
- }
}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationSlot.java b/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationSlot.java
new file mode 100644
index 000000000..981ba3e75
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationSlot.java
@@ -0,0 +1,172 @@
+package org.schabi.newpipe.settings.custom;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.ColorStateList;
+import android.os.Build;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.ImageView;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.content.res.AppCompatResources;
+import androidx.core.widget.TextViewCompat;
+
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
+import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
+import org.schabi.newpipe.player.notification.NotificationConstants;
+import org.schabi.newpipe.util.DeviceUtils;
+import org.schabi.newpipe.util.ThemeHelper;
+import org.schabi.newpipe.views.FocusOverlayView;
+
+import java.util.Objects;
+import java.util.function.BiConsumer;
+
+class NotificationSlot {
+
+ private static final int[] SLOT_ITEMS = {
+ R.id.notificationAction0,
+ R.id.notificationAction1,
+ R.id.notificationAction2,
+ R.id.notificationAction3,
+ R.id.notificationAction4,
+ };
+
+ private static final int[] SLOT_TITLES = {
+ R.string.notification_action_0_title,
+ R.string.notification_action_1_title,
+ R.string.notification_action_2_title,
+ R.string.notification_action_3_title,
+ R.string.notification_action_4_title,
+ };
+
+ private final int i;
+ private @NotificationConstants.Action int selectedAction;
+ private final Context context;
+ private final BiConsumer onToggleCompactSlot;
+
+ private ImageView icon;
+ private TextView summary;
+
+ NotificationSlot(final Context context,
+ final SharedPreferences prefs,
+ final int actionIndex,
+ final View parentView,
+ final boolean isCompactSlotChecked,
+ final BiConsumer onToggleCompactSlot) {
+ this.context = context;
+ this.i = actionIndex;
+ this.onToggleCompactSlot = onToggleCompactSlot;
+
+ selectedAction = Objects.requireNonNull(prefs).getInt(
+ context.getString(NotificationConstants.SLOT_PREF_KEYS[i]),
+ NotificationConstants.SLOT_DEFAULTS[i]);
+ final View view = parentView.findViewById(SLOT_ITEMS[i]);
+
+ // only show the last two notification slots on Android 13+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || i >= 3) {
+ setupSelectedAction(view);
+ setupTitle(view);
+ setupCheckbox(view, isCompactSlotChecked);
+ } else {
+ view.setVisibility(View.GONE);
+ }
+ }
+
+ void setupTitle(final View view) {
+ ((TextView) view.findViewById(R.id.notificationActionTitle))
+ .setText(SLOT_TITLES[i]);
+ view.findViewById(R.id.notificationActionClickableArea).setOnClickListener(
+ v -> openActionChooserDialog());
+ }
+
+ void setupCheckbox(final View view, final boolean isCompactSlotChecked) {
+ final CheckBox compactSlotCheckBox = view.findViewById(R.id.notificationActionCheckBox);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ // there are no compact slots to customize on Android 13+
+ compactSlotCheckBox.setVisibility(View.GONE);
+ view.findViewById(R.id.notificationActionCheckBoxClickableArea)
+ .setVisibility(View.GONE);
+ return;
+ }
+
+ compactSlotCheckBox.setChecked(isCompactSlotChecked);
+ view.findViewById(R.id.notificationActionCheckBoxClickableArea).setOnClickListener(
+ v -> onToggleCompactSlot.accept(i, compactSlotCheckBox));
+ }
+
+ void setupSelectedAction(final View view) {
+ icon = view.findViewById(R.id.notificationActionIcon);
+ summary = view.findViewById(R.id.notificationActionSummary);
+ updateInfo();
+ }
+
+ void updateInfo() {
+ if (NotificationConstants.ACTION_ICONS[selectedAction] == 0) {
+ icon.setImageDrawable(null);
+ } else {
+ icon.setImageDrawable(AppCompatResources.getDrawable(context,
+ NotificationConstants.ACTION_ICONS[selectedAction]));
+ }
+
+ summary.setText(NotificationConstants.getActionName(context, selectedAction));
+ }
+
+ void openActionChooserDialog() {
+ final LayoutInflater inflater = LayoutInflater.from(context);
+ final SingleChoiceDialogViewBinding binding =
+ SingleChoiceDialogViewBinding.inflate(inflater);
+
+ final AlertDialog alertDialog = new AlertDialog.Builder(context)
+ .setTitle(SLOT_TITLES[i])
+ .setView(binding.getRoot())
+ .setCancelable(true)
+ .create();
+
+ final View.OnClickListener radioButtonsClickListener = v -> {
+ selectedAction = NotificationConstants.ALL_ACTIONS[v.getId()];
+ updateInfo();
+ alertDialog.dismiss();
+ };
+
+ for (int id = 0; id < NotificationConstants.ALL_ACTIONS.length; ++id) {
+ final int action = NotificationConstants.ALL_ACTIONS[id];
+ final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater)
+ .getRoot();
+
+ // if present set action icon with correct color
+ final int iconId = NotificationConstants.ACTION_ICONS[action];
+ if (iconId != 0) {
+ radioButton.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, iconId, 0);
+
+ final var color = ColorStateList.valueOf(ThemeHelper
+ .resolveColorFromAttr(context, android.R.attr.textColorPrimary));
+ TextViewCompat.setCompoundDrawableTintList(radioButton, color);
+ }
+
+ radioButton.setText(NotificationConstants.getActionName(context, action));
+ radioButton.setChecked(action == selectedAction);
+ radioButton.setId(id);
+ radioButton.setLayoutParams(new RadioGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+ radioButton.setOnClickListener(radioButtonsClickListener);
+ binding.list.addView(radioButton);
+ }
+ alertDialog.show();
+
+ if (DeviceUtils.isTv(context)) {
+ FocusOverlayView.setupFocusObserver(alertDialog);
+ }
+ }
+
+ @NotificationConstants.Action
+ public int getSelectedAction() {
+ return selectedAction;
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java b/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java
index 74fc74c76..0fe2e0408 100644
--- a/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java
@@ -5,11 +5,15 @@ import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
+import android.os.Build;
+import android.os.storage.StorageManager;
+import android.os.storage.StorageVolume;
import android.provider.DocumentsContract;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
import androidx.documentfile.provider.DocumentFile;
import org.schabi.newpipe.settings.NewPipeSettings;
@@ -23,6 +27,7 @@ import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -30,6 +35,8 @@ import static android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME;
import static android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
+import us.shandian.giga.util.Utility;
+
public class StoredDirectoryHelper {
private static final String TAG = StoredDirectoryHelper.class.getSimpleName();
public static final int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION
@@ -168,6 +175,44 @@ public class StoredDirectoryHelper {
return docTree == null;
}
+ /**
+ * Get free memory of the storage partition (root of the directory).
+ * @return amount of free memory in the volume of current directory (bytes)
+ */
+ @RequiresApi(api = Build.VERSION_CODES.N) // Necessary for `getStorageVolume()`
+ public long getFreeMemory() {
+ final Uri uri = getUri();
+ final StorageManager storageManager = (StorageManager) context.
+ getSystemService(Context.STORAGE_SERVICE);
+ final List volumes = storageManager.getStorageVolumes();
+
+ final String docId = DocumentsContract.getDocumentId(uri);
+ final String[] split = docId.split(":");
+ if (split.length > 0) {
+ final String volumeId = split[0];
+
+ for (final StorageVolume volume : volumes) {
+ // if the volume is an internal system volume
+ if (volume.isPrimary() && volumeId.equalsIgnoreCase("primary")) {
+ return Utility.getSystemFreeMemory();
+ }
+
+ // if the volume is a removable volume (normally an SD card)
+ if (volume.isRemovable() && !volume.isPrimary()) {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ try {
+ final String sdCardUUID = volume.getUuid();
+ return storageManager.getAllocatableBytes(UUID.fromString(sdCardUUID));
+ } catch (final Exception e) {
+ // do nothing
+ }
+ }
+ }
+ }
+ }
+ return Long.MAX_VALUE;
+ }
+
/**
* Only using Java I/O. Creates the directory named by this abstract pathname, including any
* necessary but nonexistent parent directories.
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 07d0f516d..066d5f570 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java
@@ -27,6 +27,7 @@ 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 androidx.preference.PreferenceManager;
@@ -113,14 +114,14 @@ public final class ExtractorHelper {
public static Single getStreamInfo(final int serviceId, final String url,
final boolean forceLoad) {
checkServiceId(serviceId);
- return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.STREAM,
+ return checkCache(forceLoad, serviceId, url, InfoCache.Type.STREAM,
Single.fromCallable(() -> StreamInfo.getInfo(NewPipe.getService(serviceId), url)));
}
public static Single getChannelInfo(final int serviceId, final String url,
final boolean forceLoad) {
checkServiceId(serviceId);
- return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.CHANNEL,
+ return checkCache(forceLoad, serviceId, url, InfoCache.Type.CHANNEL,
Single.fromCallable(() ->
ChannelInfo.getInfo(NewPipe.getService(serviceId), url)));
}
@@ -130,7 +131,7 @@ public final class ExtractorHelper {
final boolean forceLoad) {
checkServiceId(serviceId);
return checkCache(forceLoad, serviceId,
- listLinkHandler.getUrl(), InfoItem.InfoType.CHANNEL,
+ listLinkHandler.getUrl(), InfoCache.Type.CHANNEL_TAB,
Single.fromCallable(() ->
ChannelTabInfo.getInfo(NewPipe.getService(serviceId), listLinkHandler)));
}
@@ -145,10 +146,11 @@ public final class ExtractorHelper {
listLinkHandler, nextPage));
}
- public static Single getCommentsInfo(final int serviceId, final String url,
+ public static Single getCommentsInfo(final int serviceId,
+ final String url,
final boolean forceLoad) {
checkServiceId(serviceId);
- return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.COMMENT,
+ return checkCache(forceLoad, serviceId, url, InfoCache.Type.COMMENTS,
Single.fromCallable(() ->
CommentsInfo.getInfo(NewPipe.getService(serviceId), url)));
}
@@ -162,11 +164,20 @@ public final class ExtractorHelper {
CommentsInfo.getMoreItems(NewPipe.getService(serviceId), info, nextPage));
}
+ public static Single> getMoreCommentItems(
+ final int serviceId,
+ final String url,
+ final Page nextPage) {
+ checkServiceId(serviceId);
+ return Single.fromCallable(() ->
+ CommentsInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage));
+ }
+
public static Single getPlaylistInfo(final int serviceId,
final String url,
final boolean forceLoad) {
checkServiceId(serviceId);
- return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST,
+ return checkCache(forceLoad, serviceId, url, InfoCache.Type.PLAYLIST,
Single.fromCallable(() ->
PlaylistInfo.getInfo(NewPipe.getService(serviceId), url)));
}
@@ -179,9 +190,10 @@ public final class ExtractorHelper {
PlaylistInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage));
}
- public static Single getKioskInfo(final int serviceId, final String url,
+ public static Single getKioskInfo(final int serviceId,
+ final String url,
final boolean forceLoad) {
- return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST,
+ return checkCache(forceLoad, serviceId, url, InfoCache.Type.KIOSK,
Single.fromCallable(() -> KioskInfo.getInfo(NewPipe.getService(serviceId), url)));
}
@@ -193,7 +205,7 @@ public final class ExtractorHelper {
}
/*//////////////////////////////////////////////////////////////////////////
- // Utils
+ // Cache
//////////////////////////////////////////////////////////////////////////*/
/**
@@ -205,24 +217,25 @@ public final class ExtractorHelper {
* @param forceLoad whether to force loading from the network instead of from the cache
* @param serviceId the service to load from
* @param url the URL to load
- * @param infoType the {@link InfoItem.InfoType} of the item
+ * @param cacheType the {@link InfoCache.Type} of the item
* @param loadFromNetwork the {@link Single} to load the item from the network
* @return a {@link Single} that loads the item
*/
private static Single checkCache(final boolean forceLoad,
- final int serviceId, final String url,
- final InfoItem.InfoType infoType,
- final Single loadFromNetwork) {
+ final int serviceId,
+ @NonNull final String url,
+ @NonNull final InfoCache.Type cacheType,
+ @NonNull final Single loadFromNetwork) {
checkServiceId(serviceId);
final Single actualLoadFromNetwork = loadFromNetwork
- .doOnSuccess(info -> CACHE.putInfo(serviceId, url, info, infoType));
+ .doOnSuccess(info -> CACHE.putInfo(serviceId, url, info, cacheType));
final Single load;
if (forceLoad) {
- CACHE.removeInfo(serviceId, url, infoType);
+ CACHE.removeInfo(serviceId, url, cacheType);
load = actualLoadFromNetwork;
} else {
- load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url, infoType),
+ load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url, cacheType),
actualLoadFromNetwork.toMaybe())
.firstElement() // Take the first valid
.toSingle();
@@ -237,15 +250,17 @@ public final class ExtractorHelper {
* @param the item type's class that extends {@link Info}
* @param serviceId the service to load from
* @param url the URL to load
- * @param infoType the {@link InfoItem.InfoType} of the item
+ * @param cacheType the {@link InfoCache.Type} of the item
* @return a {@link Single} that loads the item
*/
- private static Maybe loadFromCache(final int serviceId, final String url,
- final InfoItem.InfoType infoType) {
+ private static Maybe loadFromCache(
+ final int serviceId,
+ @NonNull final String url,
+ @NonNull final InfoCache.Type cacheType) {
checkServiceId(serviceId);
return Maybe.defer(() -> {
//noinspection unchecked
- final I info = (I) CACHE.getFromKey(serviceId, url, infoType);
+ final I info = (I) CACHE.getFromKey(serviceId, url, cacheType);
if (MainActivity.DEBUG) {
Log.d(TAG, "loadFromCache() called, info > " + info);
}
@@ -259,11 +274,17 @@ public final class ExtractorHelper {
});
}
- public static boolean isCached(final int serviceId, final String url,
- final InfoItem.InfoType infoType) {
- return null != loadFromCache(serviceId, url, infoType).blockingGet();
+ public static boolean isCached(final int serviceId,
+ @NonNull final String url,
+ @NonNull final InfoCache.Type cacheType) {
+ return null != loadFromCache(serviceId, url, cacheType).blockingGet();
}
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Utils
+ //////////////////////////////////////////////////////////////////////////*/
+
/**
* 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
diff --git a/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java b/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java
index edcb565a0..bc15f3f02 100644
--- a/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java
+++ b/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java
@@ -7,6 +7,7 @@ import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
+import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class FilenameUtils {
@@ -51,7 +52,7 @@ public final class FilenameUtils {
final Pattern pattern = Pattern.compile(charset);
- return createFilename(title, pattern, replacementChar);
+ return createFilename(title, pattern, Matcher.quoteReplacement(replacementChar));
}
/**
diff --git a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java
index a07f05828..b9c91f8a5 100644
--- a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java
+++ b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java
@@ -27,7 +27,6 @@ import androidx.collection.LruCache;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.extractor.Info;
-import org.schabi.newpipe.extractor.InfoItem;
import java.util.Map;
@@ -48,14 +47,27 @@ public final class InfoCache {
// no instance
}
+ /**
+ * Identifies the type of {@link Info} to put into the cache.
+ */
+ public enum Type {
+ STREAM,
+ CHANNEL,
+ CHANNEL_TAB,
+ COMMENTS,
+ PLAYLIST,
+ KIOSK,
+ }
+
public static InfoCache getInstance() {
return INSTANCE;
}
@NonNull
- private static String keyOf(final int serviceId, @NonNull final String url,
- @NonNull final InfoItem.InfoType infoType) {
- return serviceId + url + infoType.toString();
+ private static String keyOf(final int serviceId,
+ @NonNull final String url,
+ @NonNull final Type cacheType) {
+ return serviceId + ":" + cacheType.ordinal() + ":" + url;
}
private static void removeStaleCache() {
@@ -83,19 +95,22 @@ public final class InfoCache {
}
@Nullable
- public Info getFromKey(final int serviceId, @NonNull final String url,
- @NonNull final InfoItem.InfoType infoType) {
+ public Info getFromKey(final int serviceId,
+ @NonNull final String url,
+ @NonNull final Type cacheType) {
if (DEBUG) {
Log.d(TAG, "getFromKey() called with: "
+ "serviceId = [" + serviceId + "], url = [" + url + "]");
}
synchronized (LRU_CACHE) {
- return getInfo(keyOf(serviceId, url, infoType));
+ return getInfo(keyOf(serviceId, url, cacheType));
}
}
- public void putInfo(final int serviceId, @NonNull final String url, @NonNull final Info info,
- @NonNull final InfoItem.InfoType infoType) {
+ public void putInfo(final int serviceId,
+ @NonNull final String url,
+ @NonNull final Info info,
+ @NonNull final Type cacheType) {
if (DEBUG) {
Log.d(TAG, "putInfo() called with: info = [" + info + "]");
}
@@ -103,18 +118,19 @@ public final class InfoCache {
final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId());
synchronized (LRU_CACHE) {
final CacheData data = new CacheData(info, expirationMillis);
- LRU_CACHE.put(keyOf(serviceId, url, infoType), data);
+ LRU_CACHE.put(keyOf(serviceId, url, cacheType), data);
}
}
- public void removeInfo(final int serviceId, @NonNull final String url,
- @NonNull final InfoItem.InfoType infoType) {
+ public void removeInfo(final int serviceId,
+ @NonNull final String url,
+ @NonNull final Type cacheType) {
if (DEBUG) {
Log.d(TAG, "removeInfo() called with: "
+ "serviceId = [" + serviceId + "], url = [" + url + "]");
}
synchronized (LRU_CACHE) {
- LRU_CACHE.remove(keyOf(serviceId, url, infoType));
+ LRU_CACHE.remove(keyOf(serviceId, url, cacheType));
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java
index f45f3786d..f1904565d 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java
@@ -4,6 +4,7 @@ import static org.schabi.newpipe.extractor.ServiceList.YouTube;
import android.content.Context;
import android.content.SharedPreferences;
+import android.content.res.Resources;
import android.net.ConnectivityManager;
import androidx.annotation.NonNull;
@@ -45,10 +46,10 @@ public final class ListHelper {
List.of(MediaFormat.MP3, MediaFormat.M4A, MediaFormat.WEBMA);
// Use a Set for better performance
private static final Set HIGH_RESOLUTION_LIST = Set.of("1440p", "2160p");
- // Audio track types in order of priotity. 0=lowest, n=highest
+ // Audio track types in order of priority. 0=lowest, n=highest
private static final List AUDIO_TRACK_TYPE_RANKING =
List.of(AudioTrackType.DESCRIPTIVE, AudioTrackType.DUBBED, AudioTrackType.ORIGINAL);
- // Audio track types in order of priotity when descriptive audio is preferred.
+ // Audio track types in order of priority when descriptive audio is preferred.
private static final List AUDIO_TRACK_TYPE_RANKING_DESCRIPTIVE =
List.of(AudioTrackType.ORIGINAL, AudioTrackType.DUBBED, AudioTrackType.DESCRIPTIVE);
@@ -188,13 +189,16 @@ public final class ListHelper {
/**
* Return a {@link Stream} list which only contains streams which can be played by the player.
- *
- * Some formats are not supported. For more info, see {@link #SUPPORTED_ITAG_IDS}.
- * Torrent streams are also removed, because they cannot be retrieved.
+ *
+ *
+ * Some formats are not supported, see {@link #SUPPORTED_ITAG_IDS} for more details.
+ * Torrent streams are also removed, because they cannot be retrieved, like OPUS streams using
+ * HLS as their delivery method, since they are not supported by ExoPlayer.
+ *
*
* @param the item type's class that extends {@link Stream}
* @param streamList the original stream list
- * @param serviceId
+ * @param serviceId the service ID from which the streams' list comes from
* @return a stream list which only contains streams that can be played the player
*/
@NonNull
@@ -203,6 +207,8 @@ public final class ListHelper {
final int youtubeServiceId = YouTube.getServiceId();
return getFilteredStreamList(streamList,
stream -> stream.getDeliveryMethod() != DeliveryMethod.TORRENT
+ && (stream.getDeliveryMethod() != DeliveryMethod.HLS
+ || stream.getFormat() != MediaFormat.OPUS)
&& (serviceId != youtubeServiceId
|| stream.getItagItem() == null
|| SUPPORTED_ITAG_IDS.contains(stream.getItagItem().id)));
@@ -239,6 +245,41 @@ public final class ListHelper {
videoOnlyStreams, ascendingOrder, preferVideoOnlyStreams);
}
+ /**
+ * Get a sorted list containing a set of default resolution info
+ * and additional resolution info if showHigherResolutions is true.
+ *
+ * @param resources the resources to get the resolutions from
+ * @param defaultResolutionKey the settings key of the default resolution
+ * @param additionalResolutionKey the settings key of the additional resolutions
+ * @param showHigherResolutions if higher resolutions should be included in the sorted list
+ * @return a sorted list containing the default and maybe additional resolutions
+ */
+ public static List getSortedResolutionList(
+ final Resources resources,
+ final int defaultResolutionKey,
+ final int additionalResolutionKey,
+ final boolean showHigherResolutions) {
+ final List resolutions = new ArrayList<>(Arrays.asList(
+ resources.getStringArray(defaultResolutionKey)));
+ if (!showHigherResolutions) {
+ return resolutions;
+ }
+ final List additionalResolutions = Arrays.asList(
+ resources.getStringArray(additionalResolutionKey));
+ // keep "best resolution" at the top
+ resolutions.addAll(1, additionalResolutions);
+ return resolutions;
+ }
+
+ public static boolean isHighResolutionSelected(final String selectedResolution,
+ final int additionalResolutionKey,
+ final Resources resources) {
+ return Arrays.asList(resources.getStringArray(
+ additionalResolutionKey))
+ .contains(selectedResolution);
+ }
+
/**
* Filter the list of audio streams and return a list with the preferred stream for
* each audio track. Streams are sorted with the preferred language in the first position.
@@ -259,7 +300,9 @@ public final class ListHelper {
final Comparator cmp = getAudioFormatComparator(context);
for (final AudioStream stream : audioStreams) {
- if (stream.getDeliveryMethod() == DeliveryMethod.TORRENT) {
+ if (stream.getDeliveryMethod() == DeliveryMethod.TORRENT
+ || (stream.getDeliveryMethod() == DeliveryMethod.HLS
+ && stream.getFormat() == MediaFormat.OPUS)) {
continue;
}
@@ -600,6 +643,7 @@ public final class ListHelper {
context.getString(R.string.best_resolution_key), defaultFormat, videoStreams);
}
+ @Nullable
private static MediaFormat getDefaultFormat(@NonNull final Context context,
@StringRes final int defaultFormatKey,
@StringRes final int defaultFormatValueKey) {
@@ -608,18 +652,14 @@ public final class ListHelper {
final String defaultFormat = context.getString(defaultFormatValueKey);
final String defaultFormatString = preferences.getString(
- context.getString(defaultFormatKey), defaultFormat);
+ context.getString(defaultFormatKey),
+ defaultFormat
+ );
- MediaFormat defaultMediaFormat = getMediaFormatFromKey(context, defaultFormatString);
- if (defaultMediaFormat == null) {
- preferences.edit().putString(context.getString(defaultFormatKey), defaultFormat)
- .apply();
- defaultMediaFormat = getMediaFormatFromKey(context, defaultFormat);
- }
-
- return defaultMediaFormat;
+ return getMediaFormatFromKey(context, defaultFormatString);
}
+ @Nullable
private static MediaFormat getMediaFormatFromKey(@NonNull final Context context,
@NonNull final String formatKey) {
MediaFormat format = null;
@@ -653,7 +693,7 @@ public final class ListHelper {
}
}
- private static boolean isLimitingDataUsage(final Context context) {
+ static boolean isLimitingDataUsage(@NonNull final Context context) {
return getResolutionLimit(context) != null;
}
@@ -695,7 +735,7 @@ public final class ListHelper {
/**
* Get a {@link Comparator} to compare {@link AudioStream}s by their format and bitrate.
*
- * The prefered stream will be ordered last.
+ * The preferred stream will be ordered last.
*
* @param context app context
* @return Comparator
@@ -710,7 +750,7 @@ public final class ListHelper {
/**
* Get a {@link Comparator} to compare {@link AudioStream}s by their format and bitrate.
*
- * The prefered stream will be ordered last.
+ * The preferred stream will be ordered last.
*
* @param defaultFormat the default format to look for
* @param limitDataUsage choose low bitrate audio stream
@@ -752,7 +792,7 @@ public final class ListHelper {
* Language is English
*
*
- * The prefered track will be ordered last.
+ * The preferred track will be ordered last.
*
* @param context App context
* @return Comparator
@@ -789,7 +829,7 @@ public final class ListHelper {
* Language is English
*
*
- * The prefered track will be ordered last.
+ * The preferred track will be ordered last.
*
* @param preferredLanguage Preferred audio stream language
* @param preferOriginalAudio Get the original audio track regardless of its language
@@ -834,6 +874,7 @@ public final class ListHelper {
return Comparator.comparing(AudioStream::getAudioLocale, Comparator.nullsLast(
Comparator.comparing(locale -> locale.getDisplayName(appLoc))))
- .thenComparing(AudioStream::getAudioTrackType);
+ .thenComparing(AudioStream::getAudioTrackType, Comparator.nullsLast(
+ Comparator.naturalOrder()));
}
}
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 c4034252d..0485413cc 100644
--- a/app/src/main/java/org/schabi/newpipe/util/Localization.java
+++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java
@@ -1,5 +1,7 @@
package org.schabi.newpipe.util;
+import static org.schabi.newpipe.MainActivity.DEBUG;
+
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
@@ -22,6 +24,7 @@ import org.ocpsoft.prettytime.units.Decade;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.localization.ContentCountry;
+import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.AudioTrackType;
@@ -82,7 +85,7 @@ public final class Localization {
.fromLocale(getPreferredLocale(context));
}
- public static ContentCountry getPreferredContentCountry(final Context context) {
+ public static ContentCountry getPreferredContentCountry(@NonNull final Context context) {
final String contentCountry = PreferenceManager.getDefaultSharedPreferences(context)
.getString(context.getString(R.string.content_country_key),
context.getString(R.string.default_localization_key));
@@ -92,41 +95,43 @@ public final class Localization {
return new ContentCountry(contentCountry);
}
- public static Locale getPreferredLocale(final Context context) {
+ public static Locale getPreferredLocale(@NonNull final Context context) {
return getLocaleFromPrefs(context, R.string.content_language_key);
}
- public static Locale getAppLocale(final Context context) {
+ public static Locale getAppLocale(@NonNull final Context context) {
return getLocaleFromPrefs(context, R.string.app_language_key);
}
- public static String localizeNumber(final Context context, final long number) {
+ public static String localizeNumber(@NonNull final Context context, final long number) {
return localizeNumber(context, (double) number);
}
- public static String localizeNumber(final Context context, final double number) {
+ public static String localizeNumber(@NonNull final Context context, final double number) {
final NumberFormat nf = NumberFormat.getInstance(getAppLocale(context));
return nf.format(number);
}
- public static String formatDate(final OffsetDateTime offsetDateTime, final Context context) {
+ public static String formatDate(@NonNull final Context context,
+ @NonNull final OffsetDateTime offsetDateTime) {
return DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(getAppLocale(context)).format(offsetDateTime
.atZoneSameInstant(ZoneId.systemDefault()));
}
@SuppressLint("StringFormatInvalid")
- public static String localizeUploadDate(final Context context,
- final OffsetDateTime offsetDateTime) {
- return context.getString(R.string.upload_date_text, formatDate(offsetDateTime, context));
+ public static String localizeUploadDate(@NonNull final Context context,
+ @NonNull final OffsetDateTime offsetDateTime) {
+ return context.getString(R.string.upload_date_text, formatDate(context, offsetDateTime));
}
- public static String localizeViewCount(final Context context, final long viewCount) {
+ public static String localizeViewCount(@NonNull final Context context, final long viewCount) {
return getQuantity(context, R.plurals.views, R.string.no_views, viewCount,
localizeNumber(context, viewCount));
}
- public static String localizeStreamCount(final Context context, final long streamCount) {
+ public static String localizeStreamCount(@NonNull final Context context,
+ final long streamCount) {
switch ((int) streamCount) {
case (int) ListExtractor.ITEM_COUNT_UNKNOWN:
return "";
@@ -140,7 +145,8 @@ public final class Localization {
}
}
- public static String localizeStreamCountMini(final Context context, final long streamCount) {
+ public static String localizeStreamCountMini(@NonNull final Context context,
+ final long streamCount) {
switch ((int) streamCount) {
case (int) ListExtractor.ITEM_COUNT_UNKNOWN:
return "";
@@ -153,12 +159,13 @@ public final class Localization {
}
}
- public static String localizeWatchingCount(final Context context, final long watchingCount) {
+ public static String localizeWatchingCount(@NonNull final Context context,
+ final long watchingCount) {
return getQuantity(context, R.plurals.watching, R.string.no_one_watching, watchingCount,
localizeNumber(context, watchingCount));
}
- public static String shortCount(final Context context, final long count) {
+ public static String shortCount(@NonNull final Context context, final long count) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return CompactDecimalFormat.getInstance(getAppLocale(context),
CompactDecimalFormat.CompactStyle.SHORT).format(count);
@@ -179,36 +186,58 @@ public final class Localization {
}
}
- public static String listeningCount(final Context context, final long listeningCount) {
+ public static String listeningCount(@NonNull final Context context, final long listeningCount) {
return getQuantity(context, R.plurals.listening, R.string.no_one_listening, listeningCount,
shortCount(context, listeningCount));
}
- public static String shortWatchingCount(final Context context, final long watchingCount) {
+ public static String shortWatchingCount(@NonNull final Context context,
+ final long watchingCount) {
return getQuantity(context, R.plurals.watching, R.string.no_one_watching, watchingCount,
shortCount(context, watchingCount));
}
- public static String shortViewCount(final Context context, final long viewCount) {
+ public static String shortViewCount(@NonNull final Context context, final long viewCount) {
return getQuantity(context, R.plurals.views, R.string.no_views, viewCount,
shortCount(context, viewCount));
}
- public static String shortSubscriberCount(final Context context, final long subscriberCount) {
+ public static String shortSubscriberCount(@NonNull final Context context,
+ final long subscriberCount) {
return getQuantity(context, R.plurals.subscribers, R.string.no_subscribers, subscriberCount,
shortCount(context, subscriberCount));
}
- public static String downloadCount(final Context context, final int downloadCount) {
+ public static String downloadCount(@NonNull final Context context, final int downloadCount) {
return getQuantity(context, R.plurals.download_finished_notification, 0,
downloadCount, shortCount(context, downloadCount));
}
- public static String deletedDownloadCount(final Context context, final int deletedCount) {
+ public static String deletedDownloadCount(@NonNull final Context context,
+ final int deletedCount) {
return getQuantity(context, R.plurals.deleted_downloads_toast, 0,
deletedCount, shortCount(context, deletedCount));
}
+ public static String replyCount(@NonNull final Context context, final int replyCount) {
+ return getQuantity(context, R.plurals.replies, 0, replyCount,
+ String.valueOf(replyCount));
+ }
+
+ /**
+ * @param context the Android context
+ * @param likeCount the like count, possibly negative if unknown
+ * @return if {@code likeCount} is smaller than {@code 0}, the string {@code "-"}, otherwise
+ * the result of calling {@link #shortCount(Context, long)} on the like count
+ */
+ public static String likeCount(@NonNull final Context context, final int likeCount) {
+ if (likeCount < 0) {
+ return "-";
+ } else {
+ return shortCount(context, likeCount);
+ }
+ }
+
public static String getDurationString(final long duration) {
final String output;
@@ -241,7 +270,8 @@ public final class Localization {
* @return duration in a human readable string.
*/
@NonNull
- public static String localizeDuration(final Context context, final int durationInSecs) {
+ public static String localizeDuration(@NonNull final Context context,
+ final int durationInSecs) {
if (durationInSecs < 0) {
throw new IllegalArgumentException("duration can not be negative");
}
@@ -278,7 +308,7 @@ public final class Localization {
* @param track an {@link AudioStream} of the track
* @return the localized name of the audio track
*/
- public static String audioTrackName(final Context context, final AudioStream track) {
+ public static String audioTrackName(@NonNull final Context context, final AudioStream track) {
final String name;
if (track.getAudioLocale() != null) {
name = track.getAudioLocale().getDisplayLanguage(getAppLocale(context));
@@ -298,7 +328,8 @@ public final class Localization {
}
@Nullable
- private static String audioTrackType(final Context context, final AudioTrackType trackType) {
+ private static String audioTrackType(@NonNull final Context context,
+ final AudioTrackType trackType) {
switch (trackType) {
case ORIGINAL:
return context.getString(R.string.audio_track_type_original);
@@ -314,20 +345,45 @@ public final class Localization {
// Pretty Time
//////////////////////////////////////////////////////////////////////////*/
- public static void initPrettyTime(final PrettyTime time) {
+ public static void initPrettyTime(@NonNull final PrettyTime time) {
prettyTime = time;
// Do not use decades as YouTube doesn't either.
prettyTime.removeUnit(Decade.class);
}
- public static PrettyTime resolvePrettyTime(final Context context) {
+ public static PrettyTime resolvePrettyTime(@NonNull final Context context) {
return new PrettyTime(getAppLocale(context));
}
- public static String relativeTime(final OffsetDateTime offsetDateTime) {
+ public static String relativeTime(@NonNull final OffsetDateTime offsetDateTime) {
return prettyTime.formatUnrounded(offsetDateTime);
}
+ /**
+ * @param context the Android context; if {@code null} then even if in debug mode and the
+ * setting is enabled, {@code textual} will not be shown next to {@code parsed}
+ * @param parsed the textual date or time ago parsed by NewPipeExtractor, or {@code null} if
+ * the extractor could not parse it
+ * @param textual the original textual date or time ago string as provided by services
+ * @return {@link #relativeTime(OffsetDateTime)} is used if {@code parsed != null}, otherwise
+ * {@code textual} is returned. If in debug mode, {@code context != null},
+ * {@code parsed != null} and the relevant setting is enabled, {@code textual} will
+ * be appended to the returned string for debugging purposes.
+ */
+ public static String relativeTimeOrTextual(@Nullable final Context context,
+ @Nullable final DateWrapper parsed,
+ final String textual) {
+ if (parsed == null) {
+ return textual;
+ } else if (DEBUG && context != null && PreferenceManager
+ .getDefaultSharedPreferences(context)
+ .getBoolean(context.getString(R.string.show_original_time_ago_key), false)) {
+ return relativeTime(parsed.offsetDateTime()) + " (" + textual + ")";
+ } else {
+ return relativeTime(parsed.offsetDateTime());
+ }
+ }
+
public static void assureCorrectAppLanguage(final Context c) {
final Resources res = c.getResources();
final DisplayMetrics dm = res.getDisplayMetrics();
@@ -336,7 +392,8 @@ public final class Localization {
res.updateConfiguration(conf, dm);
}
- private static Locale getLocaleFromPrefs(final Context context, @StringRes final int prefKey) {
+ private static Locale getLocaleFromPrefs(@NonNull final Context context,
+ @StringRes final int prefKey) {
final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
final String defaultKey = context.getString(R.string.default_localization_key);
final String languageCode = sp.getString(context.getString(prefKey), defaultKey);
@@ -352,8 +409,10 @@ public final class Localization {
return new BigDecimal(value).setScale(1, RoundingMode.HALF_UP).doubleValue();
}
- private static String getQuantity(final Context context, @PluralsRes final int pluralId,
- @StringRes final int zeroCaseStringId, final long count,
+ private static String getQuantity(@NonNull final Context context,
+ @PluralsRes final int pluralId,
+ @StringRes final int zeroCaseStringId,
+ final long count,
final String formattedCount) {
if (count == 0) {
return context.getString(zeroCaseStringId);
diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
index b0d7dcf73..5dee32371 100644
--- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
@@ -1,5 +1,6 @@
package org.schabi.newpipe.util;
+import static android.text.TextUtils.isEmpty;
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
import android.annotation.SuppressLint;
@@ -17,6 +18,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
@@ -29,8 +31,10 @@ import org.schabi.newpipe.RouterActivity;
import org.schabi.newpipe.about.AboutActivity;
import org.schabi.newpipe.database.feed.model.FeedGroupEntity;
import org.schabi.newpipe.download.DownloadActivity;
+import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
+import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
@@ -41,6 +45,7 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
+import org.schabi.newpipe.fragments.list.comments.CommentRepliesFragment;
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment;
@@ -476,6 +481,35 @@ public final class NavigationHelper {
item.getServiceId(), uploaderUrl, item.getUploaderName());
}
+ /**
+ * Opens the comment author channel fragment, if the {@link CommentsInfoItem#getUploaderUrl()}
+ * of {@code comment} is non-null. Shows a UI-error snackbar if something goes wrong.
+ *
+ * @param activity the activity with the fragment manager and in which to show the snackbar
+ * @param comment the comment whose uploader/author will be opened
+ */
+ public static void openCommentAuthorIfPresent(@NonNull final FragmentActivity activity,
+ @NonNull final CommentsInfoItem comment) {
+ if (isEmpty(comment.getUploaderUrl())) {
+ return;
+ }
+ try {
+ openChannelFragment(activity.getSupportFragmentManager(), comment.getServiceId(),
+ comment.getUploaderUrl(), comment.getUploaderName());
+ } catch (final Exception e) {
+ ErrorUtil.showUiErrorSnackbar(activity, "Opening channel fragment", e);
+ }
+ }
+
+ public static void openCommentRepliesFragment(@NonNull final FragmentActivity activity,
+ @NonNull final CommentsInfoItem comment) {
+ defaultTransaction(activity.getSupportFragmentManager())
+ .replace(R.id.fragment_holder, new CommentRepliesFragment(comment),
+ CommentRepliesFragment.TAG)
+ .addToBackStack(CommentRepliesFragment.TAG)
+ .commit();
+ }
+
public static void openPlaylistFragment(final FragmentManager fragmentManager,
final int serviceId, final String url,
@NonNull final String name) {
diff --git a/app/src/main/java/org/schabi/newpipe/util/RelatedItemInfo.java b/app/src/main/java/org/schabi/newpipe/util/RelatedItemInfo.java
deleted file mode 100644
index f96bb0d54..000000000
--- a/app/src/main/java/org/schabi/newpipe/util/RelatedItemInfo.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package org.schabi.newpipe.util;
-
-import org.schabi.newpipe.extractor.InfoItem;
-import org.schabi.newpipe.extractor.ListInfo;
-import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
-import org.schabi.newpipe.extractor.stream.StreamInfo;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-public class RelatedItemInfo extends ListInfo {
- public RelatedItemInfo(final int serviceId, final ListLinkHandler listUrlIdHandler,
- final String name) {
- super(serviceId, listUrlIdHandler, name);
- }
-
- public static RelatedItemInfo getInfo(final StreamInfo info) {
- final ListLinkHandler handler = new ListLinkHandler(
- info.getOriginalUrl(), info.getUrl(), info.getId(), Collections.emptyList(), null);
- final RelatedItemInfo relatedItemInfo = new RelatedItemInfo(
- info.getServiceId(), handler, info.getName());
- final List relatedItems = new ArrayList<>(info.getRelatedItems());
- relatedItemInfo.setRelatedItems(relatedItems);
- return relatedItemInfo;
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt b/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt
index 5a54b29d2..3ea19fa4f 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt
+++ b/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt
@@ -1,97 +1,39 @@
package org.schabi.newpipe.util
import android.content.pm.PackageManager
-import android.content.pm.Signature
import androidx.core.content.pm.PackageInfoCompat
import org.schabi.newpipe.App
import org.schabi.newpipe.error.ErrorInfo
import org.schabi.newpipe.error.ErrorUtil.Companion.createNotification
import org.schabi.newpipe.error.UserAction
-import java.security.MessageDigest
-import java.security.NoSuchAlgorithmException
-import java.security.cert.CertificateEncodingException
-import java.security.cert.CertificateException
-import java.security.cert.CertificateFactory
-import java.security.cert.X509Certificate
import java.time.Instant
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
object ReleaseVersionUtil {
// Public key of the certificate that is used in NewPipe release versions
- private const val RELEASE_CERT_PUBLIC_KEY_SHA1 =
- "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15"
+ private const val RELEASE_CERT_PUBLIC_KEY_SHA256 =
+ "cb84069bd68116bafae5ee4ee5b08a567aa6d898404e7cb12f9e756df5cf5cab"
- @JvmStatic
- fun isReleaseApk(): Boolean {
- return certificateSHA1Fingerprint == RELEASE_CERT_PUBLIC_KEY_SHA1
- }
-
- /**
- * Method to get the APK's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
- *
- * @return String with the APK's SHA1 fingerprint in hexadecimal
- */
- private val certificateSHA1Fingerprint: String
- get() {
- val app = App.getApp()
- val signatures: List = try {
- PackageInfoCompat.getSignatures(app.packageManager, app.packageName)
- } catch (e: PackageManager.NameNotFoundException) {
- showRequestError(app, e, "Could not find package info")
- return ""
- }
- if (signatures.isEmpty()) {
- return ""
- }
- val x509cert = try {
- val cf = CertificateFactory.getInstance("X509")
- cf.generateCertificate(signatures[0].toByteArray().inputStream()) as X509Certificate
- } catch (e: CertificateException) {
- showRequestError(app, e, "Certificate error")
- return ""
- }
-
- return try {
- val md = MessageDigest.getInstance("SHA1")
- val publicKey = md.digest(x509cert.encoded)
- byte2HexFormatted(publicKey)
- } catch (e: NoSuchAlgorithmException) {
- showRequestError(app, e, "Could not retrieve SHA1 key")
- ""
- } catch (e: CertificateEncodingException) {
- showRequestError(app, e, "Could not retrieve SHA1 key")
- ""
- }
- }
-
- private fun byte2HexFormatted(arr: ByteArray): String {
- val str = StringBuilder(arr.size * 2)
- for (i in arr.indices) {
- var h = Integer.toHexString(arr[i].toInt())
- val l = h.length
- if (l == 1) {
- h = "0$h"
- }
- if (l > 2) {
- h = h.substring(l - 2, l)
- }
- str.append(h.uppercase())
- if (i < arr.size - 1) {
- str.append(':')
- }
- }
- return str.toString()
- }
-
- private fun showRequestError(app: App, e: Exception, request: String) {
- createNotification(
- app, ErrorInfo(e, UserAction.CHECK_FOR_NEW_APP_VERSION, request)
+ @OptIn(ExperimentalStdlibApi::class)
+ val isReleaseApk by lazy {
+ @Suppress("NewApi")
+ val certificates = mapOf(
+ RELEASE_CERT_PUBLIC_KEY_SHA256.hexToByteArray() to PackageManager.CERT_INPUT_SHA256
)
+ val app = App.getApp()
+ try {
+ PackageInfoCompat.hasSignatures(app.packageManager, app.packageName, certificates, false)
+ } catch (e: PackageManager.NameNotFoundException) {
+ createNotification(
+ app, ErrorInfo(e, UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info")
+ )
+ false
+ }
}
fun isLastUpdateCheckExpired(expiry: Long): Boolean {
- return Instant.ofEpochSecond(expiry).isBefore(Instant.now())
+ return Instant.ofEpochSecond(expiry) < Instant.now()
}
/**
diff --git a/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java
index 9415135cf..75d9a3892 100644
--- a/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java
@@ -1,5 +1,7 @@
package org.schabi.newpipe.util;
+import android.content.Context;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -9,6 +11,7 @@ import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper;
+import java.util.Comparator;
import java.util.List;
public class SecondaryStreamHelper {
@@ -25,14 +28,19 @@ public class SecondaryStreamHelper {
}
/**
- * Find the correct audio stream for the desired video stream.
+ * Finds an audio stream compatible with the provided video-only stream, so that the two streams
+ * can be combined in a single file by the downloader. If there are multiple available audio
+ * streams, chooses either the highest or the lowest quality one based on
+ * {@link ListHelper#isLimitingDataUsage(Context)}.
*
+ * @param context Android context
* @param audioStreams list of audio streams
- * @param videoStream desired video ONLY stream
- * @return selected audio stream or null if a candidate was not found
+ * @param videoStream desired video-ONLY stream
+ * @return the selected audio stream or null if a candidate was not found
*/
@Nullable
- public static AudioStream getAudioStreamFor(@NonNull final List audioStreams,
+ public static AudioStream getAudioStreamFor(@NonNull final Context context,
+ @NonNull final List audioStreams,
@NonNull final VideoStream videoStream) {
final MediaFormat mediaFormat = videoStream.getFormat();
if (mediaFormat == null) {
@@ -41,33 +49,36 @@ public class SecondaryStreamHelper {
switch (mediaFormat) {
case WEBM:
- case MPEG_4:// ¿is mpeg-4 DASH?
+ case MPEG_4: // Is MPEG-4 DASH?
break;
default:
return null;
}
- final boolean m4v = (mediaFormat == MediaFormat.MPEG_4);
+ final boolean m4v = mediaFormat == MediaFormat.MPEG_4;
+ final boolean isLimitingDataUsage = ListHelper.isLimitingDataUsage(context);
- for (final AudioStream audio : audioStreams) {
- if (audio.getFormat() == (m4v ? MediaFormat.M4A : MediaFormat.WEBMA)) {
- return audio;
+ Comparator comparator = ListHelper.getAudioFormatComparator(
+ m4v ? MediaFormat.M4A : MediaFormat.WEBMA, isLimitingDataUsage);
+ int preferredAudioStreamIndex = ListHelper.getAudioIndexByHighestRank(
+ audioStreams, comparator);
+
+ if (preferredAudioStreamIndex == -1) {
+ if (m4v) {
+ return null;
+ }
+
+ comparator = ListHelper.getAudioFormatComparator(
+ MediaFormat.WEBMA_OPUS, isLimitingDataUsage);
+ preferredAudioStreamIndex = ListHelper.getAudioIndexByHighestRank(
+ audioStreams, comparator);
+
+ if (preferredAudioStreamIndex == -1) {
+ return null;
}
}
- if (m4v) {
- return null;
- }
-
- // retry, but this time in reverse order
- for (int i = audioStreams.size() - 1; i >= 0; i--) {
- final AudioStream audio = audioStreams.get(i);
- if (audio.getFormat() == MediaFormat.WEBMA_OPUS) {
- return audio;
- }
- }
-
- return null;
+ return audioStreams.get(preferredAudioStreamIndex);
}
public T getStream() {
diff --git a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java
index acd019ba0..c712157b3 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java
@@ -144,6 +144,19 @@ public final class ServiceHelper {
.orElse("");
}
+ /**
+ * @param serviceId the id of the service
+ * @return the service corresponding to the provided id
+ * @throws java.util.NoSuchElementException if there is no service with the provided id
+ */
+ @NonNull
+ public static StreamingService getServiceById(final int serviceId) {
+ return ServiceList.all().stream()
+ .filter(s -> s.getServiceId() == serviceId)
+ .findFirst()
+ .orElseThrow();
+ }
+
public static void setSelectedServiceId(final Context context, final int serviceId) {
String serviceName;
try {
diff --git a/app/src/main/java/org/schabi/newpipe/util/StateSaver.java b/app/src/main/java/org/schabi/newpipe/util/StateSaver.java
index 91dc5f35b..61fdb602f 100644
--- a/app/src/main/java/org/schabi/newpipe/util/StateSaver.java
+++ b/app/src/main/java/org/schabi/newpipe/util/StateSaver.java
@@ -27,6 +27,7 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.os.BundleCompat;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.MainActivity;
@@ -82,7 +83,8 @@ public final class StateSaver {
return null;
}
- final SavedState savedState = outState.getParcelable(KEY_SAVED_STATE);
+ final SavedState savedState = BundleCompat.getParcelable(
+ outState, KEY_SAVED_STATE, SavedState.class);
if (savedState == null) {
return null;
}
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 118b77026..7524e5413 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
@@ -23,10 +23,13 @@ import androidx.core.content.FileProvider;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
-import org.schabi.newpipe.util.PicassoHelper;
+import org.schabi.newpipe.extractor.Image;
+import org.schabi.newpipe.util.image.ImageStrategy;
+import org.schabi.newpipe.util.image.PicassoHelper;
import java.io.File;
import java.io.FileOutputStream;
+import java.util.List;
public final class ShareUtils {
private static final String TAG = ShareUtils.class.getSimpleName();
@@ -249,7 +252,7 @@ public final class ShareUtils {
// If loading of images has been disabled, don't try to generate a content preview
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
&& !TextUtils.isEmpty(imagePreviewUrl)
- && PicassoHelper.getShouldLoadImages()) {
+ && ImageStrategy.shouldLoadImages()) {
final ClipData clipData = generateClipDataForImagePreview(context, imagePreviewUrl);
if (clipData != null) {
@@ -261,6 +264,29 @@ public final class ShareUtils {
openAppChooser(context, shareIntent, false);
}
+ /**
+ * Open the android share sheet to share a content.
+ *
+ *
+ * For Android 10+ users, a content preview is shown, which includes the title of the shared
+ * content and an image preview the content, if the preferred image chosen by {@link
+ * ImageStrategy#choosePreferredImage(List)} is in the image cache.
+ *
+ *
+ * @param context the context to use
+ * @param title the title of the content
+ * @param content the content to share
+ * @param images a set of possible {@link Image}s of the subject, among which to choose with
+ * {@link ImageStrategy#choosePreferredImage(List)} since that's likely to
+ * provide an image that is in Picasso's cache
+ */
+ public static void shareText(@NonNull final Context context,
+ @NonNull final String title,
+ final String content,
+ final List images) {
+ shareText(context, title, content, ImageStrategy.choosePreferredImage(images));
+ }
+
/**
* Open the android share sheet to share a content.
*
diff --git a/app/src/main/java/org/schabi/newpipe/util/image/ImageStrategy.java b/app/src/main/java/org/schabi/newpipe/util/image/ImageStrategy.java
new file mode 100644
index 000000000..da97179b6
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/image/ImageStrategy.java
@@ -0,0 +1,195 @@
+package org.schabi.newpipe.util.image;
+
+import static org.schabi.newpipe.extractor.Image.HEIGHT_UNKNOWN;
+import static org.schabi.newpipe.extractor.Image.WIDTH_UNKNOWN;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.schabi.newpipe.extractor.Image;
+
+import java.util.Comparator;
+import java.util.List;
+
+public final class ImageStrategy {
+
+ // when preferredImageQuality is LOW or MEDIUM, images are sorted by how close their preferred
+ // image quality is to these values (H stands for "Height")
+ private static final int BEST_LOW_H = 75;
+ private static final int BEST_MEDIUM_H = 250;
+
+ private static PreferredImageQuality preferredImageQuality = PreferredImageQuality.MEDIUM;
+
+ private ImageStrategy() {
+ }
+
+ public static void setPreferredImageQuality(final PreferredImageQuality preferredImageQuality) {
+ ImageStrategy.preferredImageQuality = preferredImageQuality;
+ }
+
+ public static boolean shouldLoadImages() {
+ return preferredImageQuality != PreferredImageQuality.NONE;
+ }
+
+
+ static double estimatePixelCount(final Image image, final double widthOverHeight) {
+ if (image.getHeight() == HEIGHT_UNKNOWN) {
+ if (image.getWidth() == WIDTH_UNKNOWN) {
+ // images whose size is completely unknown will be in their own subgroups, so
+ // any one of them will do, hence returning the same value for all of them
+ return 0;
+ } else {
+ return image.getWidth() * image.getWidth() / widthOverHeight;
+ }
+ } else if (image.getWidth() == WIDTH_UNKNOWN) {
+ return image.getHeight() * image.getHeight() * widthOverHeight;
+ } else {
+ return image.getHeight() * image.getWidth();
+ }
+ }
+
+ /**
+ * {@link #choosePreferredImage(List)} contains the description for this function's logic.
+ *
+ * @param images the images from which to choose
+ * @param nonNoneQuality the preferred quality (must NOT be {@link PreferredImageQuality#NONE})
+ * @return the chosen preferred image, or {@link null} if the list is empty
+ * @see #choosePreferredImage(List)
+ */
+ @Nullable
+ static String choosePreferredImage(@NonNull final List images,
+ final PreferredImageQuality nonNoneQuality) {
+ // this will be used to estimate the pixel count for images where only one of height or
+ // width are known
+ final double widthOverHeight = images.stream()
+ .filter(image -> image.getHeight() != HEIGHT_UNKNOWN
+ && image.getWidth() != WIDTH_UNKNOWN)
+ .mapToDouble(image -> ((double) image.getWidth()) / image.getHeight())
+ .findFirst()
+ .orElse(1.0);
+
+ final Image.ResolutionLevel preferredLevel = nonNoneQuality.toResolutionLevel();
+ final Comparator initialComparator = Comparator
+ // the first step splits the images into groups of resolution levels
+ .comparingInt(i -> {
+ if (i.getEstimatedResolutionLevel() == Image.ResolutionLevel.UNKNOWN) {
+ return 3; // avoid unknowns as much as possible
+ } else if (i.getEstimatedResolutionLevel() == preferredLevel) {
+ return 0; // prefer a matching resolution level
+ } else if (i.getEstimatedResolutionLevel() == Image.ResolutionLevel.MEDIUM) {
+ return 1; // the preferredLevel is only 1 "step" away (either HIGH or LOW)
+ } else {
+ return 2; // the preferredLevel is the furthest away possible (2 "steps")
+ }
+ })
+ // then each level's group is further split into two subgroups, one with known image
+ // size (which is also the preferred subgroup) and the other without
+ .thenComparing(image ->
+ image.getHeight() == HEIGHT_UNKNOWN && image.getWidth() == WIDTH_UNKNOWN);
+
+ // The third step chooses, within each subgroup with known image size, the best image based
+ // on how close its size is to BEST_LOW_H or BEST_MEDIUM_H (with proper units). Subgroups
+ // without known image size will be left untouched since estimatePixelCount always returns
+ // the same number for those.
+ final Comparator finalComparator = switch (nonNoneQuality) {
+ case NONE -> initialComparator; // unreachable
+ case LOW -> initialComparator.thenComparingDouble(image -> {
+ final double pixelCount = estimatePixelCount(image, widthOverHeight);
+ return Math.abs(pixelCount - BEST_LOW_H * BEST_LOW_H * widthOverHeight);
+ });
+ case MEDIUM -> initialComparator.thenComparingDouble(image -> {
+ final double pixelCount = estimatePixelCount(image, widthOverHeight);
+ return Math.abs(pixelCount - BEST_MEDIUM_H * BEST_MEDIUM_H * widthOverHeight);
+ });
+ case HIGH -> initialComparator.thenComparingDouble(
+ // this is reversed with a - so that the highest resolution is chosen
+ i -> -estimatePixelCount(i, widthOverHeight));
+ };
+
+ return images.stream()
+ // using "min" basically means "take the first group, then take the first subgroup,
+ // then choose the best image, while ignoring all other groups and subgroups"
+ .min(finalComparator)
+ .map(Image::getUrl)
+ .orElse(null);
+ }
+
+ /**
+ * Chooses an image amongst the provided list based on the user preference previously set with
+ * {@link #setPreferredImageQuality(PreferredImageQuality)}. {@code null} will be returned in
+ * case the list is empty or the user preference is to not show images.
+ *
+ * These properties will be preferred, from most to least important:
+ *
+ * - The image's {@link Image#getEstimatedResolutionLevel()} is not unknown and is close
+ * to {@link #preferredImageQuality}
+ * - At least one of the image's width or height are known
+ * - The highest resolution image is finally chosen if the user's preference is {@link
+ * PreferredImageQuality#HIGH}, otherwise the chosen image is the one that has the height
+ * closest to {@link #BEST_LOW_H} or {@link #BEST_MEDIUM_H}
+ *
+ *
+ * Use {@link #imageListToDbUrl(List)} if the URL is going to be saved to the database, to avoid
+ * saving nothing in case at the moment of saving the user preference is to not show images.
+ *
+ * @param images the images from which to choose
+ * @return the chosen preferred image, or {@link null} if the list is empty or the user disabled
+ * images
+ * @see #imageListToDbUrl(List)
+ */
+ @Nullable
+ public static String choosePreferredImage(@NonNull final List images) {
+ if (preferredImageQuality == PreferredImageQuality.NONE) {
+ return null; // do not load images
+ }
+
+ return choosePreferredImage(images, preferredImageQuality);
+ }
+
+ /**
+ * Like {@link #choosePreferredImage(List)}, except that if {@link #preferredImageQuality} is
+ * {@link PreferredImageQuality#NONE} an image will be chosen anyway (with preferred quality
+ * {@link PreferredImageQuality#MEDIUM}.
+ *
+ * To go back to a list of images (obviously with just the one chosen image) from a URL saved in
+ * the database use {@link #dbUrlToImageList(String)}.
+ *
+ * @param images the images from which to choose
+ * @return the chosen preferred image, or {@link null} if the list is empty
+ * @see #choosePreferredImage(List)
+ * @see #dbUrlToImageList(String)
+ */
+ @Nullable
+ public static String imageListToDbUrl(@NonNull final List images) {
+ final PreferredImageQuality quality;
+ if (preferredImageQuality == PreferredImageQuality.NONE) {
+ quality = PreferredImageQuality.MEDIUM;
+ } else {
+ quality = preferredImageQuality;
+ }
+
+ return choosePreferredImage(images, quality);
+ }
+
+ /**
+ * Wraps the URL (coming from the database) in a {@code List} so that it is usable
+ * seamlessly in all of the places where the extractor would return a list of images, including
+ * allowing to build info objects based on database objects.
+ *
+ * To obtain a url to save to the database from a list of images use {@link
+ * #imageListToDbUrl(List)}.
+ *
+ * @param url the URL to wrap coming from the database, or {@code null} to get an empty list
+ * @return a list containing just one {@link Image} wrapping the provided URL, with unknown
+ * image size fields, or an empty list if the URL is {@code null}
+ * @see #imageListToDbUrl(List)
+ */
+ @NonNull
+ public static List dbUrlToImageList(@Nullable final String url) {
+ if (url == null) {
+ return List.of();
+ } else {
+ return List.of(new Image(url, -1, -1, Image.ResolutionLevel.UNKNOWN));
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java b/app/src/main/java/org/schabi/newpipe/util/image/PicassoHelper.java
similarity index 69%
rename from app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java
rename to app/src/main/java/org/schabi/newpipe/util/image/PicassoHelper.java
index ece0c7e87..4b116bdf9 100644
--- a/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/image/PicassoHelper.java
@@ -1,13 +1,16 @@
-package org.schabi.newpipe.util;
+package org.schabi.newpipe.util.image;
import static org.schabi.newpipe.MainActivity.DEBUG;
-import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
+import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
+import static org.schabi.newpipe.util.image.ImageStrategy.choosePreferredImage;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.BitmapCompat;
@@ -19,9 +22,11 @@ import com.squareup.picasso.RequestCreator;
import com.squareup.picasso.Transformation;
import org.schabi.newpipe.R;
+import org.schabi.newpipe.extractor.Image;
import java.io.File;
import java.io.IOException;
+import java.util.List;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
@@ -41,13 +46,12 @@ public final class PicassoHelper {
@SuppressLint("StaticFieldLeak")
private static Picasso picassoInstance;
- private static boolean shouldLoadImages;
public static void init(final Context context) {
picassoCache = new LruCache(10 * 1024 * 1024);
picassoDownloaderClient = new OkHttpClient.Builder()
.cache(new okhttp3.Cache(new File(context.getExternalCacheDir(), "picasso"),
- 50 * 1024 * 1024))
+ 50L * 1024L * 1024L))
// this should already be the default timeout in OkHttp3, but just to be sure...
.callTimeout(15, TimeUnit.SECONDS)
.build();
@@ -87,47 +91,53 @@ public final class PicassoHelper {
picassoInstance.setIndicatorsEnabled(enabled); // useful for debugging
}
- public static void setShouldLoadImages(final boolean shouldLoadImages) {
- PicassoHelper.shouldLoadImages = shouldLoadImages;
+
+ public static RequestCreator loadAvatar(@NonNull final List images) {
+ return loadImageDefault(images, R.drawable.placeholder_person);
}
- public static boolean getShouldLoadImages() {
- return shouldLoadImages;
- }
-
-
- public static RequestCreator loadAvatar(final String url) {
+ public static RequestCreator loadAvatar(@Nullable final String url) {
return loadImageDefault(url, R.drawable.placeholder_person);
}
- public static RequestCreator loadThumbnail(final String url) {
+ public static RequestCreator loadThumbnail(@NonNull final List images) {
+ return loadImageDefault(images, R.drawable.placeholder_thumbnail_video);
+ }
+
+ public static RequestCreator loadThumbnail(@Nullable final String url) {
return loadImageDefault(url, R.drawable.placeholder_thumbnail_video);
}
- public static RequestCreator loadDetailsThumbnail(final String url) {
- return loadImageDefault(url, R.drawable.placeholder_thumbnail_video, false);
+ public static RequestCreator loadDetailsThumbnail(@NonNull final List images) {
+ return loadImageDefault(choosePreferredImage(images),
+ R.drawable.placeholder_thumbnail_video, false);
}
- public static RequestCreator loadBanner(final String url) {
- return loadImageDefault(url, R.drawable.placeholder_channel_banner);
+ public static RequestCreator loadBanner(@NonNull final List images) {
+ return loadImageDefault(images, R.drawable.placeholder_channel_banner);
}
- public static RequestCreator loadPlaylistThumbnail(final String url) {
+ public static RequestCreator loadPlaylistThumbnail(@NonNull final List images) {
+ return loadImageDefault(images, R.drawable.placeholder_thumbnail_playlist);
+ }
+
+ public static RequestCreator loadPlaylistThumbnail(@Nullable final String url) {
return loadImageDefault(url, R.drawable.placeholder_thumbnail_playlist);
}
- public static RequestCreator loadSeekbarThumbnailPreview(final String url) {
+ public static RequestCreator loadSeekbarThumbnailPreview(@Nullable final String url) {
return picassoInstance.load(url);
}
- public static RequestCreator loadNotificationIcon(final String url) {
+ public static RequestCreator loadNotificationIcon(@Nullable final String url) {
return loadImageDefault(url, R.drawable.ic_newpipe_triangle_white);
}
- public static RequestCreator loadScaledDownThumbnail(final Context context, final String url) {
+ public static RequestCreator loadScaledDownThumbnail(final Context context,
+ @NonNull final List images) {
// scale down the notification thumbnail for performance
- return PicassoHelper.loadThumbnail(url)
+ return PicassoHelper.loadThumbnail(images)
.transform(new Transformation() {
@Override
public Bitmap transform(final Bitmap source) {
@@ -174,19 +184,29 @@ public final class PicassoHelper {
}
@Nullable
- public static Bitmap getImageFromCacheIfPresent(final String imageUrl) {
+ public static Bitmap getImageFromCacheIfPresent(@NonNull final String imageUrl) {
// URLs in the internal cache finish with \n so we need to add \n to image URLs
return picassoCache.get(imageUrl + "\n");
}
- private static RequestCreator loadImageDefault(final String url, final int placeholderResId) {
+ private static RequestCreator loadImageDefault(@NonNull final List images,
+ @DrawableRes final int placeholderResId) {
+ return loadImageDefault(choosePreferredImage(images), placeholderResId);
+ }
+
+ private static RequestCreator loadImageDefault(@Nullable final String url,
+ @DrawableRes final int placeholderResId) {
return loadImageDefault(url, placeholderResId, true);
}
- private static RequestCreator loadImageDefault(final String url, final int placeholderResId,
+ private static RequestCreator loadImageDefault(@Nullable final String url,
+ @DrawableRes final int placeholderResId,
final boolean showPlaceholderWhileLoading) {
- if (!shouldLoadImages || isBlank(url)) {
+ // if the URL was chosen with `choosePreferredImage` it will be null, but check again
+ // `shouldLoadImages` in case the URL was chosen with `imageListToDbUrl` (which is the case
+ // for URLs stored in the database)
+ if (isNullOrEmpty(url) || !ImageStrategy.shouldLoadImages()) {
return picassoInstance
.load((String) null)
.placeholder(placeholderResId) // show placeholder when no image should load
diff --git a/app/src/main/java/org/schabi/newpipe/util/image/PreferredImageQuality.java b/app/src/main/java/org/schabi/newpipe/util/image/PreferredImageQuality.java
new file mode 100644
index 000000000..7106359b3
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/image/PreferredImageQuality.java
@@ -0,0 +1,39 @@
+package org.schabi.newpipe.util.image;
+
+import android.content.Context;
+
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.extractor.Image;
+
+public enum PreferredImageQuality {
+ NONE,
+ LOW,
+ MEDIUM,
+ HIGH;
+
+ public static PreferredImageQuality fromPreferenceKey(final Context context, final String key) {
+ if (context.getString(R.string.image_quality_none_key).equals(key)) {
+ return NONE;
+ } else if (context.getString(R.string.image_quality_low_key).equals(key)) {
+ return LOW;
+ } else if (context.getString(R.string.image_quality_high_key).equals(key)) {
+ return HIGH;
+ } else {
+ return MEDIUM; // default to medium
+ }
+ }
+
+ public Image.ResolutionLevel toResolutionLevel() {
+ switch (this) {
+ case LOW:
+ return Image.ResolutionLevel.LOW;
+ case MEDIUM:
+ return Image.ResolutionLevel.MEDIUM;
+ case HIGH:
+ return Image.ResolutionLevel.HIGH;
+ default:
+ case NONE:
+ return Image.ResolutionLevel.UNKNOWN;
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/util/text/TextEllipsizer.java b/app/src/main/java/org/schabi/newpipe/util/text/TextEllipsizer.java
new file mode 100644
index 000000000..184b73304
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/text/TextEllipsizer.java
@@ -0,0 +1,193 @@
+package org.schabi.newpipe.util.text;
+
+import android.graphics.Paint;
+import android.text.Layout;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.text.HtmlCompat;
+
+import org.schabi.newpipe.extractor.StreamingService;
+import org.schabi.newpipe.extractor.stream.Description;
+
+import java.util.function.Consumer;
+
+
+import io.reactivex.rxjava3.disposables.CompositeDisposable;
+
+/**
+ * Class to ellipsize text inside a {@link TextView}.
+ * This class provides all utils to automatically ellipsize and expand a text
+ */
+public final class TextEllipsizer {
+ private static final int EXPANDED_LINES = Integer.MAX_VALUE;
+ private static final String ELLIPSIS = "…";
+
+ @NonNull private final CompositeDisposable disposable = new CompositeDisposable();
+
+ @NonNull private final TextView view;
+ private final int maxLines;
+ @NonNull private Description content;
+ @Nullable private StreamingService streamingService;
+ @Nullable private String streamUrl;
+ private boolean isEllipsized = false;
+ @Nullable private Boolean canBeEllipsized = null;
+
+ @NonNull private final Paint paintAtContentSize = new Paint();
+ private final float ellipsisWidthPx;
+ @Nullable private Consumer stateChangeListener = null;
+ @Nullable private Consumer onContentChanged;
+
+ public TextEllipsizer(@NonNull final TextView view,
+ final int maxLines,
+ @Nullable final StreamingService streamingService) {
+ this.view = view;
+ this.maxLines = maxLines;
+ this.content = Description.EMPTY_DESCRIPTION;
+ this.streamingService = streamingService;
+
+ paintAtContentSize.setTextSize(view.getTextSize());
+ ellipsisWidthPx = paintAtContentSize.measureText(ELLIPSIS);
+ }
+
+ public void setOnContentChanged(@Nullable final Consumer onContentChanged) {
+ this.onContentChanged = onContentChanged;
+ }
+
+ public void setContent(@NonNull final Description content) {
+ this.content = content;
+ canBeEllipsized = null;
+ linkifyContentView(v -> {
+ final int currentMaxLines = view.getMaxLines();
+ view.setMaxLines(EXPANDED_LINES);
+ canBeEllipsized = view.getLineCount() > maxLines;
+ view.setMaxLines(currentMaxLines);
+ if (onContentChanged != null) {
+ onContentChanged.accept(canBeEllipsized);
+ }
+ });
+ }
+
+ public void setStreamUrl(@Nullable final String streamUrl) {
+ this.streamUrl = streamUrl;
+ }
+
+ public void setStreamingService(@NonNull final StreamingService streamingService) {
+ this.streamingService = streamingService;
+ }
+
+ /**
+ * Expand the {@link TextEllipsizer#content} to its full length.
+ */
+ public void expand() {
+ view.setMaxLines(EXPANDED_LINES);
+ linkifyContentView(v -> isEllipsized = false);
+ }
+
+ /**
+ * Shorten the {@link TextEllipsizer#content} to the given number of
+ * {@link TextEllipsizer#maxLines maximum lines} and add trailing '{@code …}'
+ * if the text was shorted.
+ */
+ public void ellipsize() {
+ // expand text to see whether it is necessary to ellipsize the text
+ view.setMaxLines(EXPANDED_LINES);
+ linkifyContentView(v -> {
+ final CharSequence charSeqText = view.getText();
+ if (charSeqText != null && view.getLineCount() > maxLines) {
+ // Note that converting to String removes spans (i.e. links), but that's something
+ // we actually want since when the text is ellipsized we want all clicks on the
+ // comment to expand the comment, not to open links.
+ final String text = charSeqText.toString();
+
+ final Layout layout = view.getLayout();
+ final float lineWidth = layout.getLineWidth(maxLines - 1);
+ final float layoutWidth = layout.getWidth();
+ final int lineStart = layout.getLineStart(maxLines - 1);
+ final int lineEnd = layout.getLineEnd(maxLines - 1);
+
+ // remove characters up until there is enough space for the ellipsis
+ // (also summing 2 more pixels, just to be sure to avoid float rounding errors)
+ int end = lineEnd;
+ float removedCharactersWidth = 0.0f;
+ while (lineWidth - removedCharactersWidth + ellipsisWidthPx + 2.0f > layoutWidth
+ && end >= lineStart) {
+ end -= 1;
+ // recalculate each time to account for ligatures or other similar things
+ removedCharactersWidth = paintAtContentSize.measureText(
+ text.substring(end, lineEnd));
+ }
+
+ // remove trailing spaces and newlines
+ while (end > 0 && Character.isWhitespace(text.charAt(end - 1))) {
+ end -= 1;
+ }
+
+ final String newVal = text.substring(0, end) + ELLIPSIS;
+ view.setText(newVal);
+ isEllipsized = true;
+ } else {
+ isEllipsized = false;
+ }
+ view.setMaxLines(maxLines);
+ });
+ }
+
+ /**
+ * Toggle the view between the ellipsized and expanded state.
+ */
+ public void toggle() {
+ if (isEllipsized) {
+ expand();
+ } else {
+ ellipsize();
+ }
+ }
+
+ /**
+ * Whether the {@link #view} can be ellipsized.
+ * This is only the case when the {@link #content} has more lines
+ * than allowed via {@link #maxLines}.
+ * @return {@code true} if the {@link #content} has more lines than allowed via
+ * {@link #maxLines} and thus can be shortened, {@code false} if the {@code content} fits into
+ * the {@link #view} without being shortened and {@code null} if the initialization is not
+ * completed yet.
+ */
+ @Nullable
+ public Boolean canBeEllipsized() {
+ return canBeEllipsized;
+ }
+
+ private void linkifyContentView(final Consumer consumer) {
+ final boolean oldState = isEllipsized;
+ disposable.clear();
+ TextLinkifier.fromDescription(view, content,
+ HtmlCompat.FROM_HTML_MODE_LEGACY, streamingService, streamUrl, disposable,
+ v -> {
+ consumer.accept(v);
+ notifyStateChangeListener(oldState);
+ });
+
+ }
+
+ /**
+ * Add a listener which is called when the given content is changed,
+ * either from ellipsized to full or vice versa.
+ * @param listener The listener to be called, or {@code null} to remove it.
+ * The Boolean parameter is the new state.
+ * Ellipsized content is represented as {@code true},
+ * normal or full content by {@code false}.
+ */
+ public void setStateChangeListener(@Nullable final Consumer listener) {
+ this.stateChangeListener = listener;
+ }
+
+ private void notifyStateChangeListener(final boolean oldState) {
+ if (oldState != isEllipsized && stateChangeListener != null) {
+ stateChangeListener.accept(isEllipsized);
+ }
+ }
+
+}
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
index e59a3dc05..1419ac85a 100644
--- a/app/src/main/java/org/schabi/newpipe/util/text/TextLinkifier.java
+++ b/app/src/main/java/org/schabi/newpipe/util/text/TextLinkifier.java
@@ -92,7 +92,7 @@ public final class TextLinkifier {
* {@link HtmlCompat#fromHtml(String, int)}.
*
*
- * @param textView the {@link TextView} to set the the HTML string block linked
+ * @param textView the {@link TextView} to set 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
diff --git a/app/src/main/java/org/schabi/newpipe/views/player/CircleClipTapView.kt b/app/src/main/java/org/schabi/newpipe/views/player/CircleClipTapView.kt
index e3d142916..8554e7194 100644
--- a/app/src/main/java/org/schabi/newpipe/views/player/CircleClipTapView.kt
+++ b/app/src/main/java/org/schabi/newpipe/views/player/CircleClipTapView.kt
@@ -80,10 +80,10 @@ class CircleClipTapView(context: Context?, attrs: AttributeSet) : View(context,
updatePathShape()
}
- override fun onDraw(canvas: Canvas?) {
+ override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
- canvas?.clipPath(shapePath)
- canvas?.drawPath(shapePath, backgroundPaint)
+ canvas.clipPath(shapePath)
+ canvas.drawPath(shapePath, backgroundPaint)
}
}
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 009a4f4be..42ff3ca8c 100755
--- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java
+++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java
@@ -23,7 +23,6 @@ import android.os.Handler;
import android.os.Handler.Callback;
import android.os.IBinder;
import android.os.Message;
-import android.os.Parcelable;
import android.util.Log;
import android.widget.Toast;
@@ -36,6 +35,7 @@ import androidx.core.app.NotificationCompat.Builder;
import androidx.core.app.PendingIntentCompat;
import androidx.core.app.ServiceCompat;
import androidx.core.content.ContextCompat;
+import androidx.core.content.IntentCompat;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
@@ -49,6 +49,7 @@ import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.MissionRecoveryInfo;
@@ -359,29 +360,29 @@ public class DownloadManagerService extends Service {
*/
public static void startMission(Context context, String[] urls, StoredFileHelper storage,
char kind, int threads, String source, String psName,
- String[] psArgs, long nearLength, MissionRecoveryInfo[] recoveryInfo) {
- Intent intent = new Intent(context, DownloadManagerService.class);
- intent.setAction(Intent.ACTION_RUN);
- intent.putExtra(EXTRA_URLS, urls);
- intent.putExtra(EXTRA_KIND, kind);
- intent.putExtra(EXTRA_THREADS, threads);
- intent.putExtra(EXTRA_SOURCE, source);
- intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName);
- intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs);
- intent.putExtra(EXTRA_NEAR_LENGTH, nearLength);
- intent.putExtra(EXTRA_RECOVERY_INFO, recoveryInfo);
-
- intent.putExtra(EXTRA_PARENT_PATH, storage.getParentUri());
- intent.putExtra(EXTRA_PATH, storage.getUri());
- intent.putExtra(EXTRA_STORAGE_TAG, storage.getTag());
+ String[] psArgs, long nearLength,
+ ArrayList recoveryInfo) {
+ final Intent intent = new Intent(context, DownloadManagerService.class)
+ .setAction(Intent.ACTION_RUN)
+ .putExtra(EXTRA_URLS, urls)
+ .putExtra(EXTRA_KIND, kind)
+ .putExtra(EXTRA_THREADS, threads)
+ .putExtra(EXTRA_SOURCE, source)
+ .putExtra(EXTRA_POSTPROCESSING_NAME, psName)
+ .putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs)
+ .putExtra(EXTRA_NEAR_LENGTH, nearLength)
+ .putExtra(EXTRA_RECOVERY_INFO, recoveryInfo)
+ .putExtra(EXTRA_PARENT_PATH, storage.getParentUri())
+ .putExtra(EXTRA_PATH, storage.getUri())
+ .putExtra(EXTRA_STORAGE_TAG, storage.getTag());
context.startService(intent);
}
private void startMission(Intent intent) {
String[] urls = intent.getStringArrayExtra(EXTRA_URLS);
- Uri path = intent.getParcelableExtra(EXTRA_PATH);
- Uri parentPath = intent.getParcelableExtra(EXTRA_PARENT_PATH);
+ Uri path = IntentCompat.getParcelableExtra(intent, EXTRA_PATH, Uri.class);
+ Uri parentPath = IntentCompat.getParcelableExtra(intent, EXTRA_PARENT_PATH, Uri.class);
int threads = intent.getIntExtra(EXTRA_THREADS, 1);
char kind = intent.getCharExtra(EXTRA_KIND, '?');
String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME);
@@ -389,7 +390,9 @@ public class DownloadManagerService extends Service {
String source = intent.getStringExtra(EXTRA_SOURCE);
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
String tag = intent.getStringExtra(EXTRA_STORAGE_TAG);
- Parcelable[] parcelRecovery = intent.getParcelableArrayExtra(EXTRA_RECOVERY_INFO);
+ final var recovery = IntentCompat.getParcelableArrayListExtra(intent, EXTRA_RECOVERY_INFO,
+ MissionRecoveryInfo.class);
+ Objects.requireNonNull(recovery);
StoredFileHelper storage;
try {
@@ -404,15 +407,11 @@ public class DownloadManagerService extends Service {
else
ps = Postprocessing.getAlgorithm(psName, psArgs);
- MissionRecoveryInfo[] recovery = new MissionRecoveryInfo[parcelRecovery.length];
- for (int i = 0; i < parcelRecovery.length; i++)
- recovery[i] = (MissionRecoveryInfo) parcelRecovery[i];
-
final DownloadMission mission = new DownloadMission(urls, storage, kind, ps);
mission.threadCount = threads;
mission.source = source;
mission.nearLength = nearLength;
- mission.recoveryInfo = recovery;
+ mission.recoveryInfo = recovery.toArray(MissionRecoveryInfo[]::new);
if (ps != null)
ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this));
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 23f1bf6a7..31e7f663d 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
@@ -490,7 +490,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb
showError(mission, UserAction.DOWNLOAD_POSTPROCESSING, R.string.error_postprocessing_failed);
return;
case ERROR_INSUFFICIENT_STORAGE:
- msg = R.string.error_insufficient_storage;
+ msg = R.string.error_insufficient_storage_left;
break;
case ERROR_UNKNOWN_EXCEPTION:
if (mission.errObject != null) {
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 3cfa22bd9..c75269757 100644
--- a/app/src/main/java/us/shandian/giga/util/Utility.java
+++ b/app/src/main/java/us/shandian/giga/util/Utility.java
@@ -2,6 +2,8 @@ package us.shandian.giga.util;
import android.content.Context;
import android.os.Build;
+import android.os.Environment;
+import android.os.StatFs;
import android.util.Log;
import androidx.annotation.ColorInt;
@@ -26,10 +28,8 @@ import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.HttpURLConnection;
import java.util.Locale;
-import java.util.Random;
import okio.ByteString;
-import us.shandian.giga.get.DownloadMission;
public class Utility {
@@ -40,6 +40,20 @@ public class Utility {
UNKNOWN
}
+ /**
+ * Get amount of free system's memory.
+ * @return free memory (bytes)
+ */
+ public static long getSystemFreeMemory() {
+ try {
+ final StatFs statFs = new StatFs(Environment.getExternalStorageDirectory().getPath());
+ return statFs.getAvailableBlocksLong() * statFs.getBlockSizeLong();
+ } catch (final Exception e) {
+ // do nothing
+ }
+ return -1;
+ }
+
public static String formatBytes(long bytes) {
Locale locale = Locale.getDefault();
if (bytes < 1024) {
diff --git a/app/src/main/res/layout-land/list_stream_card_item.xml b/app/src/main/res/layout-land/list_stream_card_item.xml
new file mode 120000
index 000000000..70228ee1d
--- /dev/null
+++ b/app/src/main/res/layout-land/list_stream_card_item.xml
@@ -0,0 +1 @@
+../layout/list_stream_item.xml
\ No newline at end of file
diff --git a/app/src/main/res/layout/comment_replies_header.xml b/app/src/main/res/layout/comment_replies_header.xml
new file mode 100644
index 000000000..ed5ba1a10
--- /dev/null
+++ b/app/src/main/res/layout/comment_replies_header.xml
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_comments.xml b/app/src/main/res/layout/fragment_comments.xml
index b1b644d8c..2a8c747cd 100644
--- a/app/src/main/res/layout/fragment_comments.xml
+++ b/app/src/main/res/layout/fragment_comments.xml
@@ -9,7 +9,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
- tools:listitem="@layout/list_comments_item" />
+ tools:listitem="@layout/list_comment_item" />
+ android:src="@drawable/ic_pin" />
+ tools:text="Author Name, Lorem ipsum • 5 months ago" />
-
+ tools:text="@tools:sample/lorem/random[1]" />
+ android:src="@drawable/ic_heart" />
-
+ android:layout_alignParentEnd="true"
+ android:layout_marginStart="@dimen/video_item_detail_heart_margin"
+ android:minHeight="0dp"
+ tools:text="543 replies" />
diff --git a/app/src/main/res/layout/list_comments_mini_item.xml b/app/src/main/res/layout/list_comments_mini_item.xml
deleted file mode 100644
index 606a237c5..000000000
--- a/app/src/main/res/layout/list_comments_mini_item.xml
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/playlist_header.xml b/app/src/main/res/layout/playlist_header.xml
index 9c038db3a..c761240d9 100644
--- a/app/src/main/res/layout/playlist_header.xml
+++ b/app/src/main/res/layout/playlist_header.xml
@@ -80,10 +80,32 @@
tools:text="234 videos" />
+
+
+
+
+ android:layout_below="@id/playlist_description_read_more">
-
+ android:id="@+id/summary"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp"
+ android:clickable="false"
+ android:focusable="false"
+ android:gravity="center"
+ android:text="@string/notification_actions_summary"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.0"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/app/src/main/res/menu/menu_recaptcha.xml b/app/src/main/res/menu/menu_recaptcha.xml
index 89fa024d0..fd614df32 100644
--- a/app/src/main/res/menu/menu_recaptcha.xml
+++ b/app/src/main/res/menu/menu_recaptcha.xml
@@ -5,6 +5,6 @@
diff --git a/app/src/main/res/values-ar-rLY/strings.xml b/app/src/main/res/values-ar-rLY/strings.xml
index 1fd5b15c9..c57ead36a 100644
--- a/app/src/main/res/values-ar-rLY/strings.xml
+++ b/app/src/main/res/values-ar-rLY/strings.xml
@@ -2,17 +2,860 @@
استخدم مشغل فيديو خارجي
مشاركة مع
- اعدادات
+ الإعدادات
بحث
تنزيل ملف البث
- تحميل
- مشاركه
+ تنزيل
+ إعادة النشر
فتح في نافدة منبثقة
افتح في المتصفح
إلغاء
تثبيت
- لم يتم العثور على مشغل بث (يمكنك تثبيت VLC لتشغيله).
+ لم يتم العثور على مشغل بث (يمكنك تثبيت VLC لتشغيلها).
لم يتم العثور على مشغل بث. يرجى تثبيت VLC؟
تم النشر في %1$s
- انقر على \"بحث\" للبدء
+ اضغط على عدسة المكبرة للبدء.
+ جودة الصورة
+ لا تتوفر تدفقات فيديو للاعبين الخارجيين
+ تعطيل تحديد النص في الوصف
+
+ - لم يتم حذف أي تنزيل
+ - تم حذف تنزيل واحد
+ - تم حذف تنزيلَيْن
+ - تم حذف %1$s تنزيلات
+ - تم حذف %1$s تنزيلاً
+ - تم حذف %1$s تنزيل
+
+ زر الإجراء الثاني
+ الافتراضي
+ الفيديوهات
+ إلغاء
+ تطبيق مجاني خفيف البث على أندرويد.
+ التاريخ
+ إيقاف التحميل مؤقتًا
+ قم بقص الصورة المصغرة للفيديو الموضحة في الإشعار من نسبة العرض إلى الارتفاع 16: 9 إلى 1: 1
+ التحديثات
+ تعليق مثبت
+ إزالة ما تمت مشاهدته
+ استرد
+ قم بتشغيل \"وضع تقييد المحتوى\" في يوتيوب
+
+ - لا ساعة
+ - ساعة واحدة
+ - ساعتان
+ - %d ساعات
+ - %d ساعة
+ - %d ساعة
+
+ حذف سجل المشاهدة بالكامل؟
+ تشغيل بواسطة كودي
+ تم حذف عنصر واحد.
+ ضع في اعتبارك أن هذه العملية يمكن أن تكون مكلفة اذا كنت تستخدم بيانات اشتراك انترنت.
+\n
+\nهل تريد الاستمرار؟
+ إغلاق
+ يأخذ مشروع NewPipe خصوصيتك على محمل الجد. لذلك، لا يجمع التطبيق أي بيانات دون موافقتك.
+\nتوضح سياسة خصوصية NewPipe بالتفصيل البيانات التي يتم إرسالها وتخزينها عند إرسال تقرير الأعطال.
+
+ - لاشيء مُحدّد
+ - واحدة محددة
+ - إثنتان محدَّدتان
+ - %d محددة
+ - %d محددة
+ - %d محددة
+
+ لا يوجد بث متاح للتنزيل
+ هل تريد استعادة الإعدادات الافتراضية؟
+ تحميل المحتوى المطلوب
+ إضافة نموذج
+ تراخيص الجهات الخارجية
+ حذف كل مواقف التشغيل
+ عن التطبيق والأسئلة الشائعة
+ استيراد الاشتراكات أو تصديرها من القائمة المكونة من 3 نقاط
+ الملفات المحملة
+ لا يوجد مثل هذا الملف/مصدر المحتوى
+ الأكثر إعجابًا
+ بليون
+ تعذر تحميل موجز \'%s\'.
+ ؟
+ التحقق من وجود تحديثات
+ مثيلات خوادم پيرتيوب
+ +100 فيديو
+ ألف
+ مثيل الخادم موجود بالفعل
+ طلب تأكيد قبل مسح قائمة الانتظار
+ المشتركون
+ تمت مشاهدتها جزئيا
+ إضافة إلى
+ رابط غير مدعوم
+ تبرَّع
+ الحصول على إشعار
+ إذاعة
+
+ - لا تنزيل اكتَمَل
+ - اكتَمَل تنزيل واحد
+ - تنزيلان اكتَمَلَا
+ - %s تنزيلات اكتَمَلت
+ - %s تنزيلًا اكتَمَل
+ - %s تنزيل اكتَمَل
+
+ سرعة الأداء
+ إزالة الإشارة المرجعية
+ تم إنشاء قائمة التشغيل
+ وحدة التخزين الخارجية غير متوفرة
+ عرض المعلومات
+ الشائعة
+ تم إلغاء الاشتراك في القناة
+ تخزين طلبات البحث محليًّا
+ تلوين الإشعارات
+ توليد تلقائي
+ لا يمكن التحقق من صحة المثال
+ عرض خيار التشغيل بواسطة كودي
+ الوضع السريع
+ إظهار الموجزات التالية
+ معظم الأحرف الخاصة
+ إزالة جميع بيانات صفحات الويب المخزنة مؤقّتًا
+ محو سجل البحث
+ إظهار تفاصيل القناة
+
+ - لا مُشاهِد
+ - مُشاهِد واحد
+ - مُشاهِدان
+ - %s مُشاهِدين
+ - %s مُشاهِدًا
+ - %s مُشاهِد
+
+ عرض تلميح عند الضغط على زر استخدام المشغل الخلفي أو النافذة المنبثقة في صفحة تفاصيل الفديو
+ نعم، ومقاطع الفيديو التي تمت مشاهدتها جزئيًا
+ انتهى وقت الاتصال
+ غير معروف
+ تشغيل تلقائي
+ الإجراء الافتراضي عند فتح المحتوى — %s
+ إعدادات الصوت
+ إشعارات أحداث البث الجديدة
+ الحد الأقصى لعدد المحاولات قبل إلغاء التحميل
+ تفضيل قائمة التشغيل
+ الألسنة التي سيتم عرضها على صفحات القناة
+ إزالة التكرارات؟
+ تعليقك (باللغة الإنجليزية):
+ تصدير إلى
+ حساب التجزئة
+ إظهار/إخفاء الموجزات
+ مِن الأفضل فتحه بـ
+ الخصوصيّة
+ لا يمكن الكتابة فوق الملف
+ الفيديوهات
+ اسحب لإعادة ترتيب
+ إظهار \"تعطل المشغل\"
+ يمكنك الآن تحديد نص داخل الوصف. لاحظ أن الصفحة قد تومض وقد لا تكون الروابط قابلة للنقر أثناء وضع التحديد.
+ تم وضع التالي على قائمة الانتظار
+ اسحب العناصر لإزالتها
+ معلومات:
+ حساب منشئ المحتوى قد تم إنهائه.
+\nلن يتمكن NewPipe من تحميل هذه الخلاصة في المستقبل.
+\nهل تريد إلغاء الاشتراك من هذه القناة؟
+ تصغير إلى مشغل منبثق
+ تم إغلاق NewPipe أثناء العمل على الملف
+ مشغل الخلفية
+ وصفي
+ تذكر خصائص النوافذ المنبثقة
+ الإبلاغ عن طريق البريد الإلكتروني
+ عذرًا، لم ينبغِ أن يحدث ذلك.
+ إنشاء قائمة تشغيل جديدة
+ حدث خطأٌ ما: %1$s
+ لم يتم العثور على نتائج
+ فتح الموقع
+
+ - لا موجَزات
+ - موجَز واحد
+ - موجَزان
+ - %s موجَزات
+ - %s موجَزًا
+ - %s موجز
+
+ وضع الجهاز اللوحي
+ تعذر التعرف على الرابط. فتح باستخدام تطبيق آخر؟
+ تم مسح ملفات تعريف الارتباط reCAPTCHA
+ التفاصيل
+ تشغيل قائمة الانتظار
+ تعيين كصورة مصغرة لقائمة التشغيل
+ العدد الأقصى للمحاولات
+ عذرًا، حدث خطأ ما.
+ عرض على GitHub
+ التفاصيل:
+ تردد الصوت
+ هل تعتقد أن تحميل التغذية بطيءٌ جدًا؟ إذا كان الأمر كذلك، فحاول تمكين التحميل السريع (يمكنك تغييره في الإعدادات أو بالضغط على الزر أدناه).
+\n
+\nيقدم NewPipe استراتيجيتين لتحميل الخلاصة:
+\n• جلب قناة الاشتراك بأكملها، وهي بطيئة ولكنها كاملة.
+\n• استخدام نقطة نهاية خدمة مخصصة، وهي سريعة ولكنها عادةً لا تكتمل.
+\n
+\nالفرق بين الاثنين هو أن العنصر السريع عادة ما يفتقر إلى بعض المعلومات، مثل مدة العنصر أو نوعه (لا يمكن التمييز بين مقاطع الفيديو المباشرة والأخرى العادية) وقد يعيد عناصر أقل.
+\n
+\nيوتيوب هو مثال على الخدمة التي تقدمها هذه طريقة سريعة مع تغذية RSS الخاصة بها.
+\n
+\nلذا فإن الاختيار يتلخص في ما تفضله: السرعة أو المعلومات الدقيقة.
+ تم حذف الملف
+ تنزيل
+ صفحة القناة
+ تصدير السجل، وقوائم تشغيل، والإعدادات، والاشتراكات
+ هل تريد حذف قائمة التشغيل هذه؟
+ اللغة الإفتراضية للمحتوى
+ جاري المعالجة ... قد يستغرق لحظة
+ قراءة الرخصة
+ ألسنة القنوات
+ معاينة مصغرة على شريط التمرير
+ ما الجديد
+ آخر تحديث للموجز: %s
+ الخطأ
+ زر الإجراء الخامس
+ جديد وساخن
+ استعمال التقديم السريع الغير دقيق
+ التنزيل
+ امنح الإذن بالعرض فوق التطبيقات الأخرى
+ داخلي
+ مستوى الصوت
+ إزالة التكرارات
+ تبديل الكل
+ فشلت المعالجة الاولية
+ انقر للحصول على التفاصيل
+ اختر الاقتراحات التي تريد إظهارها عند البحث
+ إنشاء
+ الحروف والأرقام
+ هل تقصد \"%1$s\"؟
+ عناصر الموجز الجديدة
+ المميزة
+ عرض المحتوى الذي يُحتمل أن يكون غير مناسب للأطفال لأن له حدًا عمريًا (مثل 18+)
+ بدأ التشغيل في الخلفية
+ ستتغير اللغة بمجرد إعادة تشغيل التطبيق
+ القصيرة
+ قوائم التشغيل
+ تنظيف
+ اختيار مثيل
+ كلها
+ مجموعات القنوات
+ جارٍ معالجة الموجز…
+
+ - لا فيديو
+ - فيديو واحد
+ - فيديوهاتان
+ - %s فيديوهات
+ - %s فيديوهات
+ - %s فيديو
+
+ جودة منخفضة (أصغر)
+ إظهار خطأ
+ عرض دقّات أعلى
+ تحتوي قوائم التشغيل رمادية اللون بالفعل على هذا العنصر.
+ تمت إضافتها إلى قائمة التشغيل
+
+ - اليوم
+ - منذ يوم واحد
+ - منذ يومَين
+ - منذ %d أيام
+ - منذ %d يومًا
+ - منذ %d يوم
+
+ تقرير على GitHub
+ أسود
+ بث جديد
+ معرفك, soundcloud.com/هويتك
+ ضغط مطول للإدراج الى قائمة الانتظار
+ خاص
+ اغلق التطبيق قسريا
+ تعذر استيراد الاشتراكات
+ إيقاف مؤقت
+ تعذر تحميل التعليقات
+ اقتراحات البحث عن بعد
+ استخدم ميزة فك ترميز وحدة فك التشفير الاحتياطية في ExoPlayer
+ يلغي السجل الحالي والاشتراكات وقوائم التشغيل والإعدادات (اختياريًا)
+ تعطل التطبيق / واجهة المستخدم
+ إعادة التسمية
+ لم يتبقى مساحة في الجهاز
+ تعذر إعداد قائمة التنزيل
+ اختر مجلد التنزيل لملفات الفيديو
+ تم تعطيل الإشعارات
+ هل تريد حذف هذه المجموعة؟
+ المظهر
+ تشغيل
+ يحذف تاريخ البحث عن الكلمات الرئيسية
+ التنبيهات المتعلقة بالإبلاغ عن الأخطاء
+ نسخة احتياطية
+ حدث خطأ للمشغل غير قابل للاسترداد
+ زر الإجراء الرابع
+ القادمة
+ [غير معروف]
+ الحُلّة الليلية
+ داكن
+ المحتوى الإفتراضي حسب البلد
+ رافع الصورة الرمزية المصغرة
+ بدون
+ استمر عند إنهاء قائمة التشغيل (الغير المتكررة) من خلال إلحاق التدفق المرتبط
+ ملء الشاشة
+ لم يُختر أي اشتراك
+
+ - لا مشارك
+ - مٌشارِك واحد
+ - مُشارِكان
+ - %s مشارِكين
+ - %s مشارِكون
+ - %s مشارك
+
+ جلب البيانات الوصفية…
+ إظهار مؤشرات الصور
+ انقر للتنزيل %s
+ تعطيل الوضع السريع
+ ,
+ صفحة قائمة التشغيل
+ تبديل تدوير الشاشة
+ تحديث NewPipe متاح!
+ تم حذف سجل البحث
+ قم بإيقاف التشغيل لإخفاء مربعات المعلومات الوصفية بمعلومات إضافية حول منشئ البث أو محتوى البث أو طلب البحث
+ إشعارات لتقدم تجزئة الفيديو
+ تحطيم المشغل
+ الصوت
+ بدء التنزيلات
+ مشترك
+ لا يمكن إنشاء المجلد الوجهة
+ يتم تخزين الملفات الصوتية التي تم تنزيلها هنا
+ لا شيء
+ الإطلاع على سياسة الخصوصية
+ يوفر YouTube \"وضع تقييد المحتوى\" الذي يخفي المحتوى المحتمل للكبار
+ عطّله لإخفاء التعليقات
+ واجه NewPipe خطأ، اضغط للتقرير
+ الكشك الافتراضي
+ اقتراحات البحث
+ تسريع إلى الأمام أثناء الصمت
+ إدارة بعض إعدادات ExoPlayer. تتطلب هذه التغييرات إعادة تشغيل المشغل لتصبح سارية المفعول
+ تمت إضافة وقت (أوقات) مكررة %d
+ فقط بعض الأجهزة تدعم تشغيل مقاطع فيديو 2K/4K
+ استيراد قاعدة البيانات
+ جودة منخفضة
+ موضع الألسنة الرئيسية
+ استخدم مشغل صوت خارجي
+ تعذر تغيير حالة الاشتراك
+ أحذف
+ فصول
+ استخدام منتقي مجلد النظام (SAF)
+ متوفر في بعض الخدمات، وعادةً ما يكون أسرع بكثير ولكن قد يُرجع كمية محدودة من العناصر وغالبًا ما تكون معلومات غير مكتملة (مثلًا بدون مدة أو نوع عنصر أو حالة مباشرة)
+ اجعل أندرويد يخصص لون الإشعار وفقا للّون الرئيسي في الصورة المصغرة (لاحظ أن هذا غير متوفر على جميع الأجهزة)
+ يتم التشغيل في الخلفية
+ الخادم لا يقوم بإرسال البيانات
+ سياسة خصوصية NewPipe
+ طلب اختبار الكابتشا مطلوب
+ إذا كنت تواجه مشكلة في استخدام التطبيق ، فتأكد من مراجعة هذه الإجابات للأسئلة الشائعة!
+ التحميلات
+ هل تريد إزالة جميع التدفقات المكررة في قائمة التشغيل هذه؟
+ قد تتسبب مراقبة تسرب الذاكرة في عدم استجابة التطبيق عند تفريغ السجلات
+ تم فقد التقدم بسبب حذف الملف
+ التسمية
+ الانتقال إلى وضع ملئ الشاشة
+ لا يمكن الاتصال بالخادم
+ إظهار تسرب الذاكرة
+
+ - صفر ثانية
+ - ثانية واحدة
+ - ثانيتان
+ - %d ثوانٍ
+ - %d ثانية
+ - %d ثانية
+
+ سيتم سؤالك عن مكان حفظ كل تنزيل.
+\nتمكين منتقي مجلد النظام (SAF) إذا كنت تريد التنزيل إلى بطاقة SD خارجية
+ استئناف التشغيل
+ جودة غير معروفة
+ هذا المحتوى غير متوفر في بلدك.
+ خطأ في الشبكة
+ محتوى الشاشة الرئيسية
+ إيقاف
+ الوسوم
+ المضيف
+ ابحث عن مثيلات الخوادم التي تناسبك على %s
+ مطلوب اتصال الشبكة
+ تشغيل التحقق من وجود تدفقات جديدة
+ دون مشاهدات
+ بدأ التشغيل في نافذة منبثقة
+ قائمة انتظار تلقائيّة
+ غير محمل: %d
+ قائمة الانتظار
+ هذا الفيديو مقيد بالفئة العمرية.
+\nنظرًا لسياسات YouTube الجديدة المتعلقة بمقاطع الفيديو المقيدة بالفئة العمرية، لا يمكن لـ NewPipe الوصول إلى أي من تدفقات الفيديو الخاصة به، وبالتالي لا يتمكن من تشغيلها.
+ إضافة إلى قائمة التشغيل
+ القنوات
+ لا تظهر
+ أدخل عنوان للمثيل
+ ماذا:\\nطلب:\\nلغة المحتوى:\\nبلد المحتوى:\\nلغة التطبيق:\\nالخدمات:\\nتوقيت جرينتش:\\nالحزمة:\\nالإصدار:\\nOS نسخة:
+ يزيل الصوت في بعض الجودات
+ جلب ألسنة القنوات
+ جودة عالية (أكبر)
+ أسئلة مكررة
+ لم يتم تعيين مجلد التحميل، الرجاء اختيار مجلد التحميل الافتراضي الآن
+ عتبة تحديث الموجز
+ يمكنك اختيار نسقك الليلي المفضل أدناه
+ أنت تقوم بتشغيل أحدث إصدار من NewPipe
+ احذف
+ رفض
+ ابدأ
+ تحليل
+ إظهار الوصف
+ مرة واحدة فقط
+ الصور الرمزية
+ معدل البحث
+ قم بتمكين هذا الخيار إذا كانت لديك مشكلات في تهيئة وحدة فك التشفير ، والتي تعود إلى أجهزة فك التشفير ذات الأولوية الأقل إذا فشلت تهيئة وحدات فك التشفير الأولية. قد ينتج عن ذلك أداء تشغيل ضعيف مقارنة باستخدام وحدات فك التشفير الأساسية
+ تم محو ذاكرة التخزين المؤقتّة للبيانات الوصفيّة
+ وضع علامة تمت مشاهدته
+ اجراء الإيماءة اليمنى
+ الرموز المسموح بها في أسماء الملفات
+ %1$s %2$s
+ آخر ما تم تشغيله
+ استخدم دائمًا الحل البديل لإعداد سطح إخراج فيديو ExoPlayer
+ البث التالي
+ تم تعطيل نفق وسائل الإعلام عن طريق التقصير على جهازك لأن نموذج جهازك معروف بأنه لا يدعمه.
+ الصور الرمزية للقناة الفرعية
+ بث مباشر
+ هل تريد أيضا استيراد الإعدادات؟
+ امسح البيانات الوصفيّة المخزّنة مؤقّتًا
+ حذف الملفات المحملة
+ يقوم هذا الحل البديل بتحرير وإعادة إنشاء نماذج برامج ترميز الفيديو عند حدوث تغيير في السطح، بدلا من تعيين السطح إلى برنامج الترميز مباشرة. تم استخدام هذا الإعداد بالفعل بواسطة ExoPlayer على بعض الأجهزة التي تعاني من هذه المشكلة ، وهذا الإعداد له تأثير فقط على Android 6 والإصدارات الأحدث
+\n
+\nقد يؤدي تمكين هذا الخيار إلى منع أخطاء التشغيل عند تبديل مشغل الفيديو الحالي أو التبديل إلى وضع ملء الشاشة
+ موافق
+ تتبّع مقاطع الفيديو التي تمّت مشاهدتها
+ لم يتم العثور على أي بث صوتي
+ تم تغيير الصورة المصغرة لقائمة التشغيل.
+ توقف
+ فاتح
+ التاريخ
+ تشغيل في وضع منبثق
+ كتم الصوت
+ هذا الإذن مطلوب
+\nللفتح في وضع النافذة المنبثقة
+ الموجز
+ ستتم إزالة مقاطع الفيديو التي تمت مشاهدتها قبل وبعد إضافتها إلى قائمة التشغيل.
+\nهل أنت واثق؟ لا يمكن التراجع عن هذا!
+ عدم الإعجاب
+ مشاهدة على الموقع
+ هذا الخيار متاح فقط إذا تم تحديد %s للسمة
+ إعادة المحاولة
+ المشغل
+ الفنانين
+ بدأ التنزيل
+ يجب أن يكون هناك مسار صوتي موجود بالفعل في هذا البث
+ هذا المحتوى خاص، لذلك لا يمكن دفقه أو تنزيله بواسطة NewPipe.
+ الدقة الافتراضية للنوافذ المنبثقة
+ تم إفراغ مساحة ذاكرة التخزين المؤقتة الخاصة بالصور
+ إشعار الإبلاغ عن الأخطاء
+ لغة التطبيق
+ تحميل الموجز…
+ التحقق يدويا من وجود إصدارات جديدة
+ لم يتم العثور على مدير ملفات مناسب لهذا الإجراء.
+\nالرجاء تثبيت مدير ملفات متوافق مع إطار عمل الوصول إلى التخزين
+ إظهار خيار تعطل عند استخدام المشغل
+ يرجى مراجعة ما إذا توجد بالفعل مشكلة تناقش التحطم الموجود بالفعل. عند إنشاء تذاكر مكررة، فإنك تستغرق منا وقتًا يمكن أن نقضيه في إصلاح الخطأ الفعلي.
+ فتح قائمة انتظار التسغيل
+ قم بتحرير كل إشعار أدناه من خلال النقر عليه. حدد ما يصل إلى ثلاثة منها لتظهر في الإشعار المضغوط باستخدام مربعات الاختيار الموجودة على اليمين
+ ماذا حدث:
+ ترجمات نصية توضيحية
+ الصورة الرمزية للقناة
+ جلب من موجز مخصص عندما يكون متاحًا
+ لا يمكن أن يكون اسم الملف فارغًا
+ حدد المسار الصوتي الأصلي بغض النظر عن اللغة
+ الإشعارات
+ الفيديو والصوت
+ جديد
+ إزالة مقاطع الفيديو التي تمت مشاهدتها؟
+ الالبومات
+ المسارات
+ حدد مثيلات PeerTube المفضلة لديك
+ إشعار المشغل
+ خاصية التقديم الغير دقيق تسمح للمشغل بالقفز خلال الفديو بشكل أسرع مع دقة قفز أقل. خاصية القفز ل5، 15 او 25 لا تعمل مع القفز الغير دقيق
+ متوقف
+ نافذة منبثقة
+ قم باستيراد ملف تعريف SoundCloud عن طريق كتابة عنوان URL أو معرفك:
+\n
+\n1. تمكين \"وضع سطح المكتب\" في متصفح الويب (الموقع غير متاح للأجهزة المحمولة)
+\n2. انتقل إلى عنوان URL هذا: %1$s
+\n3. تسجيل الدخول عندما يطلب منك
+\n4. انسخ عنوان URL للملف الشخصي الذي تمت إعادة توجيهك إليه.
+ اختر قائمة تشغيل
+ الحد العمري
+ لا يوجد تطبيق على جهازك يمكنه فتح هذا
+ يُرجى الإنتظار…
+ تبديل الخدمة، المحدد حاليًا:
+ سجل المشاهدة
+ لا تقم بتحميل الصور
+ الإخطار بأحداث البث الجديدة من الاشتراكات
+ اسم الملف
+ الملف غير موجود أو الإذن بالقراءة أو الكتابة إليه غير موجود
+ عرض مقاطع الفيديو \"التالية\" و \"المشابهة\"
+ الوقت بعد التحديث الأخير قبل اعتبار الاشتراك قديمًا — %s
+ استرداد المشغل من الخطأ
+ عالية الجودة
+ حدد الجودة للمشغلين الخارجيين
+ أُعجب بها منشئ المحتوى
+ عن
+ عرض نتائج ل: %s
+ افتح باستخدام
+ هل تريد حذف هذا العنصر من سجل البحث؟
+ الأكثر تشغيلا
+ عرض الوقت الأصلي على العناصر
+ استعادة مِن
+ الدقة الافتراضية
+ استئناف التشغيل
+ تقديم المحتوى
+ قد يؤدي التبديل من مشغل إلى آخر إلى استبدال قائمة الانتظار الخاصة بك
+ يتم تخزين ملفات الفيديو التي تم تنزيلها هنا
+ إشعار تجزئة الفيديو
+ حدد مسار الصوت للمشغلات الخارجية
+ استخدم الصورة المصغرة لكل من خلفية شاشة القفل والإشعارات
+ تراجع
+ تنسيق غير معروف
+ الألبومات
+ لا توجد تدفقات صوتية متاحة للمشغلات الخارجية
+ محلي
+ إعادة المحتوى
+ سيميتون
+ إعادة التشغيل
+ يوجد ملف بهذا الاسم مسبقاً
+ قم بإيقاف التشغيل لإخفاء وصف الفيديو والمعلومات الإضافية
+ صيغة الفيديو الافتراضية
+ زر الإجراء الثالث
+ لا يمكن التنزيل على بطاقة SD الخارجية. هل تريد إعادة تعيين موقع مجلد التنزيل؟
+ تعذر تحميل كافة الصور المصغرة
+ التيارات ذات الصلة
+ لا يمكن استرداد هذا التنزيل
+ إظهار الاشتراكات غير المجمعة فقط
+ حذف محفوظات البثوث التي تم تشغيلها ومواقف التشغيل
+ في قائمة الانتظار
+ تمكين الوضع السريع
+ مدبلجة
+ اقتراحات البحث المحلية
+ اقتصاص الصورة المصغرة إلى نسبة العرض إلى الارتفاع 1:1
+ تم تعطيل التعليقات
+ موقع الويب
+ سجل البحث
+ فتح الدرج
+ ∞ فيديو
+ الخادم لا يقبل التنزيل المتعدد، إعادة المحاولة مع @string/msg_threads = 1
+ إعادة تعيين
+ تذكر آخر مكان وحجم للنافذة المنبثقة
+ تمت عملية التصدير
+ تعذر تحليل الموقع
+ الألسنة التي سيتم جلبها عند تحديث الموجز. ليس لهذا الخيار أي تأثير إذا تم تحديث القناة باستخدام الوضع السريع.
+ قم بإنشاء تنبيه بالخطأ
+ إشعارات لإصدار NewPipe الجديد
+ استعادة الضبط الافتراضي
+ المقاطعة على الشبكات المقيسة
+ هذا سوف يُزيل إعداداتك الحالية.
+
+ - لا مشاهدة
+ - مشاهدة واحدة
+ - مُشاهَدتان
+ - %s مُشاهَدات
+ - %s مُشاهَدةً
+ - %s مُشاهَدة
+
+ إستيراد
+ الأحداث
+ حذف مواقف التشغيل
+ زر الإجراء الأول
+ لاشيء
+ تلقائي (سمة الجهاز)
+ يتم الآن استخدام الألسنة الافتراضية بعد حدوث خطأ أثناء قراءة الألسنة المحفوظة
+ تعديل مشغل نص التسمية التوضيحية وأنماط الخلفية. يتطلب إعادة تشغيل التطبيق لتصبح التغييرات سارية المفعول
+ المحتوى غير متوفر
+ دائماً
+ حدد مسارًا صوتيًا يحتوي على أوصاف للأشخاص ضعاف البصر إذا كان ذلك متاحًا
+ لا حدود
+ مدة, تشغيل الفيديو:
+ محو سجل المشاهدة
+ إجراء الإيماءة اليسرى
+ لاتوجد فيديوهات
+ تنبيهات NewPipe
+ محو جميع الملفات التي تم تنزيلها من القرص؟
+ اضغط على \"تم\" عند حلها
+ لا أحد يستمع
+ تكرار
+ إضافة إلى قائمة الانتظار
+ يقوم نيوبايب بالتنزيل
+ النوعية متوسطة
+ يسمح \"Storage Access Framework\" بالتنزيل على بطاقة SD خارجية
+ تنبيه: تعذر استيراد كافة الملفات.
+ مجلد الصوتيات المحفوظة
+ الإشارات المرجعية
+ حذف كل مواقف التشغيل؟
+ تفضل الصوت الوصفي
+ خلط
+ بدء التشغيل تلقائياً — %s
+ إغلاق الدرج
+ هذا المحتوى ليس مدعومًا من قبل NewPipe.
+\n
+\nنأمل أن يكون مدعومًا في التحديثات القادمة.
+
+ - لا دقيقة
+ - دقيقة واحدة
+ - دقيقتان
+ - %d دقائق
+ - %d دقيقة
+ - %d دقيقة
+
+ المشغل المنبثق
+ الصورة الرمزية للرفع
+ التوقيع
+ محتوى مقيد للبالغين
+ متابعة التشغيل بعد المقاطعات (مثل المكالمات الهاتفية)
+ عملية الاستعادة جارية …
+ قم بتغيير حجم الفاصل الزمني للتحميل على المحتويات التدريجية (حاليا %s). قد تؤدي القيمة المنخفضة إلى تسريع التحميل الأولي
+ تفضيل الصوت الأصلي
+ الإعجابات
+ خطأ
+ اختر مجلد التنزيل للملفات الصوتية
+ استيراد اشتراكات YouTube من Google takeout:
+\n
+\n1. انتقل إلى عنوان URL هذا: %1$s
+\n2. تسجيل الدخول عند سؤالك
+\n3. انقر على \"جميع البيانات المدرجة\"، ثم على \"إلغاء تحديد جميع\"، ثم حدد فقط \"الاشتراكات\" وانقر على \"موافق\"
+\n4. انقر على \"الخطوة التالية\" ثم على \"إنشاء التصدير\"
+\n5. انقر على زر \"تحميل\" بعد ظهوره
+\n6. انقر على IMPORT FILE أدناه وحدد الملف البريدي تحميلها
+\n7. [إذا فشل استيراد الرمز البريدي] استخراج ملف .csv (عادة تحت عنوان \"يوتيوب ويوتيوب الموسيقى / الاشتراكات / الاشتراكات.csv\")، انقر على IMPORT FILE أدناه وحدد ملف csv المستخرج
+ جارٍ التحميل
+ هذا مسار SoundCloud Go+، على الأقل في بلدك، لذلك لا يمكن دفقه أو تنزيله بواسطة NewPipe.
+ فقط على شبكة Wi-Fi
+ إظهار الصورة المصغرة
+ ضوابط سرعة التشغيل
+ رد الجميل
+ تم حذف سجل المشاهدة
+ سجل المشاهَدة و ذاكرة التخزين المؤقتة
+ النسبة المئوية
+ تحدي الكابتشا
+ ليس هناك مشترِكون
+ يتوفر هذا المحتوى فقط للمستخدمين الذين قاموا بالدفع، لذلك لا يمكن بثه أو تنزيله عبر NewPipe.
+ حديثة
+ تعذر تحديث الاشتراك
+ سيتم استبدال قائمة انتظار للمشغل النشط
+ التقسيم
+ اشتراك
+ أنشأها %s
+ الحرف الإستبدالي
+ وضع مظهر ”عرض القائمة“
+ بدء تشغيل المشغل الرئيسي في وضع ملء الشاشة
+ تصغير عند تبديل التطبيق
+ الصوت : %s
+ خطوة
+ حل
+ %s يقدم هذا السبب:
+ الدفق المحدد غير مدعوم من قبل المشغلون الخارجيون
+ عن تطبيق نيوپايپ
+ تسريع إلى الأمام/-ترجيع وقت البحث
+ تم رفضها من قبل النظام
+ ليس هناك تعليقات
+ مليون
+ جاري التحقق من وجود تحديثات…
+ المحتوى
+ اسأل عن مكان التنزيل
+ معطل
+ خطأ في تحميل الخلاصة
+ الإخطارات حول التدفقات الجديدة للاشتراكات
+ لا توجد مشاهدة
+ الإنتقال إلى التشغيل في الخلفية
+ المستخدمين
+ القائمة
+ لا يمكن تشغيل هذا البث
+ امسح ملفات تعريف الارتباط reCAPTCHA
+ اللافتات
+ ساهم
+ الإجراء عند التبديل إلى تطبيق آخر من مشغل الفيديو الرئيسي — %s
+ قوائم التشغيل
+ عامة
+ قم بتعطيل نفق الوسائط إذا واجهت شاشة سوداء أو تقطيع اثناء تشغيل الفيديو.
+ يوجد ملف تحميل بهذا الاسم موجود مسبقاً
+ تلقائي
+ هل تريد محو سجل التنزيل، أم تريد حذف جميع الملفات التي تم تنزيلها؟
+ أفضل دقة
+ فرز
+ الشبكة
+ استعادة آخر موقف تشغيل
+ تم إنهاء الحساب
+ اختر إيماءة للنصف الأيمن من شاشة المشغل
+ الحصول على المعلومات…
+ الكتابة فوق
+ نقل الملف أو حذفه
+ الأغاني
+ تطبيق Kore غير موجود. هل تريد تثبيته؟
+ هنالك تحميل قيد التقدم بهذا الاسم
+ الإنتقال إلى الرئيسية
+ صيغة الصوت الافتراضية
+ حدث خطأ، انظر للإشعار
+ الفيديو
+ مفيد عند التبديل إلى بيانات الجوال، ولكن لا يمكن تعليق بعض التنزيلات
+ انقل محدد اللسان الرئيسي إلى الأسفل
+ LeakCanary غير متوفر
+ عدد المشتركين غير متاح
+ حدد الاشتراكات
+ الاشتراكات
+ NewPipe هو برنامج مفتوح المصدر وبحقوق متروكة: يمكنك استخدام الكود ودراسته وتحسينه كما شئت. وعلى وجه التحديد يمكنك إعادة توزيعه / أو تعديله تحت شروط رخصة GNU العمومية والتي نشرتها مؤسسة البرمجيات الحرة، سواء الإصدار 3 من الرخصة، أو (باختيارك) أي إصدار أحدث.
+ اعرض خيار لتشغيل الفيديو عبر مركز وسائط Kodi
+ قيد المعالجة
+ إعادة تسمية
+ لا يمكن إنشاء الملف
+ حد قائمة انتظار التنزيل
+ أي شبكة
+ فشل التنزيل
+ الافتراضي ExoPlayer
+ تنظيف تاريخ التحميل
+ هناك تنزيل معلق بهذا الاسم
+ انتهى
+ أُضيف مؤخرًا
+ يتم استبدال الرموز غير المسموح بها بهذه القيمة
+ لا شيء هنا سوى الصراصير
+ مُنشأة تلقائيًا (لم يتم العثور على رافع)
+ المسار الصوتي
+ مفيد ، على سبيل المثال ، إذا كنت تستخدم سماعة رأس بأزرار مادية مكسورة
+ المشغل الخارجي لا يدعم هذه الأنواع من الروابط
+ ستكون النصوص الأصلية من الخدمات مرئية في عناصر البث
+ الإنتقال إلى التشغيل في النافذة المنبثقة
+ تعذر العثور على الخادم
+ تقرير الأخطاء خارج دورة الحياة
+ البطاقة
+ حدد كشك
+ لا يوفر وضع الموجز السريع مزيدًا من المعلومات حول هذا الموضوع.
+ يرجى تحديد مجلد التنزيل لاحقا في الإعدادات
+ سيتم سؤالك عن مكان حفظ كل تنزيل
+ لاتوجد بثوث مباشرة
+ صفحة فارغة
+ أبدا
+ فشل النسخ إلى الحافظة
+ من أجل الامتثال للائحة الأوروبية العامة لحماية البيانات (GDPR)، فإننا نلفت انتباهك إلى سياسة خصوصية NewPipe. يرجى قراءتها بعناية.
+\nويجب عليك قبولها لإرسال تقرير الأخطاء إلينا.
+ فرض الإبلاغ عن استثناءات Rx غير القابلة للتسليم خارج دورة حياة الجزء أو النشاط بعد التخلص منها
+ ضبط إشعار مشغل البث الحالي
+ تصدير قاعدة البيانات
+ إلغاء تعيين الصورة المصغرة الدائمة
+ تعليقات
+ تكبير
+ الصنف
+ تناسب مع الشاشة
+ إظهار مؤشّرات وضع التشغيل في القوائم
+ اختر جودة الصور وما إذا كنت تريد تحميل الصور على الإطلاق، لتقليل استخدام البيانات والذاكرة. تؤدي التغييرات إلى مسح ذاكرة التخزين المؤقت للصور الموجودة في الذاكرة وعلى القرص — %s
+ حجم الفاصل الزمني لتحميل التشغيل
+ حذف سِجل البحث بالكامل؟
+ اللغة
+ السلوك
+ لا يوجد مثل هذا المجلد
+ شغِّل
+ إلغاء الإرتباط (قد يسبب تشويه)
+ امسح البيانات
+ لا توجد إشارات مرجعية لقائمة التشغيل بعدُ
+ يتم تطوير NewPipe من قبل متطوعين يقضون وقت فراغهم لتقديم أفضل تجربة لك. حان الوقت لرد المساعدة مع المطورين وجعل NewPipe أكثر و أفضل بينما يستمتعون بفنجان من القهوة.
+ إظهار التعليقات
+ تم نسخه إلى الحافظة
+ المزيد من الخيارات
+ وضع البث القادم تلقائيا في قائمة الإنتظار
+ الدعم
+ ملف مضغوط ZIP غير صالح
+ غير موجود
+ حدد موضوعك الليلي المفضل — %s
+ صفحة الكشك
+
+ - لا مُستمِع
+ - مُستَمِع واحد
+ - مُستمِعان
+ - %s مُستَمِعين
+ - %s مُستَمِعًا
+ - %s مستمع
+
+ غير مدرج
+ المؤتمرات
+ إستيراد ملف
+ إظهار خطأ snackbar
+ قم بزيارة موقع NewPipe لمزيد من المعلومات والمستجدات.
+ لم يتم الاشتراك في أي قناة بعد
+ لا تبدأ تشغيل مقاطع الفيديو في المشغل المصغر، ولكن قم بتدوير وضع ملء الشاشة مباشرة، إذا تم تأمين التدوير التلقائي. لا يزال بإمكانك الوصول إلى المشغل المصغر عن طريق الخروج من ملء الشاشة
+ تراخيص NewPipe
+ تحديث دائمًا
+ التراخيص
+ عملية التصدير جارية …
+ تصغير إلى مشغل الخلفية
+ تنبيهات مشغل NewPipe
+ تمَّت عملية الإستيراد
+ الصور المصغرة
+ تحميل تفاصيل البث…
+ إظهار تلميح \"اضغط للفتح\"
+ الحد من جودة الفيديو عند استخدام بيانات الهاتف المحمول
+ سيتم تشغيل تنزيل واحد في نفس الوقت
+ قبول
+ إعدادات ExoPlayer
+ تنبيه تحديث التطبيق
+ غير صامت
+ امسح ملفات تعريف الارتباط التي يخزنها NewPipe عند حل reCAPTCHA
+ نظرا لقيود مشغل ExoPlayer مدة التقديم تم ضبطها الى %d ثانية
+ أفضل ٥٠
+ السطوع
+ تمكين تحديد نص في الوصف
+ شاهَدتَها بالكامل
+ بدون ترجمات نصية توضيحية
+ المواضع في القوائم
+ الملف
+ التحديثات
+ النظام الافتراضي
+ لا يتم عرض التدفقات التي لم يدعمها برنامج التنزيل بعد
+ لم يتم العثور على أي بث مرئي
+ وضع التالي على قائمة الانتظار
+ إظهار معلومات التعريف
+ اختر قناة
+ © %1$sبواسطة%2$sتحت%3$s
+ تعطيل نفق الوسائط
+ المسارات
+ اسم المجموعة فارغ
+ يتم دعم عناوين URL HTTPS فقط
+ المدة
+ لا يمكن تصدير الاشتراكات
+ لاتوجد بثوث
+ يمكنك تحديد ثلاثة إجراءات كحد أقصى لإظهارها في الإشعار المضغوط!
+ إذا كانت لديك أفكار؛ أو ترجمة، أو تغييرات تخص التصميم، أو تنظيف و تحسين الشفرة البرمجية، أو تعديلات عميقة عليها، فتذكر أنّ مساعدتك دائما موضع ترحيب. وكلما أتممنا شيئا كلما كان ذلك أفضل!
+ اختار الألسنة التي تودّ ظهورها على الصفحة الرئيسية
+ مجلد تحميل الفيديو
+ هذا الفيديو مقيد بالفئة العمرية.
+\n
+\nقم بتفيل \"%1$s\" في الإعدادات إذا كنت تريد رؤيته.
+ تجاهل أحداث ازرار الوسائط الأجهزة
+ اختر لسانًا
+ الصوت
+ تم حذف كل مواقف التشغيل
+ في الخلفية
+ القنوات
+ الرخصة
+ إلغاء الاشتراك
+ لقد اشتركت الآن في هذه القناة
+ بدءًا من Android 10، يتم دعم \"Storage Access Framework\" فقط
+ إنشاء اسم فريد
+ أظهر أشرطة ملونة لبيكاسو أعلى الصور تشير إلى مصدرها: الأحمر للشبكة والأزرق للقرص والأخضر للذاكرة
+ فشل الاتصال الآمن
+ يتوفر هذا الفيديو فقط لأعضاء YouTube Music Premium، لذلك لا يمكن بثه أو تنزيله من قبل NewPipe.
+ البث السابق
+ تشغيل الكل
+ السمة
+ أبلِغ
+ اختر إيماءة للنصف الأيسر من شاشة المشغل
+ نسخ التقرير مُنسق
+ مباشر
+ إظهار تنبيه للمطالبة بتحديث التطبيق عندما يتوفر إصدار جديد
+ السؤال دائماً
+ منجز
+ تقرير عن المشكلة
+ أُضيفَ إلى قائمة الانتظار
+ لم يتم العثور على مدير ملفات مناسب لهذا الإجراء.
+\nيرجى تثبيت مدير ملفات أو محاولة تعطيل \"%s\" في إعدادات التنزيل
+ مشغل الفيديو
+ بواسطة %s
+ وصف
+ ريثما
+ شارك الرابط التشعبي للقائمة
+ شارِكها بالترجمات النصية
+ %1$s
+\n%2$s
+ شارِك قائمة التشغيل
+ شارِك قائمة التشغيل بتفاصيليها مثل اسم قائمة التشغيل وعناوين الفيديو أو كقائمة بسيطة من عناوين تشعّبيّة للفيديوهات
+ - %1$s: %2$s
\ 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 d0cf90db4..d69564493 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -45,7 +45,6 @@
تعذر تحميل كافة الصور المصغرة
خطأ
تعذر تحليل الموقع
- تعذر فك تشفير توقيع رابط الفيديو
اضغط على عدسة المكبرة للبدء.
اشتراك
مشترك
@@ -243,7 +242,6 @@
الإشارات المرجعية
استعمال التقديم السريع الغير دقيق
خاصية التقديم الغير دقيق تسمح للمشغل بالقفز خلال الفديو بشكل أسرع مع دقة قفز أقل. خاصية القفز ل5، 15 او 25 لا تعمل مع القفز الغير دقيق
- تحميل الصور المصغرة
تم إفراغ مساحة ذاكرة التخزين المؤقتة الخاصة بالصور
الملف
لا يوجد مثل هذا المجلد
@@ -260,7 +258,6 @@
عملية التصدير جارية …
إستيراد ملف
معرفك, soundcloud.com/هويتك
- قم بإيقاف تشغيله لمنع تحميل الصور المصغرة وحفظ البيانات واستخدام الذاكرة. تمسح التغييرات كلاً من ذاكرة التخزين المؤقت للصورة الموجودة في الذاكرة والموجودة على القرص
امسح البيانات الوصفيّة المخزّنة مؤقّتًا
إزالة جميع بيانات صفحات الويب المخزنة مؤقّتًا
تم محو ذاكرة التخزين المؤقتّة للبيانات الوصفيّة
@@ -410,7 +407,7 @@
لا يمكن الكتابة فوق الملف
هناك تنزيل معلق بهذا الاسم
تم إغلاق NewPipe أثناء العمل على الملف
- لم يتبقى مساحة في الجهاز
+ لم يتبقى مساحة في الجهاز
تم فقد التقدم بسبب حذف الملف
انتهى وقت الاتصال
هل تريد محو سجل التنزيل، أم تريد حذف جميع الملفات التي تم تنزيلها؟
@@ -469,7 +466,7 @@
لغة التطبيق
النظام الافتراضي
اضغط على \"تم\" عند حلها
- منجز
+ منجز
الفيديوهات
- %d ثانية
@@ -587,7 +584,7 @@
خلط
تكرار
يمكنك تحديد ثلاثة إجراءات كحد أقصى لإظهارها في الإشعار المضغوط!
- قم بتحرير كل إشعار أدناه من خلال النقر عليه. حدد ما يصل إلى ثلاثة منها لتظهر في الإشعار المضغوط باستخدام مربعات الاختيار الموجودة على اليمين
+ قم بتحرير كل إجراء إعلام أدناه من خلال النقر عليه. حدد ما يصل إلى ثلاثة منها ليتم عرضها في الإشعار المضغوط باستخدام مربعات الاختيار الموجودة على اليمين.
زر الإجراء الخامس
زر الإجراء الرابع
زر الإجراء الثالث
@@ -644,7 +641,6 @@
خاص
غير مدرج
عامة
- رابط الصورة المصغرة
المضيف
الدعم
اللغة
@@ -818,4 +814,59 @@
تم تعطيل نفق وسائل الإعلام عن طريق التقصير على جهازك لأن نموذج جهازك معروف بأنه لا يدعمه.
لاتوجد بثوث
لاتوجد بثوث مباشرة
+ الفيديوات
+ المشتركون
+ ما هي التبويبات المعروضة على صفحات القناة
+ تبويبات القنوات
+ الفديوهات القصيرة
+ جلب البيانات الوصفية…
+ تبديل تدوير الشاشة
+ الانتقال إلى وضع ملئ الشاشة
+ اجلب تبويبات القنوات
+ البث التالي
+ فتح قائمة انتظار التسغيل
+ حول
+ تقديم المحتوى
+ الألبومات
+ إعادة المحتوى
+ إعادة التشغيل
+ علامات التبويب لجلبها عند تحديث الخلاصة. ليس لهذا الخيار أي تأثير إذا تم تحديث القناة باستخدام الوضع السريع.
+ قوائم التشغيل
+ شغِّل
+ المزيد من الخيارات
+ المسارات
+ المدة
+ القنوات
+ البث السابق
+ مباشر
+ جودة الصورة
+ ؟
+ جودة منخفضة
+ الصور الرمزية
+ الصور الرمزية للقناة الفرعية
+ لا تقم بتحميل الصور
+ عالية الجودة
+ النوعية متوسطة
+ الصورة الرمزية للرفع
+ اللافتات
+ اختر جودة الصور وما إذا كنت تريد تحميل الصور على الإطلاق، لتقليل استخدام البيانات والذاكرة. تؤدي التغييرات إلى مسح ذاكرة التخزين المؤقت للصور الموجودة في الذاكرة وعلى القرص — %s
+ الصور المصغرة
+ مشاركة عنوان القائمة
+ مشاركة العناوين
+ %1$s
+\n%2$s
+ مشاركة قائمة التشغيل
+ شارك تفاصيل قائمة التشغيل مثل اسم قائمة التشغيل وعناوين الفيديو أو كقائمة بسيطة من عناوين URL للفيديو
+ - %1$s: %2$s
+
+ - رد %s
+ - رد %s
+ - ردان%s
+ - ردود%s
+ - ردود %s
+ - ردود %s
+
+ عرض المزيد
+ عرض أقل
+ قم بتحرير كل إجراء إعلام أدناه من خلال النقر عليه. يتم تعيين الإجراءات الثلاثة الأولى (تشغيل/إيقاف مؤقت، السابق والتالي) بواسطة النظام ولا يمكن تخصيصها.
\ 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
index 93e9e363e..fc9d5b2b6 100644
--- a/app/src/main/res/values-as/strings.xml
+++ b/app/src/main/res/values-as/strings.xml
@@ -62,7 +62,6 @@
লোড ব্যৱধানৰ আকাৰ সলনি কৰক (বৰ্তমানে %s) । এটা কম মানে প্ৰাৰম্ভিক ভিডিঅ\' লোডিং দ্ৰুত কৰিব পাৰে। পৰিৱৰ্তনৰ বাবে এটা খেলুৱৈ পুনৰাৰম্ভৰ প্ৰয়োজন
থাম্বনেইলত থকা মূল ৰং অনুসৰি এণ্ড্ৰইডক জাননীৰ ৰং কাষ্টমাইজ কৰিবলৈ কওক (মন কৰিব যে এইটো সকলো ডিভাইচতে উপলব্ধ নহয়)
সক্ৰিয় প্লেয়াৰৰ queue সলনি কৰা হ’ব
- থাম্বনেইল লোড কৰক
মন্তব্য দেখুৱাওক
বিৱৰণ দেখুৱাওক
মেটা তথ্য দেখুৱাওক
@@ -96,6 +95,5 @@
ভিডিঅ\'ৰ বিৱৰণ আৰু অতিৰিক্ত তথ্য লুকুৱাবলৈ বন্ধ কৰক
মন্তব্য লুকুৱাবলৈ বন্ধ কৰক
\'পৰৱৰ্তী\' আৰু \'সাদৃশ্য থকা\' ভিডিঅ\' দেখুৱাওক
- থাম্বনেইলসমূহ লোড কৰা, তথ্য আৰু মেমৰি ব্যৱহাৰ সংৰক্ষণ কৰা ৰোধ কৰিবলে বন্ধ কৰক। পৰিবৰ্তনসমূহে ইন-মেমৰি আৰু অন-ডিস্ক কেশ্ব দুয়োটা পৰিষ্কাৰ কৰে
ষ্ট্ৰিমৰ সৃষ্টিকৰ্তা, ষ্ট্ৰিমৰ বিষয়বস্তু বা এটা সন্ধান অনুৰোধৰ বিষয়ে অতিৰিক্ত তথ্যৰ সৈতে মেটা তথ্যৰ বাকচসমূহ লুকুৱাবলৈ বন্ধ কৰক
\ 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 a238758af..c0ac7e6d5 100644
--- a/app/src/main/res/values-az/strings.xml
+++ b/app/src/main/res/values-az/strings.xml
@@ -136,7 +136,6 @@
\nOnu görmək istəyirsinizsə, tənzimləmələrdə \"%1$s\" seçimini aktivləşdirin.