sortedVideos,
+ String playbackQuality) {
+ return ListHelper.getResolutionIndex(context, sortedVideos, playbackQuality);
+ }
+ };
}
/*//////////////////////////////////////////////////////////////////////////
@@ -678,7 +752,6 @@ public final class MainVideoPlayer extends AppCompatActivity
@Override
public void onBuffering() {
super.onBuffering();
- animatePlayButtons(false, 100);
getRootView().setKeepScreenOn(true);
}
@@ -728,6 +801,13 @@ public final class MainVideoPlayer extends AppCompatActivity
// Utils
//////////////////////////////////////////////////////////////////////////*/
+ private void setInitialGestureValues() {
+ if (getAudioReactor() != null) {
+ final float currentVolumeNormalized = (float) getAudioReactor().getVolume() / getAudioReactor().getMaxVolume();
+ volumeProgressBar.setProgress((int) (volumeProgressBar.getMax() * currentVolumeNormalized));
+ }
+ }
+
@Override
public void showControlsThenHide() {
if (queueVisible) return;
@@ -831,12 +911,28 @@ public final class MainVideoPlayer extends AppCompatActivity
return channelTextView;
}
- public TextView getVolumeTextView() {
- return volumeTextView;
+ public RelativeLayout getVolumeRelativeLayout() {
+ return volumeRelativeLayout;
}
- public TextView getBrightnessTextView() {
- return brightnessTextView;
+ public ProgressBar getVolumeProgressBar() {
+ return volumeProgressBar;
+ }
+
+ public ImageView getVolumeImageView() {
+ return volumeImageView;
+ }
+
+ public RelativeLayout getBrightnessRelativeLayout() {
+ return brightnessRelativeLayout;
+ }
+
+ public ProgressBar getBrightnessProgressBar() {
+ return brightnessProgressBar;
+ }
+
+ public ImageView getBrightnessImageView() {
+ return brightnessImageView;
}
public ImageButton getRepeatButton() {
@@ -846,15 +942,18 @@ public final class MainVideoPlayer extends AppCompatActivity
public ImageButton getPlayPauseButton() {
return playPauseButton;
}
+
+ public int getMaxGestureLength() {
+ return maxGestureLength;
+ }
}
- private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener {
+ private class PlayerGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener {
private boolean isMoving;
@Override
public boolean onDoubleTap(MotionEvent e) {
if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY());
- if (!playerImpl.isPlaying()) return false;
if (e.getX() > playerImpl.getRootView().getWidth() * 2 / 3) {
playerImpl.onFastForward();
@@ -888,91 +987,91 @@ public final class MainVideoPlayer extends AppCompatActivity
return super.onDown(e);
}
+ private static final int MOVEMENT_THRESHOLD = 40;
+
private final boolean isPlayerGestureEnabled = PlayerHelper.isPlayerGestureEnabled(getApplicationContext());
+ private final int maxVolume = playerImpl.getAudioReactor().getMaxVolume();
- private final float stepsBrightness = 15, stepBrightness = (1f / stepsBrightness), minBrightness = .01f;
- private float currentBrightness = getWindow().getAttributes().screenBrightness > 0
- ? getWindow().getAttributes().screenBrightness
- : 0.5f;
-
- private int currentVolume, maxVolume = playerImpl.getAudioReactor().getMaxVolume();
- private final float stepsVolume = 15, stepVolume = (float) Math.ceil(maxVolume / stepsVolume), minVolume = 0;
-
- private final String brightnessUnicode = new String(Character.toChars(0x2600));
- private final String volumeUnicode = new String(Character.toChars(0x1F508));
-
- private final int MOVEMENT_THRESHOLD = 40;
- private final int eventsThreshold = 8;
- private boolean triggered = false;
- private int eventsNum;
-
- // TODO: Improve video gesture controls
@Override
- public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ public boolean onScroll(MotionEvent initialEvent, MotionEvent movingEvent, float distanceX, float distanceY) {
if (!isPlayerGestureEnabled) return false;
//noinspection PointlessBooleanExpression
if (DEBUG && false) Log.d(TAG, "MainVideoPlayer.onScroll = " +
- ", e1.getRaw = [" + e1.getRawX() + ", " + e1.getRawY() + "]" +
- ", e2.getRaw = [" + e2.getRawX() + ", " + e2.getRawY() + "]" +
+ ", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" +
+ ", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" +
", distanceXy = [" + distanceX + ", " + distanceY + "]");
- float abs = Math.abs(e2.getY() - e1.getY());
- if (!triggered) {
- triggered = abs > MOVEMENT_THRESHOLD;
+
+ final boolean insideThreshold = Math.abs(movingEvent.getY() - initialEvent.getY()) <= MOVEMENT_THRESHOLD;
+ if (!isMoving && (insideThreshold || Math.abs(distanceX) > Math.abs(distanceY))
+ || playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) {
return false;
}
- if (eventsNum++ % eventsThreshold != 0 || playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) return false;
isMoving = true;
-// boolean up = !((e2.getY() - e1.getY()) > 0) && distanceY > 0; // Android's origin point is on top
- boolean up = distanceY > 0;
-
- if (e1.getX() > playerImpl.getRootView().getWidth() / 2) {
- double floor = Math.floor(up ? stepVolume : -stepVolume);
- currentVolume = (int) (playerImpl.getAudioReactor().getVolume() + floor);
- if (currentVolume >= maxVolume) currentVolume = maxVolume;
- if (currentVolume <= minVolume) currentVolume = (int) minVolume;
+ if (initialEvent.getX() > playerImpl.getRootView().getWidth() / 2) {
+ playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY);
+ float currentProgressPercent =
+ (float) playerImpl.getVolumeProgressBar().getProgress() / playerImpl.getMaxGestureLength();
+ int currentVolume = (int) (maxVolume * currentProgressPercent);
playerImpl.getAudioReactor().setVolume(currentVolume);
- currentVolume = playerImpl.getAudioReactor().getVolume();
if (DEBUG) Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume);
- final String volumeText = volumeUnicode + " " + Math.round((((float) currentVolume) / maxVolume) * 100) + "%";
- playerImpl.getVolumeTextView().setText(volumeText);
- if (playerImpl.getVolumeTextView().getVisibility() != View.VISIBLE) animateView(playerImpl.getVolumeTextView(), true, 200);
- if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.getBrightnessTextView().setVisibility(View.GONE);
+ final int resId =
+ currentProgressPercent <= 0 ? R.drawable.ic_volume_off_white_72dp
+ : currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_72dp
+ : currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_72dp
+ : R.drawable.ic_volume_up_white_72dp;
+
+ playerImpl.getVolumeImageView().setImageDrawable(
+ AppCompatResources.getDrawable(getApplicationContext(), resId)
+ );
+
+ if (playerImpl.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) {
+ animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, true, 200);
+ }
+ if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
+ playerImpl.getBrightnessRelativeLayout().setVisibility(View.GONE);
+ }
} else {
- WindowManager.LayoutParams lp = getWindow().getAttributes();
- currentBrightness += up ? stepBrightness : -stepBrightness;
- if (currentBrightness >= 1f) currentBrightness = 1f;
- if (currentBrightness <= minBrightness) currentBrightness = minBrightness;
+ playerImpl.getBrightnessProgressBar().incrementProgressBy((int) distanceY);
+ float currentProgressPercent =
+ (float) playerImpl.getBrightnessProgressBar().getProgress() / playerImpl.getMaxGestureLength();
+ WindowManager.LayoutParams layoutParams = getWindow().getAttributes();
+ layoutParams.screenBrightness = currentProgressPercent;
+ getWindow().setAttributes(layoutParams);
- lp.screenBrightness = currentBrightness;
- getWindow().setAttributes(lp);
- if (DEBUG) Log.d(TAG, "onScroll().brightnessControl, currentBrightness = " + currentBrightness);
- int brightnessNormalized = Math.round(currentBrightness * 100);
+ if (DEBUG) Log.d(TAG, "onScroll().brightnessControl, currentBrightness = " + currentProgressPercent);
- final String brightnessText = brightnessUnicode + " " + (brightnessNormalized == 1 ? 0 : brightnessNormalized) + "%";
- playerImpl.getBrightnessTextView().setText(brightnessText);
+ final int resId =
+ currentProgressPercent < 0.25 ? R.drawable.ic_brightness_low_white_72dp
+ : currentProgressPercent < 0.75 ? R.drawable.ic_brightness_medium_white_72dp
+ : R.drawable.ic_brightness_high_white_72dp;
- if (playerImpl.getBrightnessTextView().getVisibility() != View.VISIBLE) animateView(playerImpl.getBrightnessTextView(), true, 200);
- if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.getVolumeTextView().setVisibility(View.GONE);
+ playerImpl.getBrightnessImageView().setImageDrawable(
+ AppCompatResources.getDrawable(getApplicationContext(), resId)
+ );
+
+ if (playerImpl.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) {
+ animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, true, 200);
+ }
+ if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
+ playerImpl.getVolumeRelativeLayout().setVisibility(View.GONE);
+ }
}
return true;
}
private void onScrollEnd() {
if (DEBUG) Log.d(TAG, "onScrollEnd() called");
- triggered = false;
- eventsNum = 0;
- /* if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.getVolumeTextView().setVisibility(View.GONE);
- if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.getBrightnessTextView().setVisibility(View.GONE);*/
- if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) {
- animateView(playerImpl.getVolumeTextView(), false, 200, 200);
+
+ if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
+ animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200);
}
- if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) {
- animateView(playerImpl.getBrightnessTextView(), false, 200, 200);
+ if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
+ animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200);
}
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerState.java b/app/src/main/java/org/schabi/newpipe/player/PlayerState.java
index 8ffcb6b29..359159809 100644
--- a/app/src/main/java/org/schabi/newpipe/player/PlayerState.java
+++ b/app/src/main/java/org/schabi/newpipe/player/PlayerState.java
@@ -14,21 +14,26 @@ public class PlayerState implements Serializable {
private final float playbackSpeed;
private final float playbackPitch;
@Nullable private final String playbackQuality;
+ private final boolean playbackSkipSilence;
private final boolean wasPlaying;
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
- final float playbackSpeed, final float playbackPitch, final boolean wasPlaying) {
- this(playQueue, repeatMode, playbackSpeed, playbackPitch, null, wasPlaying);
+ final float playbackSpeed, final float playbackPitch,
+ final boolean playbackSkipSilence, final boolean wasPlaying) {
+ this(playQueue, repeatMode, playbackSpeed, playbackPitch, null,
+ playbackSkipSilence, wasPlaying);
}
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
final float playbackSpeed, final float playbackPitch,
- @Nullable final String playbackQuality, final boolean wasPlaying) {
+ @Nullable final String playbackQuality, final boolean playbackSkipSilence,
+ final boolean wasPlaying) {
this.playQueue = playQueue;
this.repeatMode = repeatMode;
this.playbackSpeed = playbackSpeed;
this.playbackPitch = playbackPitch;
this.playbackQuality = playbackQuality;
+ this.playbackSkipSilence = playbackSkipSilence;
this.wasPlaying = wasPlaying;
}
@@ -62,6 +67,10 @@ public class PlayerState implements Serializable {
return playbackQuality;
}
+ public boolean isPlaybackSkipSilence() {
+ return playbackSkipSilence;
+ }
+
public boolean wasPlaying() {
return wasPlaying;
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java
index f9892ecb5..0e7328020 100644
--- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java
@@ -19,6 +19,8 @@
package org.schabi.newpipe.player;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
import android.annotation.SuppressLint;
import android.app.NotificationManager;
import android.app.PendingIntent;
@@ -34,7 +36,7 @@ import android.os.Build;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
+import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.NotificationCompat;
import android.util.DisplayMetrics;
import android.util.Log;
@@ -42,7 +44,9 @@ import android.view.GestureDetector;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
+import android.view.ViewGroup;
import android.view.WindowManager;
+import android.view.animation.AnticipateInterpolator;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.PopupMenu;
@@ -56,17 +60,17 @@ import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.text.CaptionStyleCompat;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.SubtitleView;
+import com.nostra13.universalimageloader.core.assist.FailReason;
import org.schabi.newpipe.BuildConfig;
-import org.schabi.newpipe.CheckForNewAppVersionTask;
import org.schabi.newpipe.R;
-import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.LockManager;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.old.PlayVideoActivity;
-import org.schabi.newpipe.player.playqueue.PlayQueueItem;
+import org.schabi.newpipe.player.resolver.MediaSourceTag;
+import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;
@@ -99,11 +103,19 @@ public final class PopupVideoPlayer extends Service {
private static final int MINIMUM_SHOW_EXTRA_WIDTH_DP = 300;
- private WindowManager windowManager;
- private WindowManager.LayoutParams windowLayoutParams;
- private GestureDetector gestureDetector;
+ private static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
+ WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+ private static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS |
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
+
+ private WindowManager windowManager;
+ private WindowManager.LayoutParams popupLayoutParams;
+ private GestureDetector popupGestureDetector;
+
+ private View closeOverlayView;
+ private FloatingActionButton closeOverlayButton;
+ private WindowManager.LayoutParams closeOverlayLayoutParams;
- private int shutdownFlingVelocity;
private int tossFlingVelocity;
private float screenWidth, screenHeight;
@@ -118,6 +130,7 @@ public final class PopupVideoPlayer extends Service {
private VideoPlayerImpl playerImpl;
private LockManager lockManager;
+ private boolean isPopupClosing = false;
/*//////////////////////////////////////////////////////////////////////////
// Service-Activity Binder
@@ -146,7 +159,10 @@ public final class PopupVideoPlayer extends Service {
public int onStartCommand(final Intent intent, int flags, int startId) {
if (DEBUG)
Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]");
- if (playerImpl.getPlayer() == null) initPopup();
+ if (playerImpl.getPlayer() == null) {
+ initPopup();
+ initPopupCloseOverlay();
+ }
if (!playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(true);
playerImpl.handleIntent(intent);
@@ -156,15 +172,16 @@ public final class PopupVideoPlayer extends Service {
@Override
public void onConfigurationChanged(Configuration newConfig) {
+ if (DEBUG) Log.d(TAG, "onConfigurationChanged() called with: newConfig = [" + newConfig + "]");
updateScreenSize();
- updatePopupSize(windowLayoutParams.width, -1);
- checkPositionBounds();
+ updatePopupSize(popupLayoutParams.width, -1);
+ checkPopupPositionBounds();
}
@Override
public void onDestroy() {
if (DEBUG) Log.d(TAG, "onDestroy() called");
- onClose();
+ closePopup();
}
@Override
@@ -182,7 +199,6 @@ public final class PopupVideoPlayer extends Service {
View rootView = View.inflate(this, R.layout.player_popup, null);
playerImpl.setup(rootView);
- shutdownFlingVelocity = PlayerHelper.getShutdownFlingVelocity(this);
tossFlingVelocity = PlayerHelper.getTossFlingVelocity(this);
updateScreenSize();
@@ -192,28 +208,56 @@ public final class PopupVideoPlayer extends Service {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
popupWidth = popupRememberSizeAndPos ? sharedPreferences.getFloat(POPUP_SAVED_WIDTH, defaultSize) : defaultSize;
- final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_PHONE : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+ final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O ?
+ WindowManager.LayoutParams.TYPE_PHONE :
+ WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
- windowLayoutParams = new WindowManager.LayoutParams(
+ popupLayoutParams = new WindowManager.LayoutParams(
(int) popupWidth, (int) getMinimumVideoHeight(popupWidth),
layoutParamType,
- WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ IDLE_WINDOW_FLAGS,
PixelFormat.TRANSLUCENT);
- windowLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
+ popupLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
+ popupLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
int centerX = (int) (screenWidth / 2f - popupWidth / 2f);
int centerY = (int) (screenHeight / 2f - popupHeight / 2f);
- windowLayoutParams.x = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_X, centerX) : centerX;
- windowLayoutParams.y = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_Y, centerY) : centerY;
+ popupLayoutParams.x = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_X, centerX) : centerX;
+ popupLayoutParams.y = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_Y, centerY) : centerY;
- checkPositionBounds();
+ checkPopupPositionBounds();
- MySimpleOnGestureListener listener = new MySimpleOnGestureListener();
- gestureDetector = new GestureDetector(this, listener);
+ PopupWindowGestureListener listener = new PopupWindowGestureListener();
+ popupGestureDetector = new GestureDetector(this, listener);
rootView.setOnTouchListener(listener);
- playerImpl.getLoadingPanel().setMinimumWidth(windowLayoutParams.width);
- playerImpl.getLoadingPanel().setMinimumHeight(windowLayoutParams.height);
- windowManager.addView(rootView, windowLayoutParams);
+
+ playerImpl.getLoadingPanel().setMinimumWidth(popupLayoutParams.width);
+ playerImpl.getLoadingPanel().setMinimumHeight(popupLayoutParams.height);
+ windowManager.addView(rootView, popupLayoutParams);
+ }
+
+ @SuppressLint("RtlHardcoded")
+ private void initPopupCloseOverlay() {
+ if (DEBUG) Log.d(TAG, "initPopupCloseOverlay() called");
+ closeOverlayView = View.inflate(this, R.layout.player_popup_close_overlay, null);
+ closeOverlayButton = closeOverlayView.findViewById(R.id.closeButton);
+
+ final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O ?
+ WindowManager.LayoutParams.TYPE_PHONE :
+ WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+ final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+ | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+
+ closeOverlayLayoutParams = new WindowManager.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
+ layoutParamType,
+ flags,
+ PixelFormat.TRANSLUCENT);
+ closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
+ closeOverlayLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
+
+ closeOverlayButton.setVisibility(View.GONE);
+ windowManager.addView(closeOverlayView, closeOverlayLayoutParams);
}
/*//////////////////////////////////////////////////////////////////////////
@@ -229,6 +273,7 @@ public final class PopupVideoPlayer extends Service {
notRemoteView.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle());
notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getUploaderName());
+ notRemoteView.setImageViewBitmap(R.id.notificationCover, playerImpl.getThumbnail());
notRemoteView.setOnClickPendingIntent(R.id.notificationPlayPause,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT));
@@ -244,11 +289,15 @@ public final class PopupVideoPlayer extends Service {
setRepeatModeRemote(notRemoteView, playerImpl.getRepeatMode());
- return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
.setOngoing(true)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContent(notRemoteView);
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
+ builder.setPriority(NotificationCompat.PRIORITY_MAX);
+ }
+ return builder;
}
/**
@@ -268,44 +317,105 @@ public final class PopupVideoPlayer extends Service {
// Misc
//////////////////////////////////////////////////////////////////////////*/
- public void onClose() {
- if (DEBUG) Log.d(TAG, "onClose() called");
+ public void closePopup() {
+ if (DEBUG) Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing);
+ if (isPopupClosing) return;
+ isPopupClosing = true;
if (playerImpl != null) {
if (playerImpl.getRootView() != null) {
windowManager.removeView(playerImpl.getRootView());
- playerImpl.setRootView(null);
}
+ playerImpl.setRootView(null);
playerImpl.stopActivityBinding();
playerImpl.destroy();
+ playerImpl = null;
}
+
+ mBinder = null;
if (lockManager != null) lockManager.releaseWifiAndCpu();
if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID);
- mBinder = null;
- playerImpl = null;
- stopForeground(true);
- stopSelf();
+ animateOverlayAndFinishService();
+ }
+
+ private void animateOverlayAndFinishService() {
+ final int targetTranslationY = (int) (closeOverlayButton.getRootView().getHeight() - closeOverlayButton.getY());
+
+ closeOverlayButton.animate().setListener(null).cancel();
+ closeOverlayButton.animate()
+ .setInterpolator(new AnticipateInterpolator())
+ .translationY(targetTranslationY)
+ .setDuration(400)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ end();
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ end();
+ }
+
+ private void end() {
+ windowManager.removeView(closeOverlayView);
+
+ stopForeground(true);
+ stopSelf();
+ }
+ }).start();
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
- private void checkPositionBounds() {
- if (windowLayoutParams.x > screenWidth - windowLayoutParams.width)
- windowLayoutParams.x = (int) (screenWidth - windowLayoutParams.width);
- if (windowLayoutParams.x < 0) windowLayoutParams.x = 0;
- if (windowLayoutParams.y > screenHeight - windowLayoutParams.height)
- windowLayoutParams.y = (int) (screenHeight - windowLayoutParams.height);
- if (windowLayoutParams.y < 0) windowLayoutParams.y = 0;
+ /**
+ * @see #checkPopupPositionBounds(float, float)
+ */
+ @SuppressWarnings("UnusedReturnValue")
+ private boolean checkPopupPositionBounds() {
+ return checkPopupPositionBounds(screenWidth, screenHeight);
+ }
+
+ /**
+ * Check if {@link #popupLayoutParams}' position is within a arbitrary boundary that goes from (0,0) to (boundaryWidth,boundaryHeight).
+ *
+ * If it's out of these boundaries, {@link #popupLayoutParams}' position is changed and {@code true} is returned
+ * to represent this change.
+ *
+ * @return if the popup was out of bounds and have been moved back to it
+ */
+ private boolean checkPopupPositionBounds(final float boundaryWidth, final float boundaryHeight) {
+ if (DEBUG) {
+ Log.d(TAG, "checkPopupPositionBounds() called with: boundaryWidth = [" + boundaryWidth + "], boundaryHeight = [" + boundaryHeight + "]");
+ }
+
+ if (popupLayoutParams.x < 0) {
+ popupLayoutParams.x = 0;
+ return true;
+ } else if (popupLayoutParams.x > boundaryWidth - popupLayoutParams.width) {
+ popupLayoutParams.x = (int) (boundaryWidth - popupLayoutParams.width);
+ return true;
+ }
+
+ if (popupLayoutParams.y < 0) {
+ popupLayoutParams.y = 0;
+ return true;
+ } else if (popupLayoutParams.y > boundaryHeight - popupLayoutParams.height) {
+ popupLayoutParams.y = (int) (boundaryHeight - popupLayoutParams.height);
+ return true;
+ }
+
+ return false;
}
private void savePositionAndSize() {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(PopupVideoPlayer.this);
- sharedPreferences.edit().putInt(POPUP_SAVED_X, windowLayoutParams.x).apply();
- sharedPreferences.edit().putInt(POPUP_SAVED_Y, windowLayoutParams.y).apply();
- sharedPreferences.edit().putFloat(POPUP_SAVED_WIDTH, windowLayoutParams.width).apply();
+ sharedPreferences.edit().putInt(POPUP_SAVED_X, popupLayoutParams.x).apply();
+ sharedPreferences.edit().putInt(POPUP_SAVED_Y, popupLayoutParams.y).apply();
+ sharedPreferences.edit().putFloat(POPUP_SAVED_WIDTH, popupLayoutParams.width).apply();
}
private float getMinimumVideoHeight(float width) {
@@ -340,13 +450,13 @@ public final class PopupVideoPlayer extends Service {
if (height == -1) height = (int) getMinimumVideoHeight(width);
else height = (int) (height > maximumHeight ? maximumHeight : height < minimumHeight ? minimumHeight : height);
- windowLayoutParams.width = width;
- windowLayoutParams.height = height;
+ popupLayoutParams.width = width;
+ popupLayoutParams.height = height;
popupWidth = width;
popupHeight = height;
if (DEBUG) Log.d(TAG, "updatePopupSize() updated values: width = [" + width + "], height = [" + height + "]");
- windowManager.updateViewLayout(playerImpl.getRootView(), windowLayoutParams);
+ windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams);
}
protected void setRepeatModeRemote(final RemoteViews remoteViews, final int repeatMode) {
@@ -367,6 +477,12 @@ public final class PopupVideoPlayer extends Service {
}
}
+ private void updateWindowFlags(final int flags) {
+ if (popupLayoutParams == null || windowManager == null || playerImpl == null) return;
+
+ popupLayoutParams.flags = flags;
+ windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams);
+ }
///////////////////////////////////////////////////////////////////////////
protected class VideoPlayerImpl extends VideoPlayer implements View.OnLayoutChangeListener {
@@ -375,6 +491,7 @@ public final class PopupVideoPlayer extends Service {
private ImageView videoPlayPause;
private View extraOptionsView;
+ private View closingOverlayView;
@Override
public void handleIntent(Intent intent) {
@@ -395,12 +512,18 @@ public final class PopupVideoPlayer extends Service {
fullScreenButton = rootView.findViewById(R.id.fullScreenButton);
fullScreenButton.setOnClickListener(v -> onFullScreenButtonClicked());
videoPlayPause = rootView.findViewById(R.id.videoPlayPause);
- videoPlayPause.setOnClickListener(this::onPlayPauseButtonPressed);
extraOptionsView = rootView.findViewById(R.id.extraOptionsView);
+ closingOverlayView = rootView.findViewById(R.id.closingOverlay);
rootView.addOnLayoutChangeListener(this);
}
+ @Override
+ public void initListeners() {
+ super.initListeners();
+ videoPlayPause.setOnClickListener(v -> onPlayPause());
+ }
+
@Override
protected void setupSubtitleView(@NonNull SubtitleView view,
final float captionScale,
@@ -411,10 +534,6 @@ public final class PopupVideoPlayer extends Service {
view.setStyle(captionStyle);
}
- private void onPlayPauseButtonPressed(View ib) {
- onPlayPause();
- }
-
@Override
public void onLayoutChange(final View view, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
@@ -429,21 +548,6 @@ public final class PopupVideoPlayer extends Service {
super.destroy();
}
- @Override
- public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
- super.onLoadingComplete(imageUri, view, loadedImage);
- if (loadedImage != null) {
- // rebuild notification here since remote view does not release bitmaps, causing memory leaks
- notBuilder = createNotification();
-
- if (notRemoteView != null) {
- notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage);
- }
-
- updateNotification(-1);
- }
- }
-
@Override
public void onFullScreenButtonClicked() {
super.onFullScreenButtonClicked();
@@ -460,6 +564,7 @@ public final class PopupVideoPlayer extends Service {
this.getRepeatMode(),
this.getPlaybackSpeed(),
this.getPlaybackPitch(),
+ this.getPlaybackSkipSilence(),
this.getPlaybackQuality()
);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -472,7 +577,7 @@ public final class PopupVideoPlayer extends Service {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
- onClose();
+ closePopup();
}
@Override
@@ -511,14 +616,47 @@ public final class PopupVideoPlayer extends Service {
}
@Override
- protected int getDefaultResolutionIndex(final List sortedVideos) {
- return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos);
+ protected VideoPlaybackResolver.QualityResolver getQualityResolver() {
+ return new VideoPlaybackResolver.QualityResolver() {
+ @Override
+ public int getDefaultResolutionIndex(List sortedVideos) {
+ return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos);
+ }
+
+ @Override
+ public int getOverrideResolutionIndex(List sortedVideos,
+ String playbackQuality) {
+ return ListHelper.getPopupResolutionIndex(context, sortedVideos,
+ playbackQuality);
+ }
+ };
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Thumbnail Loading
+ //////////////////////////////////////////////////////////////////////////*/
+
+ @Override
+ public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
+ super.onLoadingComplete(imageUri, view, loadedImage);
+ // rebuild notification here since remote view does not release bitmaps,
+ // causing memory leaks
+ resetNotification();
+ updateNotification(-1);
}
@Override
- protected int getOverrideResolutionIndex(final List sortedVideos,
- final String playbackQuality) {
- return ListHelper.getPopupResolutionIndex(context, sortedVideos, playbackQuality);
+ public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
+ super.onLoadingFailed(imageUri, view, failReason);
+ resetNotification();
+ updateNotification(-1);
+ }
+
+ @Override
+ public void onLoadingCancelled(String imageUri, View view) {
+ super.onLoadingCancelled(imageUri, view);
+ resetNotification();
+ updateNotification(-1);
}
/*//////////////////////////////////////////////////////////////////////////
@@ -539,8 +677,8 @@ public final class PopupVideoPlayer extends Service {
}
private void updateMetadata() {
- if (activityListener != null && currentInfo != null) {
- activityListener.onMetadataUpdate(currentInfo);
+ if (activityListener != null && getCurrentMetadata() != null) {
+ activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata());
}
}
@@ -572,8 +710,9 @@ public final class PopupVideoPlayer extends Service {
public void onRepeatModeChanged(int i) {
super.onRepeatModeChanged(i);
setRepeatModeRemote(notRemoteView, i);
- updateNotification(-1);
updatePlayback();
+ resetNotification();
+ updateNotification(-1);
}
@Override
@@ -586,18 +725,17 @@ public final class PopupVideoPlayer extends Service {
// Playback Listener
//////////////////////////////////////////////////////////////////////////*/
- protected void onMetadataChanged(@NonNull final PlayQueueItem item,
- @Nullable final StreamInfo info,
- final int newPlayQueueIndex,
- final boolean hasPlayQueueItemChanged) {
- super.onMetadataChanged(item, info, newPlayQueueIndex, false);
+ protected void onMetadataChanged(@NonNull final MediaSourceTag tag) {
+ super.onMetadataChanged(tag);
+ resetNotification();
+ updateNotification(-1);
updateMetadata();
}
@Override
public void onPlaybackShutdown() {
super.onPlaybackShutdown();
- onClose();
+ closePopup();
}
/*//////////////////////////////////////////////////////////////////////////
@@ -623,7 +761,7 @@ public final class PopupVideoPlayer extends Service {
if (DEBUG) Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]");
switch (intent.getAction()) {
case ACTION_CLOSE:
- onClose();
+ closePopup();
break;
case ACTION_PLAY_PAUSE:
onPlayPause();
@@ -653,49 +791,70 @@ public final class PopupVideoPlayer extends Service {
@Override
public void onBlocked() {
super.onBlocked();
+ resetNotification();
updateNotification(R.drawable.ic_play_arrow_white);
}
@Override
public void onPlaying() {
super.onPlaying();
- updateNotification(R.drawable.ic_pause_white);
- videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white);
- lockManager.acquireWifiAndCpu();
+ updateWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS);
+
+ resetNotification();
+ updateNotification(R.drawable.ic_pause_white);
+
+ videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white);
hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
- // Check for new version
- //new CheckForNewAppVersionTask().execute();
+ startForeground(NOTIFICATION_ID, notBuilder.build());
+ lockManager.acquireWifiAndCpu();
}
@Override
public void onBuffering() {
super.onBuffering();
+ resetNotification();
updateNotification(R.drawable.ic_play_arrow_white);
}
@Override
public void onPaused() {
super.onPaused();
+
+ updateWindowFlags(IDLE_WINDOW_FLAGS);
+
+ resetNotification();
updateNotification(R.drawable.ic_play_arrow_white);
+
videoPlayPause.setBackgroundResource(R.drawable.ic_play_arrow_white);
lockManager.releaseWifiAndCpu();
+
+ stopForeground(false);
}
@Override
public void onPausedSeek() {
super.onPausedSeek();
- videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white);
+ resetNotification();
updateNotification(R.drawable.ic_play_arrow_white);
+
+ videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white);
}
@Override
public void onCompleted() {
super.onCompleted();
+
+ updateWindowFlags(IDLE_WINDOW_FLAGS);
+
+ resetNotification();
updateNotification(R.drawable.ic_replay_white);
+
videoPlayPause.setBackgroundResource(R.drawable.ic_replay_white);
lockManager.releaseWifiAndCpu();
+
+ stopForeground(false);
}
@Override
@@ -713,16 +872,15 @@ public final class PopupVideoPlayer extends Service {
super.hideControlsAndButton(duration, delay, videoPlayPause);
}
-
-
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
/*package-private*/ void enableVideoRenderer(final boolean enable) {
final int videoRendererIndex = getRendererIndex(C.TRACK_TYPE_VIDEO);
- if (trackSelector != null && videoRendererIndex != RENDERER_UNAVAILABLE) {
- trackSelector.setRendererDisabled(videoRendererIndex, !enable);
+ if (videoRendererIndex != RENDERER_UNAVAILABLE) {
+ trackSelector.setParameters(trackSelector.buildUponParameters()
+ .setRendererDisabled(videoRendererIndex, !enable));
}
}
@@ -734,12 +892,15 @@ public final class PopupVideoPlayer extends Service {
public TextView getResizingIndicator() {
return resizingIndicator;
}
+
+ public View getClosingOverlayView() {
+ return closingOverlayView;
+ }
}
- private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener {
+ private class PopupWindowGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener {
private int initialPopupX, initialPopupY;
private boolean isMoving;
-
private boolean isResizing;
@Override
@@ -775,10 +936,15 @@ public final class PopupVideoPlayer extends Service {
@Override
public boolean onDown(MotionEvent e) {
if (DEBUG) Log.d(TAG, "onDown() called with: e = [" + e + "]");
- initialPopupX = windowLayoutParams.x;
- initialPopupY = windowLayoutParams.y;
- popupWidth = windowLayoutParams.width;
- popupHeight = windowLayoutParams.height;
+
+ // Fix popup position when the user touch it, it may have the wrong one
+ // because the soft input is visible (the draggable area is currently resized).
+ checkPopupPositionBounds(closeOverlayView.getWidth(), closeOverlayView.getHeight());
+
+ initialPopupX = popupLayoutParams.x;
+ initialPopupY = popupLayoutParams.y;
+ popupWidth = popupLayoutParams.width;
+ popupHeight = popupLayoutParams.height;
return super.onDown(e);
}
@@ -786,20 +952,22 @@ public final class PopupVideoPlayer extends Service {
public void onLongPress(MotionEvent e) {
if (DEBUG) Log.d(TAG, "onLongPress() called with: e = [" + e + "]");
updateScreenSize();
- checkPositionBounds();
+ checkPopupPositionBounds();
updatePopupSize((int) screenWidth, -1);
}
@Override
- public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
- if (isResizing || playerImpl == null) return super.onScroll(e1, e2, distanceX, distanceY);
+ public boolean onScroll(MotionEvent initialEvent, MotionEvent movingEvent, float distanceX, float distanceY) {
+ if (isResizing || playerImpl == null) return super.onScroll(initialEvent, movingEvent, distanceX, distanceY);
+
+ if (!isMoving) {
+ animateView(closeOverlayButton, true, 200);
+ }
- if (playerImpl.getCurrentState() != BasePlayer.STATE_BUFFERING
- && (!isMoving || playerImpl.getControlsRoot().getAlpha() != 1f)) playerImpl.showControls(0);
isMoving = true;
- float diffX = (int) (e2.getRawX() - e1.getRawX()), posX = (int) (initialPopupX + diffX);
- float diffY = (int) (e2.getRawY() - e1.getRawY()), posY = (int) (initialPopupY + diffY);
+ float diffX = (int) (movingEvent.getRawX() - initialEvent.getRawX()), posX = (int) (initialPopupX + diffX);
+ float diffY = (int) (movingEvent.getRawY() - initialEvent.getRawY()), posY = (int) (initialPopupY + diffY);
if (posX > (screenWidth - popupWidth)) posX = (int) (screenWidth - popupWidth);
else if (posX < 0) posX = 0;
@@ -807,26 +975,49 @@ public final class PopupVideoPlayer extends Service {
if (posY > (screenHeight - popupHeight)) posY = (int) (screenHeight - popupHeight);
else if (posY < 0) posY = 0;
- windowLayoutParams.x = (int) posX;
- windowLayoutParams.y = (int) posY;
+ popupLayoutParams.x = (int) posX;
+ popupLayoutParams.y = (int) posY;
+
+ final View closingOverlayView = playerImpl.getClosingOverlayView();
+ if (isInsideClosingRadius(movingEvent)) {
+ if (closingOverlayView.getVisibility() == View.GONE) {
+ animateView(closingOverlayView, true, 250);
+ }
+ } else {
+ if (closingOverlayView.getVisibility() == View.VISIBLE) {
+ animateView(closingOverlayView, false, 0);
+ }
+ }
//noinspection PointlessBooleanExpression
- if (DEBUG && false) Log.d(TAG, "PopupVideoPlayer.onScroll = " +
- ", e1.getRaw = [" + e1.getRawX() + ", " + e1.getRawY() + "]" +
- ", e2.getRaw = [" + e2.getRawX() + ", " + e2.getRawY() + "]" +
- ", distanceXy = [" + distanceX + ", " + distanceY + "]" +
- ", posXy = [" + posX + ", " + posY + "]" +
- ", popupWh = [" + popupWidth + " x " + popupHeight + "]");
- windowManager.updateViewLayout(playerImpl.getRootView(), windowLayoutParams);
+ if (DEBUG && false) {
+ Log.d(TAG, "PopupVideoPlayer.onScroll = " +
+ ", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" + ", e1.getX,Y = [" + initialEvent.getX() + ", " + initialEvent.getY() + "]" +
+ ", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" + ", e2.getX,Y = [" + movingEvent.getX() + ", " + movingEvent.getY() + "]" +
+ ", distanceX,Y = [" + distanceX + ", " + distanceY + "]" +
+ ", posX,Y = [" + posX + ", " + posY + "]" +
+ ", popupW,H = [" + popupWidth + " x " + popupHeight + "]");
+ }
+ windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams);
return true;
}
- private void onScrollEnd() {
+ private void onScrollEnd(MotionEvent event) {
if (DEBUG) Log.d(TAG, "onScrollEnd() called");
if (playerImpl == null) return;
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
}
+
+ if (isInsideClosingRadius(event)) {
+ closePopup();
+ } else {
+ animateView(playerImpl.getClosingOverlayView(), false, 0);
+
+ if (!isPopupClosing) {
+ animateView(closeOverlayButton, false, 200);
+ }
+ }
}
@Override
@@ -836,14 +1027,11 @@ public final class PopupVideoPlayer extends Service {
final float absVelocityX = Math.abs(velocityX);
final float absVelocityY = Math.abs(velocityY);
- if (absVelocityX > shutdownFlingVelocity) {
- onClose();
- return true;
- } else if (Math.max(absVelocityX, absVelocityY) > tossFlingVelocity) {
- if (absVelocityX > tossFlingVelocity) windowLayoutParams.x = (int) velocityX;
- if (absVelocityY > tossFlingVelocity) windowLayoutParams.y = (int) velocityY;
- checkPositionBounds();
- windowManager.updateViewLayout(playerImpl.getRootView(), windowLayoutParams);
+ if (Math.max(absVelocityX, absVelocityY) > tossFlingVelocity) {
+ if (absVelocityX > tossFlingVelocity) popupLayoutParams.x = (int) velocityX;
+ if (absVelocityY > tossFlingVelocity) popupLayoutParams.y = (int) velocityY;
+ checkPopupPositionBounds();
+ windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams);
return true;
}
return false;
@@ -851,7 +1039,7 @@ public final class PopupVideoPlayer extends Service {
@Override
public boolean onTouch(View v, MotionEvent event) {
- gestureDetector.onTouchEvent(event);
+ popupGestureDetector.onTouchEvent(event);
if (playerImpl == null) return false;
if (event.getPointerCount() == 2 && !isResizing) {
if (DEBUG) Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.");
@@ -874,7 +1062,7 @@ public final class PopupVideoPlayer extends Service {
Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]");
if (isMoving) {
isMoving = false;
- onScrollEnd();
+ onScrollEnd(event);
}
if (isResizing) {
@@ -882,7 +1070,10 @@ public final class PopupVideoPlayer extends Service {
animateView(playerImpl.getResizingIndicator(), false, 100, 0);
playerImpl.changeState(playerImpl.getCurrentState());
}
- savePositionAndSize();
+
+ if (!isPopupClosing) {
+ savePositionAndSize();
+ }
}
v.performClick();
@@ -898,13 +1089,13 @@ public final class PopupVideoPlayer extends Service {
final float diff = Math.abs(firstPointerX - secondPointerX);
if (firstPointerX > secondPointerX) {
// second pointer is the anchor (the leftmost pointer)
- windowLayoutParams.x = (int) (event.getRawX() - diff);
+ popupLayoutParams.x = (int) (event.getRawX() - diff);
} else {
// first pointer is the anchor
- windowLayoutParams.x = (int) event.getRawX();
+ popupLayoutParams.x = (int) event.getRawX();
}
- checkPositionBounds();
+ checkPopupPositionBounds();
updateScreenSize();
final int width = (int) Math.min(screenWidth, diff);
@@ -912,5 +1103,29 @@ public final class PopupVideoPlayer extends Service {
return true;
}
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Utils
+ //////////////////////////////////////////////////////////////////////////*/
+
+ private int distanceFromCloseButton(MotionEvent popupMotionEvent) {
+ final int closeOverlayButtonX = closeOverlayButton.getLeft() + closeOverlayButton.getWidth() / 2;
+ final int closeOverlayButtonY = closeOverlayButton.getTop() + closeOverlayButton.getHeight() / 2;
+
+ float fingerX = popupLayoutParams.x + popupMotionEvent.getX();
+ float fingerY = popupLayoutParams.y + popupMotionEvent.getY();
+
+ return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2) + Math.pow(closeOverlayButtonY - fingerY, 2));
+ }
+
+ private float getClosingRadius() {
+ final int buttonRadius = closeOverlayButton.getWidth() / 2;
+ // 20% wider than the button itself
+ return buttonRadius * 1.2f;
+ }
+
+ private boolean isInsideClosingRadius(MotionEvent popupMotionEvent) {
+ return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius();
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java
index 8b96b651e..94305e6c4 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java
@@ -16,6 +16,7 @@ import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
+import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
@@ -187,6 +188,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
this.player.getRepeatMode(),
this.player.getPlaybackSpeed(),
this.player.getPlaybackPitch(),
+ this.player.getPlaybackSkipSilence(),
null
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
@@ -340,6 +342,13 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
return true;
});
+ final MenuItem share = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/3,
+ Menu.NONE, R.string.share);
+ share.setOnMenuItemClickListener(menuItem -> {
+ shareUrl(item.getTitle(), item.getUrl());
+ return true;
+ });
+
menu.show();
}
@@ -459,13 +468,16 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private void openPlaybackParameterDialog() {
if (player == null) return;
- PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(),
- player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag());
+ PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(),
+ player.getPlaybackSkipSilence()).show(getSupportFragmentManager(), getTag());
}
@Override
- public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) {
- if (player != null) player.setPlaybackParameters(playbackTempo, playbackPitch);
+ public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch,
+ boolean playbackSkipSilence) {
+ if (player != null) {
+ player.setPlaybackParameters(playbackTempo, playbackPitch, playbackSkipSilence);
+ }
}
////////////////////////////////////////////////////////////////////////////
@@ -509,6 +521,18 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
.show(getSupportFragmentManager(), getTag());
}
+ ////////////////////////////////////////////////////////////////////////////
+ // Share
+ ////////////////////////////////////////////////////////////////////////////
+
+ private void shareUrl(String subject, String url) {
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setType("text/plain");
+ intent.putExtra(Intent.EXTRA_SUBJECT, subject);
+ intent.putExtra(Intent.EXTRA_TEXT, url);
+ startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title)));
+ }
+
////////////////////////////////////////////////////////////////////////////
// Binding Service Listener
////////////////////////////////////////////////////////////////////////////
@@ -539,6 +563,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
if (player != null) {
progressLiveSync.setClickable(!player.isLiveEdge());
}
+
+ // this will make shure progressCurrentTime has the same width as progressEndTime
+ final ViewGroup.LayoutParams endTimeParams = progressEndTime.getLayoutParams();
+ final ViewGroup.LayoutParams currentTimeParams = progressCurrentTime.getLayoutParams();
+ currentTimeParams.width = progressEndTime.getWidth();
+ progressCurrentTime.setLayoutParams(currentTimeParams);
}
@Override
diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java
index 5ea1c74a0..679fc6645 100644
--- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java
@@ -29,7 +29,6 @@ import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.PorterDuff;
-import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.support.annotation.NonNull;
@@ -47,11 +46,9 @@ import android.widget.SeekBar;
import android.widget.TextView;
import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.MediaSource;
-import com.google.android.exoplayer2.source.MergingMediaSource;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.CaptionStyleCompat;
@@ -62,21 +59,17 @@ import com.google.android.exoplayer2.video.VideoListener;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.MediaFormat;
-import org.schabi.newpipe.extractor.Subtitles;
-import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
-import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
+import org.schabi.newpipe.player.resolver.MediaSourceTag;
+import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
import org.schabi.newpipe.util.AnimationUtils;
-import org.schabi.newpipe.util.ListHelper;
import java.util.ArrayList;
import java.util.List;
-import static com.google.android.exoplayer2.C.SELECTION_FLAG_AUTOSELECT;
-import static com.google.android.exoplayer2.C.TIME_UNSET;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
@@ -105,13 +98,12 @@ public abstract class VideoPlayer extends BasePlayer
public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis
public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds
- private ArrayList availableStreams;
+ private List availableStreams;
private int selectedStreamIndex;
- protected String playbackQuality;
-
protected boolean wasPlaying = false;
+ @NonNull final private VideoPlaybackResolver resolver;
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
@@ -162,6 +154,7 @@ public abstract class VideoPlayer extends BasePlayer
public VideoPlayer(String debugTag, Context context) {
super(context);
this.TAG = debugTag;
+ this.resolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver());
}
public void setup(View rootView) {
@@ -241,7 +234,8 @@ public abstract class VideoPlayer extends BasePlayer
// Setup audio session with onboard equalizer
if (Build.VERSION.SDK_INT >= 21) {
- trackSelector.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context));
+ trackSelector.setParameters(trackSelector.buildUponParameters()
+ .setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context)));
}
}
@@ -297,8 +291,9 @@ public abstract class VideoPlayer extends BasePlayer
0, Menu.NONE, R.string.caption_none);
captionOffItem.setOnMenuItemClickListener(menuItem -> {
final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT);
- if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) {
- trackSelector.setRendererDisabled(textRendererIndex, true);
+ if (textRendererIndex != RENDERER_UNAVAILABLE) {
+ trackSelector.setParameters(trackSelector.buildUponParameters()
+ .setRendererDisabled(textRendererIndex, true));
}
return true;
});
@@ -310,68 +305,61 @@ public abstract class VideoPlayer extends BasePlayer
i + 1, Menu.NONE, captionLanguage);
captionItem.setOnMenuItemClickListener(menuItem -> {
final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT);
- if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) {
+ if (textRendererIndex != RENDERER_UNAVAILABLE) {
trackSelector.setPreferredTextLanguage(captionLanguage);
- trackSelector.setRendererDisabled(textRendererIndex, false);
+ trackSelector.setParameters(trackSelector.buildUponParameters()
+ .setRendererDisabled(textRendererIndex, false));
}
return true;
});
}
captionPopupMenu.setOnDismissListener(this);
}
- /*//////////////////////////////////////////////////////////////////////////
- // Playback Listener
- //////////////////////////////////////////////////////////////////////////*/
- protected abstract int getDefaultResolutionIndex(final List sortedVideos);
- protected abstract int getOverrideResolutionIndex(final List sortedVideos, final String playbackQuality);
+ private void updateStreamRelatedViews() {
+ if (getCurrentMetadata() == null) return;
+
+ final MediaSourceTag tag = getCurrentMetadata();
+ final StreamInfo metadata = tag.getMetadata();
- protected void onMetadataChanged(@NonNull final PlayQueueItem item,
- @Nullable final StreamInfo info,
- final int newPlayQueueIndex,
- final boolean hasPlayQueueItemChanged) {
qualityTextView.setVisibility(View.GONE);
playbackSpeedTextView.setVisibility(View.GONE);
playbackEndTime.setVisibility(View.GONE);
playbackLiveSync.setVisibility(View.GONE);
- final StreamType streamType = info == null ? StreamType.NONE : info.getStreamType();
-
- switch (streamType) {
+ switch (metadata.getStreamType()) {
case AUDIO_STREAM:
surfaceView.setVisibility(View.GONE);
+ endScreen.setVisibility(View.VISIBLE);
playbackEndTime.setVisibility(View.VISIBLE);
break;
case AUDIO_LIVE_STREAM:
surfaceView.setVisibility(View.GONE);
+ endScreen.setVisibility(View.VISIBLE);
playbackLiveSync.setVisibility(View.VISIBLE);
break;
case LIVE_STREAM:
surfaceView.setVisibility(View.VISIBLE);
+ endScreen.setVisibility(View.GONE);
playbackLiveSync.setVisibility(View.VISIBLE);
break;
case VIDEO_STREAM:
- if (info.getVideoStreams().size() + info.getVideoOnlyStreams().size() == 0) break;
-
- final List videos = ListHelper.getSortedStreamVideosList(context,
- info.getVideoStreams(), info.getVideoOnlyStreams(), false);
- availableStreams = new ArrayList<>(videos);
- if (playbackQuality == null) {
- selectedStreamIndex = getDefaultResolutionIndex(videos);
- } else {
- selectedStreamIndex = getOverrideResolutionIndex(videos, getPlaybackQuality());
- }
+ if (metadata.getVideoStreams().size() + metadata.getVideoOnlyStreams().size() == 0)
+ break;
+ availableStreams = tag.getSortedAvailableVideoStreams();
+ selectedStreamIndex = tag.getSelectedVideoStreamIndex();
buildQualityMenu();
- qualityTextView.setVisibility(View.VISIBLE);
+ qualityTextView.setVisibility(View.VISIBLE);
surfaceView.setVisibility(View.VISIBLE);
default:
+ endScreen.setVisibility(View.GONE);
playbackEndTime.setVisibility(View.VISIBLE);
break;
}
@@ -379,69 +367,21 @@ public abstract class VideoPlayer extends BasePlayer
buildPlaybackSpeedMenu();
playbackSpeedTextView.setVisibility(View.VISIBLE);
}
+ /*//////////////////////////////////////////////////////////////////////////
+ // Playback Listener
+ //////////////////////////////////////////////////////////////////////////*/
+
+ protected abstract VideoPlaybackResolver.QualityResolver getQualityResolver();
+
+ protected void onMetadataChanged(@NonNull final MediaSourceTag tag) {
+ super.onMetadataChanged(tag);
+ updateStreamRelatedViews();
+ }
@Override
@Nullable
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
- final MediaSource liveSource = super.sourceOf(item, info);
- if (liveSource != null) return liveSource;
-
- List mediaSources = new ArrayList<>();
-
- // Create video stream source
- final List videos = ListHelper.getSortedStreamVideosList(context,
- info.getVideoStreams(), info.getVideoOnlyStreams(), false);
- final int index;
- if (videos.isEmpty()) {
- index = -1;
- } else if (playbackQuality == null) {
- index = getDefaultResolutionIndex(videos);
- } else {
- index = getOverrideResolutionIndex(videos, getPlaybackQuality());
- }
- final VideoStream video = index >= 0 && index < videos.size() ? videos.get(index) : null;
- if (video != null) {
- final MediaSource streamSource = buildMediaSource(video.getUrl(),
- PlayerHelper.cacheKeyOf(info, video),
- MediaFormat.getSuffixById(video.getFormatId()));
- mediaSources.add(streamSource);
- }
-
- // Create optional audio stream source
- final List audioStreams = info.getAudioStreams();
- final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get(
- ListHelper.getDefaultAudioFormat(context, audioStreams));
- // Use the audio stream if there is no video stream, or
- // Merge with audio stream in case if video does not contain audio
- if (audio != null && ((video != null && video.isVideoOnly) || video == null)) {
- final MediaSource audioSource = buildMediaSource(audio.getUrl(),
- PlayerHelper.cacheKeyOf(info, audio),
- MediaFormat.getSuffixById(audio.getFormatId()));
- mediaSources.add(audioSource);
- }
-
- // If there is no audio or video sources, then this media source cannot be played back
- if (mediaSources.isEmpty()) return null;
- // Below are auxiliary media sources
-
- // Create subtitle sources
- for (final Subtitles subtitle : info.getSubtitles()) {
- final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType());
- if (mimeType == null) continue;
-
- final Format textFormat = Format.createTextSampleFormat(null, mimeType,
- SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle));
- final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
- .createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET);
- mediaSources.add(textSource);
- }
-
- if (mediaSources.size() == 1) {
- return mediaSources.get(0);
- } else {
- return new MergingMediaSource(mediaSources.toArray(
- new MediaSource[mediaSources.size()]));
- }
+ return resolver.resolve(info);
}
/*//////////////////////////////////////////////////////////////////////////
@@ -460,7 +400,6 @@ public abstract class VideoPlayer extends BasePlayer
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
- animateView(endScreen, false, 0);
loadingPanel.setBackgroundColor(Color.BLACK);
animateView(loadingPanel, true, 0);
animateView(surfaceForeground, true, 100);
@@ -470,6 +409,8 @@ public abstract class VideoPlayer extends BasePlayer
public void onPlaying() {
super.onPlaying();
+ updateStreamRelatedViews();
+
showAndAnimateControl(-1, true);
playbackSeekBar.setEnabled(true);
@@ -480,14 +421,12 @@ public abstract class VideoPlayer extends BasePlayer
loadingPanel.setVisibility(View.GONE);
animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200);
- animateView(endScreen, false, 0);
}
@Override
public void onBuffering() {
if (DEBUG) Log.d(TAG, "onBuffering() called");
loadingPanel.setBackgroundColor(Color.TRANSPARENT);
- animateView(loadingPanel, true, 500);
}
@Override
@@ -552,8 +491,7 @@ public abstract class VideoPlayer extends BasePlayer
final int textRenderer = getRendererIndex(C.TRACK_TYPE_TEXT);
if (captionTextView == null) return;
- if (trackSelector == null || trackSelector.getCurrentMappedTrackInfo() == null ||
- textRenderer == RENDERER_UNAVAILABLE) {
+ if (trackSelector.getCurrentMappedTrackInfo() == null || textRenderer == RENDERER_UNAVAILABLE) {
captionTextView.setVisibility(View.GONE);
return;
}
@@ -575,8 +513,8 @@ public abstract class VideoPlayer extends BasePlayer
// Build UI
buildCaptionMenu(availableLanguages);
- if (trackSelector.getRendererDisabled(textRenderer) || preferredLanguage == null ||
- !availableLanguages.contains(preferredLanguage)) {
+ if (trackSelector.getParameters().getRendererDisabled(textRenderer) ||
+ preferredLanguage == null || !availableLanguages.contains(preferredLanguage)) {
captionTextView.setText(R.string.caption_none);
} else {
captionTextView.setText(preferredLanguage);
@@ -905,11 +843,12 @@ public abstract class VideoPlayer extends BasePlayer
//////////////////////////////////////////////////////////////////////////*/
public void setPlaybackQuality(final String quality) {
- this.playbackQuality = quality;
+ this.resolver.setPlaybackQuality(quality);
}
+ @Nullable
public String getPlaybackQuality() {
- return playbackQuality;
+ return resolver.getPlaybackQuality();
}
public AspectRatioFrameLayout getAspectRatioFrameLayout() {
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java
index b174ed3ed..63c0bf333 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java
@@ -39,10 +39,13 @@ public class MediaSessionManager {
return MediaButtonReceiver.handleIntent(mediaSession, intent);
}
+ /**
+ * Should be called on player destruction to prevent leakage.
+ * */
public void dispose() {
this.sessionConnector.setPlayer(null, null);
this.sessionConnector.setQueueNavigator(null);
this.mediaSession.setActive(false);
this.mediaSession.release();
- }
+ }
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
index 7c7d87791..d6453f579 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
@@ -21,25 +21,34 @@ import static org.schabi.newpipe.player.BasePlayer.DEBUG;
public class PlaybackParameterDialog extends DialogFragment {
@NonNull private static final String TAG = "PlaybackParameterDialog";
- public static final double MINIMUM_PLAYBACK_VALUE = 0.25f;
+ // Minimum allowable range in ExoPlayer
+ public static final double MINIMUM_PLAYBACK_VALUE = 0.10f;
public static final double MAXIMUM_PLAYBACK_VALUE = 3.00f;
public static final char STEP_UP_SIGN = '+';
public static final char STEP_DOWN_SIGN = '-';
- public static final double PLAYBACK_STEP_VALUE = 0.05f;
- public static final double NIGHTCORE_TEMPO = 1.20f;
- public static final double NIGHTCORE_PITCH_LOWER = 1.15f;
- public static final double NIGHTCORE_PITCH_UPPER = 1.25f;
+ public static final double STEP_ONE_PERCENT_VALUE = 0.01f;
+ public static final double STEP_FIVE_PERCENT_VALUE = 0.05f;
+ public static final double STEP_TEN_PERCENT_VALUE = 0.10f;
+ public static final double STEP_TWENTY_FIVE_PERCENT_VALUE = 0.25f;
+ public static final double STEP_ONE_HUNDRED_PERCENT_VALUE = 1.00f;
public static final double DEFAULT_TEMPO = 1.00f;
public static final double DEFAULT_PITCH = 1.00f;
+ public static final double DEFAULT_STEP = STEP_TWENTY_FIVE_PERCENT_VALUE;
+ public static final boolean DEFAULT_SKIP_SILENCE = false;
@NonNull private static final String INITIAL_TEMPO_KEY = "initial_tempo_key";
@NonNull private static final String INITIAL_PITCH_KEY = "initial_pitch_key";
+ @NonNull private static final String TEMPO_KEY = "tempo_key";
+ @NonNull private static final String PITCH_KEY = "pitch_key";
+ @NonNull private static final String STEP_SIZE_KEY = "step_size_key";
+
public interface Callback {
- void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch);
+ void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch,
+ final boolean playbackSkipSilence);
}
@Nullable private Callback callback;
@@ -50,6 +59,11 @@ public class PlaybackParameterDialog extends DialogFragment {
private double initialTempo = DEFAULT_TEMPO;
private double initialPitch = DEFAULT_PITCH;
+ private boolean initialSkipSilence = DEFAULT_SKIP_SILENCE;
+
+ private double tempo = DEFAULT_TEMPO;
+ private double pitch = DEFAULT_PITCH;
+ private double stepSize = DEFAULT_STEP;
@Nullable private SeekBar tempoSlider;
@Nullable private TextView tempoMinimumText;
@@ -65,16 +79,26 @@ public class PlaybackParameterDialog extends DialogFragment {
@Nullable private TextView pitchStepDownText;
@Nullable private TextView pitchStepUpText;
- @Nullable private CheckBox unhookingCheckbox;
+ @Nullable private TextView stepSizeOnePercentText;
+ @Nullable private TextView stepSizeFivePercentText;
+ @Nullable private TextView stepSizeTenPercentText;
+ @Nullable private TextView stepSizeTwentyFivePercentText;
+ @Nullable private TextView stepSizeOneHundredPercentText;
- @Nullable private TextView nightCorePresetText;
- @Nullable private TextView resetPresetText;
+ @Nullable private CheckBox unhookingCheckbox;
+ @Nullable private CheckBox skipSilenceCheckbox;
public static PlaybackParameterDialog newInstance(final double playbackTempo,
- final double playbackPitch) {
+ final double playbackPitch,
+ final boolean playbackSkipSilence) {
PlaybackParameterDialog dialog = new PlaybackParameterDialog();
dialog.initialTempo = playbackTempo;
dialog.initialPitch = playbackPitch;
+
+ dialog.tempo = playbackTempo;
+ dialog.pitch = playbackPitch;
+
+ dialog.initialSkipSilence = playbackSkipSilence;
return dialog;
}
@@ -98,6 +122,10 @@ public class PlaybackParameterDialog extends DialogFragment {
if (savedInstanceState != null) {
initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO);
initialPitch = savedInstanceState.getDouble(INITIAL_PITCH_KEY, DEFAULT_PITCH);
+
+ tempo = savedInstanceState.getDouble(TEMPO_KEY, DEFAULT_TEMPO);
+ pitch = savedInstanceState.getDouble(PITCH_KEY, DEFAULT_PITCH);
+ stepSize = savedInstanceState.getDouble(STEP_SIZE_KEY, DEFAULT_STEP);
}
}
@@ -106,6 +134,10 @@ public class PlaybackParameterDialog extends DialogFragment {
super.onSaveInstanceState(outState);
outState.putDouble(INITIAL_TEMPO_KEY, initialTempo);
outState.putDouble(INITIAL_PITCH_KEY, initialPitch);
+
+ outState.putDouble(TEMPO_KEY, getCurrentTempo());
+ outState.putDouble(PITCH_KEY, getCurrentPitch());
+ outState.putDouble(STEP_SIZE_KEY, getCurrentStepSize());
}
/*//////////////////////////////////////////////////////////////////////////
@@ -123,7 +155,9 @@ public class PlaybackParameterDialog extends DialogFragment {
.setView(view)
.setCancelable(true)
.setNegativeButton(R.string.cancel, (dialogInterface, i) ->
- setPlaybackParameters(initialTempo, initialPitch))
+ setPlaybackParameters(initialTempo, initialPitch, initialSkipSilence))
+ .setNeutralButton(R.string.playback_reset, (dialogInterface, i) ->
+ setPlaybackParameters(DEFAULT_TEMPO, DEFAULT_PITCH, DEFAULT_SKIP_SILENCE))
.setPositiveButton(R.string.finish, (dialogInterface, i) ->
setCurrentPlaybackParameters());
@@ -136,9 +170,13 @@ public class PlaybackParameterDialog extends DialogFragment {
private void setupControlViews(@NonNull View rootView) {
setupHookingControl(rootView);
+ setupSkipSilenceControl(rootView);
+
setupTempoControl(rootView);
setupPitchControl(rootView);
- setupPresetControl(rootView);
+
+ changeStepSize(stepSize);
+ setupStepSizeSelector(rootView);
}
private void setupTempoControl(@NonNull View rootView) {
@@ -150,31 +188,15 @@ public class PlaybackParameterDialog extends DialogFragment {
tempoStepDownText = rootView.findViewById(R.id.tempoStepDown);
if (tempoCurrentText != null)
- tempoCurrentText.setText(PlayerHelper.formatSpeed(initialTempo));
+ tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo));
if (tempoMaximumText != null)
tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE));
if (tempoMinimumText != null)
tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE));
- if (tempoStepUpText != null) {
- tempoStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE));
- tempoStepUpText.setOnClickListener(view -> {
- onTempoSliderUpdated(getCurrentTempo() + PLAYBACK_STEP_VALUE);
- setCurrentPlaybackParameters();
- });
- }
-
- if (tempoStepDownText != null) {
- tempoStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE));
- tempoStepDownText.setOnClickListener(view -> {
- onTempoSliderUpdated(getCurrentTempo() - PLAYBACK_STEP_VALUE);
- setCurrentPlaybackParameters();
- });
- }
-
if (tempoSlider != null) {
tempoSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE));
- tempoSlider.setProgress(strategy.progressOf(initialTempo));
+ tempoSlider.setProgress(strategy.progressOf(tempo));
tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener());
}
}
@@ -188,31 +210,15 @@ public class PlaybackParameterDialog extends DialogFragment {
pitchStepUpText = rootView.findViewById(R.id.pitchStepUp);
if (pitchCurrentText != null)
- pitchCurrentText.setText(PlayerHelper.formatPitch(initialPitch));
+ pitchCurrentText.setText(PlayerHelper.formatPitch(pitch));
if (pitchMaximumText != null)
pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE));
if (pitchMinimumText != null)
pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE));
- if (pitchStepUpText != null) {
- pitchStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE));
- pitchStepUpText.setOnClickListener(view -> {
- onPitchSliderUpdated(getCurrentPitch() + PLAYBACK_STEP_VALUE);
- setCurrentPlaybackParameters();
- });
- }
-
- if (pitchStepDownText != null) {
- pitchStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE));
- pitchStepDownText.setOnClickListener(view -> {
- onPitchSliderUpdated(getCurrentPitch() - PLAYBACK_STEP_VALUE);
- setCurrentPlaybackParameters();
- });
- }
-
if (pitchSlider != null) {
pitchSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE));
- pitchSlider.setProgress(strategy.progressOf(initialPitch));
+ pitchSlider.setProgress(strategy.progressOf(pitch));
pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener());
}
}
@@ -220,7 +226,7 @@ public class PlaybackParameterDialog extends DialogFragment {
private void setupHookingControl(@NonNull View rootView) {
unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox);
if (unhookingCheckbox != null) {
- unhookingCheckbox.setChecked(initialPitch != initialTempo);
+ unhookingCheckbox.setChecked(pitch != tempo);
unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
if (isChecked) return;
// When unchecked, slide back to the minimum of current tempo or pitch
@@ -231,24 +237,84 @@ public class PlaybackParameterDialog extends DialogFragment {
}
}
- private void setupPresetControl(@NonNull View rootView) {
- nightCorePresetText = rootView.findViewById(R.id.presetNightcore);
- if (nightCorePresetText != null) {
- nightCorePresetText.setOnClickListener(view -> {
- final double randomPitch = NIGHTCORE_PITCH_LOWER +
- Math.random() * (NIGHTCORE_PITCH_UPPER - NIGHTCORE_PITCH_LOWER);
+ private void setupSkipSilenceControl(@NonNull View rootView) {
+ skipSilenceCheckbox = rootView.findViewById(R.id.skipSilenceCheckbox);
+ if (skipSilenceCheckbox != null) {
+ skipSilenceCheckbox.setChecked(initialSkipSilence);
+ skipSilenceCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) ->
+ setCurrentPlaybackParameters());
+ }
+ }
- setTempoSlider(NIGHTCORE_TEMPO);
- setPitchSlider(randomPitch);
+ private void setupStepSizeSelector(@NonNull final View rootView) {
+ stepSizeOnePercentText = rootView.findViewById(R.id.stepSizeOnePercent);
+ stepSizeFivePercentText = rootView.findViewById(R.id.stepSizeFivePercent);
+ stepSizeTenPercentText = rootView.findViewById(R.id.stepSizeTenPercent);
+ stepSizeTwentyFivePercentText = rootView.findViewById(R.id.stepSizeTwentyFivePercent);
+ stepSizeOneHundredPercentText = rootView.findViewById(R.id.stepSizeOneHundredPercent);
+
+ if (stepSizeOnePercentText != null) {
+ stepSizeOnePercentText.setText(getPercentString(STEP_ONE_PERCENT_VALUE));
+ stepSizeOnePercentText.setOnClickListener(view ->
+ changeStepSize(STEP_ONE_PERCENT_VALUE));
+ }
+
+ if (stepSizeFivePercentText != null) {
+ stepSizeFivePercentText.setText(getPercentString(STEP_FIVE_PERCENT_VALUE));
+ stepSizeFivePercentText.setOnClickListener(view ->
+ changeStepSize(STEP_FIVE_PERCENT_VALUE));
+ }
+
+ if (stepSizeTenPercentText != null) {
+ stepSizeTenPercentText.setText(getPercentString(STEP_TEN_PERCENT_VALUE));
+ stepSizeTenPercentText.setOnClickListener(view ->
+ changeStepSize(STEP_TEN_PERCENT_VALUE));
+ }
+
+ if (stepSizeTwentyFivePercentText != null) {
+ stepSizeTwentyFivePercentText.setText(getPercentString(STEP_TWENTY_FIVE_PERCENT_VALUE));
+ stepSizeTwentyFivePercentText.setOnClickListener(view ->
+ changeStepSize(STEP_TWENTY_FIVE_PERCENT_VALUE));
+ }
+
+ if (stepSizeOneHundredPercentText != null) {
+ stepSizeOneHundredPercentText.setText(getPercentString(STEP_ONE_HUNDRED_PERCENT_VALUE));
+ stepSizeOneHundredPercentText.setOnClickListener(view ->
+ changeStepSize(STEP_ONE_HUNDRED_PERCENT_VALUE));
+ }
+ }
+
+ private void changeStepSize(final double stepSize) {
+ this.stepSize = stepSize;
+
+ if (tempoStepUpText != null) {
+ tempoStepUpText.setText(getStepUpPercentString(stepSize));
+ tempoStepUpText.setOnClickListener(view -> {
+ onTempoSliderUpdated(getCurrentTempo() + stepSize);
setCurrentPlaybackParameters();
});
}
- resetPresetText = rootView.findViewById(R.id.presetReset);
- if (resetPresetText != null) {
- resetPresetText.setOnClickListener(view -> {
- setTempoSlider(DEFAULT_TEMPO);
- setPitchSlider(DEFAULT_PITCH);
+ if (tempoStepDownText != null) {
+ tempoStepDownText.setText(getStepDownPercentString(stepSize));
+ tempoStepDownText.setOnClickListener(view -> {
+ onTempoSliderUpdated(getCurrentTempo() - stepSize);
+ setCurrentPlaybackParameters();
+ });
+ }
+
+ if (pitchStepUpText != null) {
+ pitchStepUpText.setText(getStepUpPercentString(stepSize));
+ pitchStepUpText.setOnClickListener(view -> {
+ onPitchSliderUpdated(getCurrentPitch() + stepSize);
+ setCurrentPlaybackParameters();
+ });
+ }
+
+ if (pitchStepDownText != null) {
+ pitchStepDownText.setText(getStepDownPercentString(stepSize));
+ pitchStepDownText.setOnClickListener(view -> {
+ onPitchSliderUpdated(getCurrentPitch() - stepSize);
setCurrentPlaybackParameters();
});
}
@@ -342,10 +408,11 @@ public class PlaybackParameterDialog extends DialogFragment {
//////////////////////////////////////////////////////////////////////////*/
private void setCurrentPlaybackParameters() {
- setPlaybackParameters(getCurrentTempo(), getCurrentPitch());
+ setPlaybackParameters(getCurrentTempo(), getCurrentPitch(), getCurrentSkipSilence());
}
- private void setPlaybackParameters(final double tempo, final double pitch) {
+ private void setPlaybackParameters(final double tempo, final double pitch,
+ final boolean skipSilence) {
if (callback != null && tempoCurrentText != null && pitchCurrentText != null) {
if (DEBUG) Log.d(TAG, "Setting playback parameters to " +
"tempo=[" + tempo + "], " +
@@ -353,27 +420,40 @@ public class PlaybackParameterDialog extends DialogFragment {
tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo));
pitchCurrentText.setText(PlayerHelper.formatPitch(pitch));
- callback.onPlaybackParameterChanged((float) tempo, (float) pitch);
+ callback.onPlaybackParameterChanged((float) tempo, (float) pitch, skipSilence);
}
}
private double getCurrentTempo() {
- return tempoSlider == null ? initialTempo : strategy.valueOf(
+ return tempoSlider == null ? tempo : strategy.valueOf(
tempoSlider.getProgress());
}
private double getCurrentPitch() {
- return pitchSlider == null ? initialPitch : strategy.valueOf(
+ return pitchSlider == null ? pitch : strategy.valueOf(
pitchSlider.getProgress());
}
+ private double getCurrentStepSize() {
+ return stepSize;
+ }
+
+ private boolean getCurrentSkipSilence() {
+ return skipSilenceCheckbox != null && skipSilenceCheckbox.isChecked();
+ }
+
@NonNull
private static String getStepUpPercentString(final double percent) {
- return STEP_UP_SIGN + PlayerHelper.formatPitch(percent);
+ return STEP_UP_SIGN + getPercentString(percent);
}
@NonNull
private static String getStepDownPercentString(final double percent) {
- return STEP_DOWN_SIGN + PlayerHelper.formatPitch(percent);
+ return STEP_DOWN_SIGN + getPercentString(percent);
+ }
+
+ @NonNull
+ private static String getPercentString(final double percent) {
+ return PlayerHelper.formatPitch(percent);
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
index dbe0e9f46..ae187a834 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
@@ -4,6 +4,7 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
+import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.accessibility.CaptioningManager;
@@ -28,6 +29,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
+import java.lang.annotation.Retention;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
@@ -42,6 +44,8 @@ import java.util.concurrent.TimeUnit;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FILL;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.*;
public class PlayerHelper {
private PlayerHelper() {}
@@ -51,6 +55,14 @@ public class PlayerHelper {
private static final NumberFormat speedFormatter = new DecimalFormat("0.##x");
private static final NumberFormat pitchFormatter = new DecimalFormat("##%");
+ @Retention(SOURCE)
+ @IntDef({MINIMIZE_ON_EXIT_MODE_NONE, MINIMIZE_ON_EXIT_MODE_BACKGROUND,
+ MINIMIZE_ON_EXIT_MODE_POPUP})
+ public @interface MinimizeMode {
+ int MINIMIZE_ON_EXIT_MODE_NONE = 0;
+ int MINIMIZE_ON_EXIT_MODE_BACKGROUND = 1;
+ int MINIMIZE_ON_EXIT_MODE_POPUP = 2;
+ }
////////////////////////////////////////////////////////////////////////////
// Exposed helpers
////////////////////////////////////////////////////////////////////////////
@@ -173,6 +185,22 @@ public class PlayerHelper {
return isAutoQueueEnabled(context, false);
}
+ @MinimizeMode
+ public static int getMinimizeOnExitAction(@NonNull final Context context) {
+ final String defaultAction = context.getString(R.string.minimize_on_exit_none_key);
+ final String popupAction = context.getString(R.string.minimize_on_exit_popup_key);
+ final String backgroundAction = context.getString(R.string.minimize_on_exit_background_key);
+
+ final String action = getMinimizeOnExitAction(context, defaultAction);
+ if (action.equals(popupAction)) {
+ return MINIMIZE_ON_EXIT_MODE_POPUP;
+ } else if (action.equals(backgroundAction)) {
+ return MINIMIZE_ON_EXIT_MODE_BACKGROUND;
+ } else {
+ return MINIMIZE_ON_EXIT_MODE_NONE;
+ }
+ }
+
@NonNull
public static SeekParameters getSeekParameters(@NonNull final Context context) {
return isUsingInexactSeek(context, false) ?
@@ -213,7 +241,6 @@ public class PlayerHelper {
public static TrackSelection.Factory getQualitySelector(@NonNull final Context context,
@NonNull final BandwidthMeter meter) {
return new AdaptiveTrackSelection.Factory(meter,
- AdaptiveTrackSelection.DEFAULT_MAX_INITIAL_BITRATE,
/*bufferDurationRequiredForQualityIncrease=*/1000,
AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
@@ -224,10 +251,6 @@ public class PlayerHelper {
return true;
}
- public static int getShutdownFlingVelocity(@NonNull final Context context) {
- return 10000;
- }
-
public static int getTossFlingVelocity(@NonNull final Context context) {
return 2500;
}
@@ -249,7 +272,6 @@ public class PlayerHelper {
* System font scaling:
* Very small - 0.25f, Small - 0.5f, Normal - 1.0f, Large - 1.5f, Very Large - 2.0f
* */
- @NonNull
public static float getCaptionScale(@NonNull final Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return 1f;
@@ -322,4 +344,10 @@ public class PlayerHelper {
return sp.getFloat(context.getString(R.string.screen_brightness_key), screenBrightness);
}
}
+
+ private static String getMinimizeOnExitAction(@NonNull final Context context,
+ final String key) {
+ return getPreferences(context).getString(context.getString(R.string.minimize_on_exit_key),
+ key);
+ }
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java
index 8d498a9bf..2f233c464 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java
@@ -4,6 +4,7 @@ import android.support.annotation.NonNull;
import android.util.Log;
import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.upstream.Allocator;
@@ -11,7 +12,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import java.io.IOException;
-public class FailedMediaSource implements ManagedMediaSource {
+public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSource {
private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode());
public static class FailedMediaSourceException extends Exception {
@@ -72,11 +73,6 @@ public class FailedMediaSource implements ManagedMediaSource {
return System.currentTimeMillis() >= retryTimestamp;
}
- @Override
- public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
- Log.e(TAG, "Loading failed source: ", error);
- }
-
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
throw new IOException(error);
@@ -90,8 +86,14 @@ public class FailedMediaSource implements ManagedMediaSource {
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {}
+
@Override
- public void releaseSource() {}
+ protected void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
+ Log.e(TAG, "Loading failed source: ", error);
+ }
+
+ @Override
+ protected void releaseSourceInternal() {}
@Override
public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity,
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java
index 1a9cfeb4d..c39b0a03d 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java
@@ -1,10 +1,12 @@
package org.schabi.newpipe.player.mediasource;
+import android.os.Handler;
import android.support.annotation.NonNull;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.upstream.Allocator;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
@@ -34,7 +36,8 @@ public class LoadedMediaSource implements ManagedMediaSource {
}
@Override
- public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+ public void prepareSource(ExoPlayer player, boolean isTopLevelSource,
+ SourceInfoRefreshListener listener) {
source.prepareSource(player, isTopLevelSource, listener);
}
@@ -54,8 +57,18 @@ public class LoadedMediaSource implements ManagedMediaSource {
}
@Override
- public void releaseSource() {
- source.releaseSource();
+ public void releaseSource(SourceInfoRefreshListener listener) {
+ source.releaseSource(listener);
+ }
+
+ @Override
+ public void addEventListener(Handler handler, MediaSourceEventListener eventListener) {
+ source.addEventListener(handler, eventListener);
+ }
+
+ @Override
+ public void removeEventListener(MediaSourceEventListener eventListener) {
+ source.removeEventListener(eventListener);
}
@Override
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java
index 310f1062b..5fe107657 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java
@@ -3,14 +3,14 @@ package org.schabi.newpipe.player.mediasource;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
+import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ShuffleOrder;
public class ManagedMediaSourcePlaylist {
- @NonNull private final DynamicConcatenatingMediaSource internalSource;
+ @NonNull private final ConcatenatingMediaSource internalSource;
public ManagedMediaSourcePlaylist() {
- internalSource = new DynamicConcatenatingMediaSource(/*isPlaylistAtomic=*/false,
+ internalSource = new ConcatenatingMediaSource(/*isPlaylistAtomic=*/false,
new ShuffleOrder.UnshuffledShuffleOrder(0));
}
@@ -32,12 +32,8 @@ public class ManagedMediaSourcePlaylist {
null : (ManagedMediaSource) internalSource.getMediaSource(index);
}
- public void dispose() {
- internalSource.releaseSource();
- }
-
@NonNull
- public DynamicConcatenatingMediaSource getParentMediaSource() {
+ public ConcatenatingMediaSource getParentMediaSource() {
return internalSource;
}
@@ -46,7 +42,7 @@ public class ManagedMediaSourcePlaylist {
//////////////////////////////////////////////////////////////////////////*/
/**
- * Expands the {@link DynamicConcatenatingMediaSource} by appending it with a
+ * Expands the {@link ConcatenatingMediaSource} by appending it with a
* {@link PlaceholderMediaSource}.
*
* @see #append(ManagedMediaSource)
@@ -56,17 +52,17 @@ public class ManagedMediaSourcePlaylist {
}
/**
- * Appends a {@link ManagedMediaSource} to the end of {@link DynamicConcatenatingMediaSource}.
- * @see DynamicConcatenatingMediaSource#addMediaSource
+ * Appends a {@link ManagedMediaSource} to the end of {@link ConcatenatingMediaSource}.
+ * @see ConcatenatingMediaSource#addMediaSource
* */
public synchronized void append(@NonNull final ManagedMediaSource source) {
internalSource.addMediaSource(source);
}
/**
- * Removes a {@link ManagedMediaSource} from {@link DynamicConcatenatingMediaSource}
+ * Removes a {@link ManagedMediaSource} from {@link ConcatenatingMediaSource}
* at the given index. If this index is out of bound, then the removal is ignored.
- * @see DynamicConcatenatingMediaSource#removeMediaSource(int)
+ * @see ConcatenatingMediaSource#removeMediaSource(int)
* */
public synchronized void remove(final int index) {
if (index < 0 || index > internalSource.getSize()) return;
@@ -75,10 +71,10 @@ public class ManagedMediaSourcePlaylist {
}
/**
- * Moves a {@link ManagedMediaSource} in {@link DynamicConcatenatingMediaSource}
+ * Moves a {@link ManagedMediaSource} in {@link ConcatenatingMediaSource}
* from the given source index to the target index. If either index is out of bound,
* then the call is ignored.
- * @see DynamicConcatenatingMediaSource#moveMediaSource(int, int)
+ * @see ConcatenatingMediaSource#moveMediaSource(int, int)
* */
public synchronized void move(final int source, final int target) {
if (source < 0 || target < 0) return;
@@ -99,7 +95,7 @@ public class ManagedMediaSourcePlaylist {
}
/**
- * Updates the {@link ManagedMediaSource} in {@link DynamicConcatenatingMediaSource}
+ * Updates the {@link ManagedMediaSource} in {@link ConcatenatingMediaSource}
* at the given index with a given {@link ManagedMediaSource}.
* @see #update(int, ManagedMediaSource, Runnable)
* */
@@ -108,11 +104,11 @@ public class ManagedMediaSourcePlaylist {
}
/**
- * Updates the {@link ManagedMediaSource} in {@link DynamicConcatenatingMediaSource}
+ * Updates the {@link ManagedMediaSource} in {@link ConcatenatingMediaSource}
* at the given index with a given {@link ManagedMediaSource}. If the index is out of bound,
* then the replacement is ignored.
- * @see DynamicConcatenatingMediaSource#addMediaSource
- * @see DynamicConcatenatingMediaSource#removeMediaSource(int, Runnable)
+ * @see ConcatenatingMediaSource#addMediaSource
+ * @see ConcatenatingMediaSource#removeMediaSource(int, Runnable)
* */
public synchronized void update(final int index, @NonNull final ManagedMediaSource source,
@Nullable final Runnable finalizingAction) {
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java
index 318f9a316..bfd734393 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java
@@ -3,20 +3,19 @@ package org.schabi.newpipe.player.mediasource;
import android.support.annotation.NonNull;
import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.upstream.Allocator;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
-import java.io.IOException;
-
-public class PlaceholderMediaSource implements ManagedMediaSource {
+public class PlaceholderMediaSource extends BaseMediaSource implements ManagedMediaSource {
// Do nothing, so this will stall the playback
- @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {}
- @Override public void maybeThrowSourceInfoRefreshError() throws IOException {}
+ @Override public void maybeThrowSourceInfoRefreshError() {}
@Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { return null; }
@Override public void releasePeriod(MediaPeriod mediaPeriod) {}
- @Override public void releaseSource() {}
+ @Override protected void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {}
+ @Override protected void releaseSourceInternal() {}
@Override
public boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity,
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java
index 8ab3cba98..b27dc3dd6 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java
@@ -5,12 +5,10 @@ import android.support.annotation.Nullable;
import android.support.v4.util.ArraySet;
import android.util.Log;
-import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
-import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.mediasource.FailedMediaSource;
import org.schabi.newpipe.player.mediasource.LoadedMediaSource;
import org.schabi.newpipe.player.mediasource.ManagedMediaSource;
@@ -24,10 +22,8 @@ import org.schabi.newpipe.player.playqueue.events.RemoveEvent;
import org.schabi.newpipe.player.playqueue.events.ReorderEvent;
import org.schabi.newpipe.util.ServiceHelper;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
-import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -37,8 +33,6 @@ import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
-import io.reactivex.disposables.SerialDisposable;
-import io.reactivex.functions.Consumer;
import io.reactivex.internal.subscriptions.EmptySubscription;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject;
@@ -104,7 +98,6 @@ public class MediaSourceManager {
private final static int MAXIMUM_LOADER_SIZE = WINDOW_SIZE * 2 + 1;
@NonNull private final CompositeDisposable loaderReactor;
@NonNull private final Set loadingItems;
- @NonNull private final SerialDisposable syncReactor;
@NonNull private final AtomicBoolean isBlocked;
@@ -144,7 +137,6 @@ public class MediaSourceManager {
this.playQueueReactor = EmptySubscription.INSTANCE;
this.loaderReactor = new CompositeDisposable();
- this.syncReactor = new SerialDisposable();
this.isBlocked = new AtomicBoolean(false);
@@ -171,8 +163,6 @@ public class MediaSourceManager {
playQueueReactor.cancel();
loaderReactor.dispose();
- syncReactor.dispose();
- playlist.dispose();
}
/*//////////////////////////////////////////////////////////////////////////
@@ -311,21 +301,7 @@ public class MediaSourceManager {
final PlayQueueItem currentItem = playQueue.getItem();
if (isBlocked.get() || currentItem == null) return;
- final Consumer onSuccess = info -> syncInternal(currentItem, info);
- final Consumer onError = throwable -> syncInternal(currentItem, null);
-
- final Disposable sync = currentItem.getStream()
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(onSuccess, onError);
- syncReactor.set(sync);
- }
-
- private void syncInternal(@NonNull final PlayQueueItem item,
- @Nullable final StreamInfo info) {
- // Ensure the current item is up to date with the play queue
- if (playQueue.getItem() == item) {
- playbackListener.onPlaybackSynchronize(item, info);
- }
+ playbackListener.onPlaybackSynchronize(currentItem);
}
private synchronized void maybeSynchronizePlayer() {
@@ -424,7 +400,8 @@ public class MediaSourceManager {
}
/**
- * Checks if the corresponding MediaSource in {@link DynamicConcatenatingMediaSource}
+ * Checks if the corresponding MediaSource in
+ * {@link com.google.android.exoplayer2.source.ConcatenatingMediaSource}
* for a given {@link PlayQueueItem} needs replacement, either due to gapless playback
* readiness or playlist desynchronization.
*
@@ -481,8 +458,6 @@ public class MediaSourceManager {
private void resetSources() {
if (DEBUG) Log.d(TAG, "resetSources() called.");
-
- playlist.dispose();
playlist = new ManagedMediaSourcePlaylist();
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java
index 4dcb30aa3..238bdfcd0 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java
@@ -45,7 +45,7 @@ public interface PlaybackListener {
*
* May be called anytime at any amount once unblock is called.
* */
- void onPlaybackSynchronize(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info);
+ void onPlaybackSynchronize(@NonNull final PlayQueueItem item);
/**
* Requests the listener to resolve a stream info into a media source
diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java
index a21560abd..c9e07c96a 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java
@@ -6,6 +6,7 @@ import android.util.Log;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
+import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.player.playqueue.events.AppendEvent;
import org.schabi.newpipe.player.playqueue.events.ErrorEvent;
import org.schabi.newpipe.player.playqueue.events.InitEvent;
@@ -41,7 +42,7 @@ import io.reactivex.subjects.BehaviorSubject;
public abstract class PlayQueue implements Serializable {
private final String TAG = "PlayQueue@" + Integer.toHexString(hashCode());
- public static final boolean DEBUG = true;
+ public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
private ArrayList backup;
private ArrayList streams;
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java
new file mode 100644
index 000000000..6bb556850
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java
@@ -0,0 +1,41 @@
+package org.schabi.newpipe.player.resolver;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import com.google.android.exoplayer2.source.MediaSource;
+
+import org.schabi.newpipe.extractor.MediaFormat;
+import org.schabi.newpipe.extractor.stream.AudioStream;
+import org.schabi.newpipe.extractor.stream.StreamInfo;
+import org.schabi.newpipe.player.helper.PlayerDataSource;
+import org.schabi.newpipe.player.helper.PlayerHelper;
+import org.schabi.newpipe.util.ListHelper;
+
+public class AudioPlaybackResolver implements PlaybackResolver {
+
+ @NonNull private final Context context;
+ @NonNull private final PlayerDataSource dataSource;
+
+ public AudioPlaybackResolver(@NonNull final Context context,
+ @NonNull final PlayerDataSource dataSource) {
+ this.context = context;
+ this.dataSource = dataSource;
+ }
+
+ @Override
+ @Nullable
+ public MediaSource resolve(@NonNull StreamInfo info) {
+ final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info);
+ if (liveSource != null) return liveSource;
+
+ final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams());
+ if (index < 0 || index >= info.getAudioStreams().size()) return null;
+
+ final AudioStream audio = info.getAudioStreams().get(index);
+ final MediaSourceTag tag = new MediaSourceTag(info);
+ return buildMediaSource(dataSource, audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio),
+ MediaFormat.getSuffixById(audio.getFormatId()), tag);
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/MediaSourceTag.java b/app/src/main/java/org/schabi/newpipe/player/resolver/MediaSourceTag.java
new file mode 100644
index 000000000..bbe5d33ca
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/MediaSourceTag.java
@@ -0,0 +1,51 @@
+package org.schabi.newpipe.player.resolver;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import org.schabi.newpipe.extractor.stream.StreamInfo;
+import org.schabi.newpipe.extractor.stream.VideoStream;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.List;
+
+public class MediaSourceTag implements Serializable {
+ @NonNull private final StreamInfo metadata;
+
+ @NonNull private final List sortedAvailableVideoStreams;
+ private final int selectedVideoStreamIndex;
+
+ public MediaSourceTag(@NonNull final StreamInfo metadata,
+ @NonNull final List sortedAvailableVideoStreams,
+ final int selectedVideoStreamIndex) {
+ this.metadata = metadata;
+ this.sortedAvailableVideoStreams = sortedAvailableVideoStreams;
+ this.selectedVideoStreamIndex = selectedVideoStreamIndex;
+ }
+
+ public MediaSourceTag(@NonNull final StreamInfo metadata) {
+ this(metadata, Collections.emptyList(), /*indexNotAvailable=*/-1);
+ }
+
+ @NonNull
+ public StreamInfo getMetadata() {
+ return metadata;
+ }
+
+ @NonNull
+ public List getSortedAvailableVideoStreams() {
+ return sortedAvailableVideoStreams;
+ }
+
+ public int getSelectedVideoStreamIndex() {
+ return selectedVideoStreamIndex;
+ }
+
+ @Nullable
+ public VideoStream getSelectedVideoStream() {
+ return selectedVideoStreamIndex < 0 ||
+ selectedVideoStreamIndex >= sortedAvailableVideoStreams.size() ? null :
+ sortedAvailableVideoStreams.get(selectedVideoStreamIndex);
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
new file mode 100644
index 000000000..1da3ec211
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
@@ -0,0 +1,84 @@
+package org.schabi.newpipe.player.resolver;
+
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.util.Util;
+
+import org.schabi.newpipe.extractor.stream.StreamInfo;
+import org.schabi.newpipe.extractor.stream.StreamType;
+import org.schabi.newpipe.player.helper.PlayerDataSource;
+
+public interface PlaybackResolver extends Resolver {
+
+ @Nullable
+ default MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
+ @NonNull final StreamInfo info) {
+ final StreamType streamType = info.getStreamType();
+ if (!(streamType == StreamType.AUDIO_LIVE_STREAM || streamType == StreamType.LIVE_STREAM)) {
+ return null;
+ }
+
+ final MediaSourceTag tag = new MediaSourceTag(info);
+ if (!info.getHlsUrl().isEmpty()) {
+ return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.TYPE_HLS, tag);
+ } else if (!info.getDashMpdUrl().isEmpty()) {
+ return buildLiveMediaSource(dataSource, info.getDashMpdUrl(), C.TYPE_DASH, tag);
+ }
+
+ return null;
+ }
+
+ @NonNull
+ default MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
+ @NonNull final String sourceUrl,
+ @C.ContentType final int type,
+ @NonNull final MediaSourceTag metadata) {
+ final Uri uri = Uri.parse(sourceUrl);
+ switch (type) {
+ case C.TYPE_SS:
+ return dataSource.getLiveSsMediaSourceFactory().setTag(metadata)
+ .createMediaSource(uri);
+ case C.TYPE_DASH:
+ return dataSource.getLiveDashMediaSourceFactory().setTag(metadata)
+ .createMediaSource(uri);
+ case C.TYPE_HLS:
+ return dataSource.getLiveHlsMediaSourceFactory().setTag(metadata)
+ .createMediaSource(uri);
+ default:
+ throw new IllegalStateException("Unsupported type: " + type);
+ }
+ }
+
+ @NonNull
+ default MediaSource buildMediaSource(@NonNull final PlayerDataSource dataSource,
+ @NonNull final String sourceUrl,
+ @NonNull final String cacheKey,
+ @NonNull final String overrideExtension,
+ @NonNull final MediaSourceTag metadata) {
+ final Uri uri = Uri.parse(sourceUrl);
+ @C.ContentType final int type = TextUtils.isEmpty(overrideExtension) ?
+ Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension);
+
+ switch (type) {
+ case C.TYPE_SS:
+ return dataSource.getLiveSsMediaSourceFactory().setTag(metadata)
+ .createMediaSource(uri);
+ case C.TYPE_DASH:
+ return dataSource.getDashMediaSourceFactory().setTag(metadata)
+ .createMediaSource(uri);
+ case C.TYPE_HLS:
+ return dataSource.getHlsMediaSourceFactory().setTag(metadata)
+ .createMediaSource(uri);
+ case C.TYPE_OTHER:
+ return dataSource.getExtractorMediaSourceFactory(cacheKey).setTag(metadata)
+ .createMediaSource(uri);
+ default:
+ throw new IllegalStateException("Unsupported type: " + type);
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java
new file mode 100644
index 000000000..4bd795574
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java
@@ -0,0 +1,8 @@
+package org.schabi.newpipe.player.resolver;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+public interface Resolver {
+ @Nullable Product resolve(@NonNull Source source);
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java
new file mode 100644
index 000000000..8f91f4886
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java
@@ -0,0 +1,123 @@
+package org.schabi.newpipe.player.resolver;
+
+import android.content.Context;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.MergingMediaSource;
+
+import org.schabi.newpipe.extractor.MediaFormat;
+import org.schabi.newpipe.extractor.Subtitles;
+import org.schabi.newpipe.extractor.stream.AudioStream;
+import org.schabi.newpipe.extractor.stream.StreamInfo;
+import org.schabi.newpipe.extractor.stream.VideoStream;
+import org.schabi.newpipe.player.helper.PlayerDataSource;
+import org.schabi.newpipe.player.helper.PlayerHelper;
+import org.schabi.newpipe.util.ListHelper;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.google.android.exoplayer2.C.SELECTION_FLAG_AUTOSELECT;
+import static com.google.android.exoplayer2.C.TIME_UNSET;
+
+public class VideoPlaybackResolver implements PlaybackResolver {
+
+ public interface QualityResolver {
+ int getDefaultResolutionIndex(final List sortedVideos);
+ int getOverrideResolutionIndex(final List sortedVideos,
+ final String playbackQuality);
+ }
+
+ @NonNull private final Context context;
+ @NonNull private final PlayerDataSource dataSource;
+ @NonNull private final QualityResolver qualityResolver;
+
+ @Nullable private String playbackQuality;
+
+ public VideoPlaybackResolver(@NonNull final Context context,
+ @NonNull final PlayerDataSource dataSource,
+ @NonNull final QualityResolver qualityResolver) {
+ this.context = context;
+ this.dataSource = dataSource;
+ this.qualityResolver = qualityResolver;
+ }
+
+ @Override
+ @Nullable
+ public MediaSource resolve(@NonNull StreamInfo info) {
+ final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info);
+ if (liveSource != null) return liveSource;
+
+ List mediaSources = new ArrayList<>();
+
+ // Create video stream source
+ final List videos = ListHelper.getSortedStreamVideosList(context,
+ info.getVideoStreams(), info.getVideoOnlyStreams(), false);
+ final int index;
+ if (videos.isEmpty()) {
+ index = -1;
+ } else if (playbackQuality == null) {
+ index = qualityResolver.getDefaultResolutionIndex(videos);
+ } else {
+ index = qualityResolver.getOverrideResolutionIndex(videos, getPlaybackQuality());
+ }
+ final MediaSourceTag tag = new MediaSourceTag(info, videos, index);
+ @Nullable final VideoStream video = tag.getSelectedVideoStream();
+
+ if (video != null) {
+ final MediaSource streamSource = buildMediaSource(dataSource, video.getUrl(),
+ PlayerHelper.cacheKeyOf(info, video),
+ MediaFormat.getSuffixById(video.getFormatId()), tag);
+ mediaSources.add(streamSource);
+ }
+
+ // Create optional audio stream source
+ final List audioStreams = info.getAudioStreams();
+ final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get(
+ ListHelper.getDefaultAudioFormat(context, audioStreams));
+ // Use the audio stream if there is no video stream, or
+ // Merge with audio stream in case if video does not contain audio
+ if (audio != null && ((video != null && video.isVideoOnly) || video == null)) {
+ final MediaSource audioSource = buildMediaSource(dataSource, audio.getUrl(),
+ PlayerHelper.cacheKeyOf(info, audio),
+ MediaFormat.getSuffixById(audio.getFormatId()), tag);
+ mediaSources.add(audioSource);
+ }
+
+ // If there is no audio or video sources, then this media source cannot be played back
+ if (mediaSources.isEmpty()) return null;
+ // Below are auxiliary media sources
+
+ // Create subtitle sources
+ for (final Subtitles subtitle : info.getSubtitles()) {
+ final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType());
+ if (mimeType == null) continue;
+
+ final Format textFormat = Format.createTextSampleFormat(null, mimeType,
+ SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle));
+ final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
+ .createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET);
+ mediaSources.add(textSource);
+ }
+
+ if (mediaSources.size() == 1) {
+ return mediaSources.get(0);
+ } else {
+ return new MergingMediaSource(mediaSources.toArray(
+ new MediaSource[mediaSources.size()]));
+ }
+ }
+
+ @Nullable
+ public String getPlaybackQuality() {
+ return playbackQuality;
+ }
+
+ public void setPlaybackQuality(@Nullable String playbackQuality) {
+ this.playbackQuality = playbackQuality;
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/report/UserAction.java b/app/src/main/java/org/schabi/newpipe/report/UserAction.java
index 93a3ce16c..00a25ed8d 100644
--- a/app/src/main/java/org/schabi/newpipe/report/UserAction.java
+++ b/app/src/main/java/org/schabi/newpipe/report/UserAction.java
@@ -15,7 +15,8 @@ public enum UserAction {
REQUESTED_CHANNEL("requested channel"),
REQUESTED_PLAYLIST("requested playlist"),
REQUESTED_KIOSK("requested kiosk"),
- DELETE_FROM_HISTORY("delete from history");
+ DELETE_FROM_HISTORY("delete from history"),
+ PLAY_STREAM("Play stream");
private final String message;
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 a02a9df34..0ca78b34a 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java
@@ -9,6 +9,7 @@ import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
import android.support.v7.preference.ListPreference;
import android.support.v7.preference.Preference;
import android.util.Log;
@@ -20,15 +21,12 @@ import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
-import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
-import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.ZipHelper;
-import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
@@ -42,11 +40,8 @@ import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.zip.ZipFile;
-import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
-import static android.content.Context.MODE_PRIVATE;
-
public class ContentSettingsFragment extends BasePreferenceFragment {
private static final int REQUEST_IMPORT_PATH = 8945;
@@ -56,6 +51,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
private File databasesDir;
private File newpipe_db;
private File newpipe_db_journal;
+ private File newpipe_db_shm;
+ private File newpipe_db_wal;
private File newpipe_settings;
private String thumbnailLoadToggleKey;
@@ -88,73 +85,14 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
databasesDir = new File(homeDir + "/databases");
newpipe_db = new File(homeDir + "/databases/newpipe.db");
newpipe_db_journal = new File(homeDir + "/databases/newpipe.db-journal");
+ newpipe_db_shm = new File(homeDir + "/databases/newpipe.db-shm");
+ newpipe_db_wal = new File(homeDir + "/databases/newpipe.db-wal");
+
newpipe_settings = new File(homeDir + "/databases/newpipe.settings");
newpipe_settings.delete();
addPreferencesFromResource(R.xml.content_settings);
- final ListPreference mainPageContentPref = (ListPreference) findPreference(getString(R.string.main_page_content_key));
- mainPageContentPref.setOnPreferenceChangeListener((Preference preference, Object newValueO) -> {
- final String newValue = newValueO.toString();
-
- final String mainPrefOldValue =
- defaultPreferences.getString(getString(R.string.main_page_content_key), "blank_page");
- final String mainPrefOldSummary = getMainPagePrefSummery(mainPrefOldValue, mainPageContentPref);
-
- if(newValue.equals(getString(R.string.kiosk_page_key))) {
- SelectKioskFragment selectKioskFragment = new SelectKioskFragment();
- selectKioskFragment.setOnSelectedLisener((String kioskId, int service_id) -> {
- defaultPreferences.edit()
- .putInt(getString(R.string.main_page_selected_service), service_id).apply();
- defaultPreferences.edit()
- .putString(getString(R.string.main_page_selectd_kiosk_id), kioskId).apply();
- String serviceName = "";
- try {
- serviceName = NewPipe.getService(service_id).getServiceInfo().getName();
- } catch (ExtractionException e) {
- onError(e);
- }
- String kioskName = KioskTranslator.getTranslatedKioskName(kioskId,
- getContext());
-
- String summary =
- String.format(getString(R.string.service_kiosk_string),
- serviceName,
- kioskName);
-
- mainPageContentPref.setSummary(summary);
- });
- selectKioskFragment.setOnCancelListener(() -> {
- mainPageContentPref.setSummary(mainPrefOldSummary);
- mainPageContentPref.setValue(mainPrefOldValue);
- });
- selectKioskFragment.show(getFragmentManager(), "select_kiosk");
- } else if(newValue.equals(getString(R.string.channel_page_key))) {
- SelectChannelFragment selectChannelFragment = new SelectChannelFragment();
- selectChannelFragment.setOnSelectedLisener((String url, String name, int service) -> {
- defaultPreferences.edit()
- .putInt(getString(R.string.main_page_selected_service), service).apply();
- defaultPreferences.edit()
- .putString(getString(R.string.main_page_selected_channel_url), url).apply();
- defaultPreferences.edit()
- .putString(getString(R.string.main_page_selected_channel_name), name).apply();
-
- mainPageContentPref.setSummary(name);
- });
- selectChannelFragment.setOnCancelListener(() -> {
- mainPageContentPref.setSummary(mainPrefOldSummary);
- mainPageContentPref.setValue(mainPrefOldValue);
- });
- selectChannelFragment.show(getFragmentManager(), "select_channel");
- } else {
- mainPageContentPref.setSummary(getMainPageSummeryByKey(newValue));
- }
-
- defaultPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, true).apply();
-
- return true;
- });
-
Preference importDataPreference = findPreference(getString(R.string.import_data));
importDataPreference.setOnPreferenceClickListener((Preference p) -> {
Intent i = new Intent(getActivity(), FilePickerActivityHelper.class)
@@ -207,7 +145,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
new BufferedOutputStream(
new FileOutputStream(path)));
ZipHelper.addFileToZip(outZip, newpipe_db.getPath(), "newpipe.db");
- ZipHelper.addFileToZip(outZip, newpipe_db_journal.getPath(), "newpipe.db-journal");
+
saveSharedPreferencesToFile(newpipe_settings);
ZipHelper.addFileToZip(outZip, newpipe_settings.getPath(), "newpipe.settings");
@@ -263,8 +201,16 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
throw new Exception("Could not create databases dir");
}
- if(!(ZipHelper.extractFileFromZip(filePath, newpipe_db.getPath(), "newpipe.db")
- && ZipHelper.extractFileFromZip(filePath, newpipe_db_journal.getPath(), "newpipe.db-journal"))) {
+ final boolean isDbFileExtracted = ZipHelper.extractFileFromZip(filePath,
+ newpipe_db.getPath(), "newpipe.db");
+
+ if (isDbFileExtracted) {
+ newpipe_db_journal.delete();
+ newpipe_db_wal.delete();
+ newpipe_db_shm.delete();
+
+ } else {
+
Toast.makeText(getContext(), R.string.could_not_import_all_files, Toast.LENGTH_LONG)
.show();
}
@@ -336,66 +282,6 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
}
}
- @Override
- public void onResume() {
- super.onResume();
-
- final String mainPageContentKey = getString(R.string.main_page_content_key);
- final Preference mainPagePref = findPreference(getString(R.string.main_page_content_key));
- final String bpk = getString(R.string.blank_page_key);
- if(defaultPreferences.getString(mainPageContentKey, bpk)
- .equals(getString(R.string.channel_page_key))) {
- mainPagePref.setSummary(defaultPreferences.getString(getString(R.string.main_page_selected_channel_name), "error"));
- } else if(defaultPreferences.getString(mainPageContentKey, bpk)
- .equals(getString(R.string.kiosk_page_key))) {
- try {
- StreamingService service = NewPipe.getService(
- defaultPreferences.getInt(
- getString(R.string.main_page_selected_service), 0));
-
- String kioskName = KioskTranslator.getTranslatedKioskName(
- defaultPreferences.getString(
- getString(R.string.main_page_selectd_kiosk_id), "Trending"),
- getContext());
-
- String summary =
- String.format(getString(R.string.service_kiosk_string),
- service.getServiceInfo().getName(),
- kioskName);
-
- mainPagePref.setSummary(summary);
- } catch (Exception e) {
- onError(e);
- }
- }
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Utils
- //////////////////////////////////////////////////////////////////////////*/
- private String getMainPagePrefSummery(final String mainPrefOldValue, final ListPreference mainPageContentPref) {
- if(mainPrefOldValue.equals(getString(R.string.channel_page_key))) {
- return defaultPreferences.getString(getString(R.string.main_page_selected_channel_name), "error");
- } else {
- return mainPageContentPref.getSummary().toString();
- }
- }
-
- private int getMainPageSummeryByKey(final String key) {
- if(key.equals(getString(R.string.blank_page_key))) {
- return R.string.blank_page_summary;
- } else if(key.equals(getString(R.string.kiosk_page_key))) {
- return R.string.kiosk_page_summary;
- } else if(key.equals(getString(R.string.feed_page_key))) {
- return R.string.feed_page_summary;
- } else if(key.equals(getString(R.string.subscription_page_key))) {
- return R.string.subscription_page_summary;
- } else if(key.equals(getString(R.string.channel_page_key))) {
- return R.string.channel_page_summary;
- }
- return R.string.blank_page_summary;
- }
-
/*//////////////////////////////////////////////////////////////////////////
// Error
//////////////////////////////////////////////////////////////////////////*/
diff --git a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java
index 92f98a9a2..2a0e2645b 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java
@@ -71,7 +71,7 @@ public class NewPipeSettings {
}
public static File getVideoDownloadFolder(Context context) {
- return getFolder(context, R.string.download_path_key, Environment.DIRECTORY_MOVIES);
+ return getDir(context, R.string.download_path_key, Environment.DIRECTORY_MOVIES);
}
public static String getVideoDownloadPath(Context context) {
@@ -81,7 +81,7 @@ public class NewPipeSettings {
}
public static File getAudioDownloadFolder(Context context) {
- return getFolder(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC);
+ return getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC);
}
public static String getAudioDownloadPath(Context context) {
@@ -90,21 +90,37 @@ public class NewPipeSettings {
return prefs.getString(key, Environment.DIRECTORY_MUSIC);
}
- private static File getFolder(Context context, int keyID, String defaultDirectoryName) {
+ private static File getDir(Context context, int keyID, String defaultDirectoryName) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String key = context.getString(keyID);
String downloadPath = prefs.getString(key, null);
if ((downloadPath != null) && (!downloadPath.isEmpty())) return new File(downloadPath.trim());
- final File folder = getFolder(defaultDirectoryName);
+ final File dir = getDir(defaultDirectoryName);
SharedPreferences.Editor spEditor = prefs.edit();
- spEditor.putString(key, new File(folder, "NewPipe").getAbsolutePath());
+ spEditor.putString(key, getNewPipeChildFolderPathForDir(dir));
spEditor.apply();
- return folder;
+ return dir;
}
@NonNull
- private static File getFolder(String defaultDirectoryName) {
+ private static File getDir(String defaultDirectoryName) {
return new File(Environment.getExternalStorageDirectory(), defaultDirectoryName);
}
+
+ public static void resetDownloadFolders(Context context) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ resetDownloadFolder(prefs, context.getString(R.string.download_path_audio_key), Environment.DIRECTORY_MUSIC);
+ resetDownloadFolder(prefs, context.getString(R.string.download_path_key), Environment.DIRECTORY_MOVIES);
+ }
+
+ private static void resetDownloadFolder(SharedPreferences prefs, String key, String defaultDirectoryName) {
+ SharedPreferences.Editor spEditor = prefs.edit();
+ spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName)));
+ spEditor.apply();
+ }
+
+ private static String getNewPipeChildFolderPathForDir(File dir) {
+ return new File(dir, "NewPipe").getAbsolutePath();
+ }
}
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 e961de969..0ebdbefe0 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java
@@ -66,7 +66,7 @@ public class SelectChannelFragment extends DialogFragment {
//////////////////////////////////////////////////////////////////////////*/
public interface OnSelectedLisener {
- void onChannelSelected(String url, String name, int service);
+ void onChannelSelected(int serviceId, String url, String name);
}
OnSelectedLisener onSelectedLisener = null;
public void setOnSelectedLisener(OnSelectedLisener listener) {
@@ -126,7 +126,7 @@ public class SelectChannelFragment extends DialogFragment {
private void clickedItem(int position) {
if(onSelectedLisener != null) {
SubscriptionEntity entry = subscriptions.get(position);
- onSelectedLisener.onChannelSelected(entry.getUrl(), entry.getName(), entry.getServiceId());
+ onSelectedLisener.onChannelSelected(entry.getServiceId(), entry.getUrl(), entry.getName());
}
dismiss();
}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java
index 00b618889..44cb16682 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java
@@ -56,7 +56,7 @@ public class SelectKioskFragment extends DialogFragment {
//////////////////////////////////////////////////////////////////////////*/
public interface OnSelectedLisener {
- void onKioskSelected(String kioskId, int service_id);
+ void onKioskSelected(int serviceId, String kioskId, String kioskName);
}
OnSelectedLisener onSelectedLisener = null;
@@ -101,7 +101,7 @@ public class SelectKioskFragment extends DialogFragment {
private void clickedItem(SelectKioskAdapter.Entry entry) {
if(onSelectedLisener != null) {
- onSelectedLisener.onKioskSelected(entry.kioskId, entry.serviceId);
+ onSelectedLisener.onKioskSelected(entry.serviceId, entry.kioskId, entry.kioskName);
}
dismiss();
}
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 7d6f8d633..a8482e0eb 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java
@@ -77,7 +77,8 @@ public class SettingsActivity extends AppCompatActivity implements BasePreferenc
finish();
} else getSupportFragmentManager().popBackStack();
}
- return true;
+
+ return super.onOptionsItemSelected(item);
}
@Override
diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/AddTabDialog.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/AddTabDialog.java
new file mode 100644
index 000000000..695f81ff5
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/AddTabDialog.java
@@ -0,0 +1,94 @@
+package org.schabi.newpipe.settings.tabs;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.v7.widget.AppCompatImageView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.util.ThemeHelper;
+
+public class AddTabDialog {
+ private final AlertDialog dialog;
+
+ AddTabDialog(@NonNull final Context context,
+ @NonNull final ChooseTabListItem[] items,
+ @NonNull final DialogInterface.OnClickListener actions) {
+
+ dialog = new AlertDialog.Builder(context)
+ .setTitle(context.getString(R.string.tab_choose))
+ .setAdapter(new DialogListAdapter(context, items), actions)
+ .create();
+ }
+
+ public void show() {
+ dialog.show();
+ }
+
+ public static final class ChooseTabListItem {
+ final int tabId;
+ final String itemName;
+ @DrawableRes final int itemIcon;
+
+ ChooseTabListItem(Context context, Tab tab) {
+ this(tab.getTabId(), tab.getTabName(context), tab.getTabIconRes(context));
+ }
+
+ ChooseTabListItem(int tabId, String itemName, @DrawableRes int itemIcon) {
+ this.tabId = tabId;
+ this.itemName = itemName;
+ this.itemIcon = itemIcon;
+ }
+ }
+
+ private static class DialogListAdapter extends BaseAdapter {
+ private final LayoutInflater inflater;
+ private final ChooseTabListItem[] items;
+
+ @DrawableRes private final int fallbackIcon;
+
+ private DialogListAdapter(Context context, ChooseTabListItem[] items) {
+ this.inflater = LayoutInflater.from(context);
+ this.items = items;
+ this.fallbackIcon = ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_hot);
+ }
+
+ @Override
+ public int getCount() {
+ return items.length;
+ }
+
+ @Override
+ public ChooseTabListItem getItem(int position) {
+ return items[position];
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return getItem(position).tabId;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.list_choose_tabs_dialog, parent, false);
+ }
+
+ final ChooseTabListItem item = getItem(position);
+ final AppCompatImageView tabIconView = convertView.findViewById(R.id.tabIcon);
+ final TextView tabNameView = convertView.findViewById(R.id.tabName);
+
+ tabIconView.setImageResource(item.itemIcon > 0 ? item.itemIcon : fallbackIcon);
+ tabNameView.setText(item.itemName);
+
+ return convertView;
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java
new file mode 100644
index 000000000..b86f13d14
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java
@@ -0,0 +1,386 @@
+package org.schabi.newpipe.settings.tabs;
+
+import android.annotation.SuppressLint;
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v4.app.Fragment;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.content.res.AppCompatResources;
+import android.support.v7.widget.AppCompatImageView;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.helper.ItemTouchHelper;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.extractor.NewPipe;
+import org.schabi.newpipe.report.ErrorActivity;
+import org.schabi.newpipe.report.UserAction;
+import org.schabi.newpipe.settings.SelectChannelFragment;
+import org.schabi.newpipe.settings.SelectKioskFragment;
+import org.schabi.newpipe.settings.tabs.AddTabDialog.ChooseTabListItem;
+import org.schabi.newpipe.util.ThemeHelper;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.schabi.newpipe.settings.tabs.Tab.typeFrom;
+
+public class ChooseTabsFragment extends Fragment {
+
+ private TabsManager tabsManager;
+ private List tabList = new ArrayList<>();
+ public ChooseTabsFragment.SelectedTabsAdapter selectedTabsAdapter;
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Lifecycle
+ //////////////////////////////////////////////////////////////////////////*/
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ tabsManager = TabsManager.getManager(requireContext());
+ updateTabList();
+
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_choose_tabs, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View rootView, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(rootView, savedInstanceState);
+
+ initButton(rootView);
+
+ RecyclerView listSelectedTabs = rootView.findViewById(R.id.selectedTabs);
+ listSelectedTabs.setLayoutManager(new LinearLayoutManager(requireContext()));
+
+ ItemTouchHelper itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
+ itemTouchHelper.attachToRecyclerView(listSelectedTabs);
+
+ selectedTabsAdapter = new SelectedTabsAdapter(requireContext(), itemTouchHelper);
+ listSelectedTabs.setAdapter(selectedTabsAdapter);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ updateTitle();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ saveChanges();
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Menu
+ //////////////////////////////////////////////////////////////////////////*/
+
+ private final int MENU_ITEM_RESTORE_ID = 123456;
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+
+ final MenuItem restoreItem = menu.add(Menu.NONE, MENU_ITEM_RESTORE_ID, Menu.NONE, R.string.restore_defaults);
+ restoreItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+
+ final int restoreIcon = ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_restore_defaults);
+ restoreItem.setIcon(AppCompatResources.getDrawable(requireContext(), restoreIcon));
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == MENU_ITEM_RESTORE_ID) {
+ restoreDefaults();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Utils
+ //////////////////////////////////////////////////////////////////////////*/
+
+ private void updateTabList() {
+ tabList.clear();
+ tabList.addAll(tabsManager.getTabs());
+ }
+
+ private void updateTitle() {
+ if (getActivity() instanceof AppCompatActivity) {
+ ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
+ if (actionBar != null) actionBar.setTitle(R.string.main_page_content);
+ }
+ }
+
+ private void saveChanges() {
+ tabsManager.saveTabs(tabList);
+ }
+
+ private void restoreDefaults() {
+ new AlertDialog.Builder(requireContext(), ThemeHelper.getDialogTheme(requireContext()))
+ .setTitle(R.string.restore_defaults)
+ .setMessage(R.string.restore_defaults_confirmation)
+ .setNegativeButton(R.string.cancel, null)
+ .setPositiveButton(R.string.yes, (dialog, which) -> {
+ tabsManager.resetTabs();
+ updateTabList();
+ selectedTabsAdapter.notifyDataSetChanged();
+ })
+ .show();
+ }
+
+ private void initButton(View rootView) {
+ final FloatingActionButton fab = rootView.findViewById(R.id.addTabsButton);
+ fab.setOnClickListener(v -> {
+ final ChooseTabListItem[] availableTabs = getAvailableTabs(requireContext());
+
+ if (availableTabs.length == 0) {
+ //Toast.makeText(requireContext(), "No available tabs", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Dialog.OnClickListener actionListener = (dialog, which) -> {
+ final ChooseTabListItem selected = availableTabs[which];
+ addTab(selected.tabId);
+ };
+
+ new AddTabDialog(requireContext(), availableTabs, actionListener)
+ .show();
+ });
+ }
+
+ private void addTab(final Tab tab) {
+ tabList.add(tab);
+ selectedTabsAdapter.notifyDataSetChanged();
+ }
+
+ private void addTab(int tabId) {
+ final Tab.Type type = typeFrom(tabId);
+
+ if (type == null) {
+ ErrorActivity.reportError(requireContext(), new IllegalStateException("Tab id not found: " + tabId), null, null,
+ ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", "Choosing tabs on settings", 0));
+ return;
+ }
+
+ switch (type) {
+ case KIOSK: {
+ SelectKioskFragment selectFragment = new SelectKioskFragment();
+ selectFragment.setOnSelectedLisener((serviceId, kioskId, kioskName) ->
+ addTab(new Tab.KioskTab(serviceId, kioskId)));
+ selectFragment.show(requireFragmentManager(), "select_kiosk");
+ return;
+ }
+ case CHANNEL: {
+ SelectChannelFragment selectFragment = new SelectChannelFragment();
+ selectFragment.setOnSelectedLisener((serviceId, url, name) ->
+ addTab(new Tab.ChannelTab(serviceId, url, name)));
+ selectFragment.show(requireFragmentManager(), "select_channel");
+ return;
+ }
+ default:
+ addTab(type.getTab());
+ break;
+ }
+ }
+
+ public ChooseTabListItem[] getAvailableTabs(Context context) {
+ final ArrayList returnList = new ArrayList<>();
+
+ for (Tab.Type type : Tab.Type.values()) {
+ final Tab tab = type.getTab();
+ switch (type) {
+ case BLANK:
+ if (!tabList.contains(tab)) {
+ returnList.add(new ChooseTabListItem(tab.getTabId(), getString(R.string.blank_page_summary),
+ tab.getTabIconRes(context)));
+ }
+ break;
+ case KIOSK:
+ returnList.add(new ChooseTabListItem(tab.getTabId(), getString(R.string.kiosk_page_summary),
+ ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_hot)));
+ break;
+ case CHANNEL:
+ returnList.add(new ChooseTabListItem(tab.getTabId(), getString(R.string.channel_page_summary),
+ tab.getTabIconRes(context)));
+ break;
+ default:
+ if (!tabList.contains(tab)) {
+ returnList.add(new ChooseTabListItem(context, tab));
+ }
+ break;
+ }
+ }
+
+ return returnList.toArray(new ChooseTabListItem[0]);
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // List Handling
+ //////////////////////////////////////////////////////////////////////////*/
+
+ private class SelectedTabsAdapter extends RecyclerView.Adapter {
+ private ItemTouchHelper itemTouchHelper;
+ private final LayoutInflater inflater;
+
+ SelectedTabsAdapter(Context context, ItemTouchHelper itemTouchHelper) {
+ this.itemTouchHelper = itemTouchHelper;
+ this.inflater = LayoutInflater.from(context);
+ }
+
+ public void swapItems(int fromPosition, int toPosition) {
+ Collections.swap(tabList, fromPosition, toPosition);
+ notifyItemMoved(fromPosition, toPosition);
+ }
+
+ @NonNull
+ @Override
+ public ChooseTabsFragment.SelectedTabsAdapter.TabViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View view = inflater.inflate(R.layout.list_choose_tabs, parent, false);
+ return new ChooseTabsFragment.SelectedTabsAdapter.TabViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ChooseTabsFragment.SelectedTabsAdapter.TabViewHolder holder, int position) {
+ holder.bind(position, holder);
+ }
+
+ @Override
+ public int getItemCount() {
+ return tabList.size();
+ }
+
+ class TabViewHolder extends RecyclerView.ViewHolder {
+ private AppCompatImageView tabIconView;
+ private TextView tabNameView;
+ private ImageView handle;
+
+ TabViewHolder(View itemView) {
+ super(itemView);
+
+ tabNameView = itemView.findViewById(R.id.tabName);
+ tabIconView = itemView.findViewById(R.id.tabIcon);
+ handle = itemView.findViewById(R.id.handle);
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ void bind(int position, TabViewHolder holder) {
+ handle.setOnTouchListener(getOnTouchListener(holder));
+
+ final Tab tab = tabList.get(position);
+ final Tab.Type type = Tab.typeFrom(tab.getTabId());
+
+ if (type == null) {
+ return;
+ }
+
+ String tabName = tab.getTabName(requireContext());
+ switch (type) {
+ case BLANK:
+ tabName = requireContext().getString(R.string.blank_page_summary);
+ break;
+ case KIOSK:
+ tabName = NewPipe.getNameOfService(((Tab.KioskTab) tab).getKioskServiceId()) + "/" + tabName;
+ break;
+ case CHANNEL:
+ tabName = NewPipe.getNameOfService(((Tab.ChannelTab) tab).getChannelServiceId()) + "/" + tabName;
+ break;
+ }
+
+
+ tabNameView.setText(tabName);
+ tabIconView.setImageResource(tab.getTabIconRes(requireContext()));
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ private View.OnTouchListener getOnTouchListener(final RecyclerView.ViewHolder item) {
+ return (view, motionEvent) -> {
+ if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ if (itemTouchHelper != null && getItemCount() > 1) {
+ itemTouchHelper.startDrag(item);
+ return true;
+ }
+ }
+ return false;
+ };
+ }
+ }
+ }
+
+ private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
+ return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
+ ItemTouchHelper.START | ItemTouchHelper.END) {
+ @Override
+ public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize,
+ int viewSizeOutOfBounds, int totalSize,
+ long msSinceStartScroll) {
+ final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize,
+ viewSizeOutOfBounds, totalSize, msSinceStartScroll);
+ final int minimumAbsVelocity = Math.max(12,
+ Math.abs(standardSpeed));
+ return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
+ }
+
+ @Override
+ public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source,
+ RecyclerView.ViewHolder target) {
+ if (source.getItemViewType() != target.getItemViewType() ||
+ selectedTabsAdapter == null) {
+ return false;
+ }
+
+ final int sourceIndex = source.getAdapterPosition();
+ final int targetIndex = target.getAdapterPosition();
+ selectedTabsAdapter.swapItems(sourceIndex, targetIndex);
+ return true;
+ }
+
+ @Override
+ public boolean isLongPressDragEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isItemViewSwipeEnabled() {
+ return true;
+ }
+
+ @Override
+ public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
+ int position = viewHolder.getAdapterPosition();
+ tabList.remove(position);
+ selectedTabsAdapter.notifyItemRemoved(position);
+
+ if (tabList.isEmpty()) {
+ tabList.add(Tab.Type.BLANK.getTab());
+ selectedTabsAdapter.notifyItemInserted(0);
+ }
+ }
+ };
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java
new file mode 100644
index 000000000..d7c249a3e
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java
@@ -0,0 +1,416 @@
+package org.schabi.newpipe.settings.tabs;
+
+import android.content.Context;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+
+import com.grack.nanojson.JsonObject;
+import com.grack.nanojson.JsonSink;
+
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.extractor.exceptions.ExtractionException;
+import org.schabi.newpipe.fragments.BlankFragment;
+import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
+import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
+import org.schabi.newpipe.local.bookmark.BookmarkFragment;
+import org.schabi.newpipe.local.feed.FeedFragment;
+import org.schabi.newpipe.local.history.StatisticsPlaylistFragment;
+import org.schabi.newpipe.local.subscription.SubscriptionFragment;
+import org.schabi.newpipe.util.KioskTranslator;
+import org.schabi.newpipe.util.ThemeHelper;
+
+public abstract class Tab {
+ Tab() {
+ }
+
+ Tab(@NonNull JsonObject jsonObject) {
+ readDataFromJson(jsonObject);
+ }
+
+ public abstract int getTabId();
+ public abstract String getTabName(Context context);
+ @DrawableRes public abstract int getTabIconRes(Context context);
+
+ /**
+ * Return a instance of the fragment that this tab represent.
+ */
+ public abstract Fragment getFragment() throws ExtractionException;
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof Tab && obj.getClass().equals(this.getClass())
+ && ((Tab) obj).getTabId() == this.getTabId();
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // JSON Handling
+ //////////////////////////////////////////////////////////////////////////*/
+
+ private static final String JSON_TAB_ID_KEY = "tab_id";
+
+ public void writeJsonOn(JsonSink jsonSink) {
+ jsonSink.object();
+
+ jsonSink.value(JSON_TAB_ID_KEY, getTabId());
+ writeDataToJson(jsonSink);
+
+ jsonSink.end();
+ }
+
+ protected void writeDataToJson(JsonSink writerSink) {
+ // No-op
+ }
+
+ protected void readDataFromJson(JsonObject jsonObject) {
+ // No-op
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Tab Handling
+ //////////////////////////////////////////////////////////////////////////*/
+
+ @Nullable
+ public static Tab from(@NonNull JsonObject jsonObject) {
+ final int tabId = jsonObject.getInt(Tab.JSON_TAB_ID_KEY, -1);
+
+ if (tabId == -1) {
+ return null;
+ }
+
+ return from(tabId, jsonObject);
+ }
+
+ @Nullable
+ public static Tab from(final int tabId) {
+ return from(tabId, null);
+ }
+
+ @Nullable
+ public static Type typeFrom(int tabId) {
+ for (Type available : Type.values()) {
+ if (available.getTabId() == tabId) {
+ return available;
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ private static Tab from(final int tabId, @Nullable JsonObject jsonObject) {
+ final Type type = typeFrom(tabId);
+
+ if (type == null) {
+ return null;
+ }
+
+ if (jsonObject != null) {
+ switch (type) {
+ case KIOSK:
+ return new KioskTab(jsonObject);
+ case CHANNEL:
+ return new ChannelTab(jsonObject);
+ }
+ }
+
+ return type.getTab();
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Implementations
+ //////////////////////////////////////////////////////////////////////////*/
+
+ public enum Type {
+ BLANK(new BlankTab()),
+ SUBSCRIPTIONS(new SubscriptionsTab()),
+ FEED(new FeedTab()),
+ BOOKMARKS(new BookmarksTab()),
+ HISTORY(new HistoryTab()),
+ KIOSK(new KioskTab()),
+ CHANNEL(new ChannelTab());
+
+ private Tab tab;
+
+ Type(Tab tab) {
+ this.tab = tab;
+ }
+
+ public int getTabId() {
+ return tab.getTabId();
+ }
+
+ public Tab getTab() {
+ return tab;
+ }
+ }
+
+ public static class BlankTab extends Tab {
+ public static final int ID = 0;
+
+ @Override
+ public int getTabId() {
+ return ID;
+ }
+
+ @Override
+ public String getTabName(Context context) {
+ return "NewPipe"; //context.getString(R.string.blank_page_summary);
+ }
+
+ @DrawableRes
+ @Override
+ public int getTabIconRes(Context context) {
+ return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_blank_page);
+ }
+
+ @Override
+ public BlankFragment getFragment() {
+ return new BlankFragment();
+ }
+ }
+
+ public static class SubscriptionsTab extends Tab {
+ public static final int ID = 1;
+
+ @Override
+ public int getTabId() {
+ return ID;
+ }
+
+ @Override
+ public String getTabName(Context context) {
+ return context.getString(R.string.tab_subscriptions);
+ }
+
+ @DrawableRes
+ @Override
+ public int getTabIconRes(Context context) {
+ return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_channel);
+ }
+
+ @Override
+ public SubscriptionFragment getFragment() {
+ return new SubscriptionFragment();
+ }
+
+ }
+
+ public static class FeedTab extends Tab {
+ public static final int ID = 2;
+
+ @Override
+ public int getTabId() {
+ return ID;
+ }
+
+ @Override
+ public String getTabName(Context context) {
+ return context.getString(R.string.fragment_whats_new);
+ }
+
+ @DrawableRes
+ @Override
+ public int getTabIconRes(Context context) {
+ return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.rss);
+ }
+
+ @Override
+ public FeedFragment getFragment() {
+ return new FeedFragment();
+ }
+ }
+
+ public static class BookmarksTab extends Tab {
+ public static final int ID = 3;
+
+ @Override
+ public int getTabId() {
+ return ID;
+ }
+
+ @Override
+ public String getTabName(Context context) {
+ return context.getString(R.string.tab_bookmarks);
+ }
+
+ @DrawableRes
+ @Override
+ public int getTabIconRes(Context context) {
+ return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_bookmark);
+ }
+
+ @Override
+ public BookmarkFragment getFragment() {
+ return new BookmarkFragment();
+ }
+ }
+
+ public static class HistoryTab extends Tab {
+ public static final int ID = 4;
+
+ @Override
+ public int getTabId() {
+ return ID;
+ }
+
+ @Override
+ public String getTabName(Context context) {
+ return context.getString(R.string.title_activity_history);
+ }
+
+ @DrawableRes
+ @Override
+ public int getTabIconRes(Context context) {
+ return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.history);
+ }
+
+ @Override
+ public StatisticsPlaylistFragment getFragment() {
+ return new StatisticsPlaylistFragment();
+ }
+ }
+
+ public static class KioskTab extends Tab {
+ public static final int ID = 5;
+
+ private int kioskServiceId;
+ private String kioskId;
+
+ private static final String JSON_KIOSK_SERVICE_ID_KEY = "service_id";
+ private static final String JSON_KIOSK_ID_KEY = "kiosk_id";
+
+ private KioskTab() {
+ this(-1, "");
+ }
+
+ public KioskTab(int kioskServiceId, String kioskId) {
+ this.kioskServiceId = kioskServiceId;
+ this.kioskId = kioskId;
+ }
+
+ public KioskTab(JsonObject jsonObject) {
+ super(jsonObject);
+ }
+
+ @Override
+ public int getTabId() {
+ return ID;
+ }
+
+ @Override
+ public String getTabName(Context context) {
+ return KioskTranslator.getTranslatedKioskName(kioskId, context);
+ }
+
+ @DrawableRes
+ @Override
+ public int getTabIconRes(Context context) {
+ final int kioskIcon = KioskTranslator.getKioskIcons(kioskId, context);
+
+ if (kioskIcon <= 0) {
+ throw new IllegalStateException("Kiosk ID is not valid: \"" + kioskId + "\"");
+ }
+
+ return kioskIcon;
+ }
+
+ @Override
+ public KioskFragment getFragment() throws ExtractionException {
+ return KioskFragment.getInstance(kioskServiceId, kioskId);
+ }
+
+ @Override
+ protected void writeDataToJson(JsonSink writerSink) {
+ writerSink.value(JSON_KIOSK_SERVICE_ID_KEY, kioskServiceId)
+ .value(JSON_KIOSK_ID_KEY, kioskId);
+ }
+
+ @Override
+ protected void readDataFromJson(JsonObject jsonObject) {
+ kioskServiceId = jsonObject.getInt(JSON_KIOSK_SERVICE_ID_KEY, -1);
+ kioskId = jsonObject.getString(JSON_KIOSK_ID_KEY, "");
+ }
+
+ public int getKioskServiceId() {
+ return kioskServiceId;
+ }
+
+ public String getKioskId() {
+ return kioskId;
+ }
+ }
+
+ public static class ChannelTab extends Tab {
+ public static final int ID = 6;
+
+ private int channelServiceId;
+ private String channelUrl;
+ private String channelName;
+
+ private static final String JSON_CHANNEL_SERVICE_ID_KEY = "channel_service_id";
+ private static final String JSON_CHANNEL_URL_KEY = "channel_url";
+ private static final String JSON_CHANNEL_NAME_KEY = "channel_name";
+
+ private ChannelTab() {
+ this(-1, "", "");
+ }
+
+ public ChannelTab(int channelServiceId, String channelUrl, String channelName) {
+ this.channelServiceId = channelServiceId;
+ this.channelUrl = channelUrl;
+ this.channelName = channelName;
+ }
+
+ public ChannelTab(JsonObject jsonObject) {
+ super(jsonObject);
+ }
+
+ @Override
+ public int getTabId() {
+ return ID;
+ }
+
+ @Override
+ public String getTabName(Context context) {
+ return channelName;
+ }
+
+ @DrawableRes
+ @Override
+ public int getTabIconRes(Context context) {
+ return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_channel);
+ }
+
+ @Override
+ public ChannelFragment getFragment() {
+ return ChannelFragment.getInstance(channelServiceId, channelUrl, channelName);
+ }
+
+ @Override
+ protected void writeDataToJson(JsonSink writerSink) {
+ writerSink.value(JSON_CHANNEL_SERVICE_ID_KEY, channelServiceId)
+ .value(JSON_CHANNEL_URL_KEY, channelUrl)
+ .value(JSON_CHANNEL_NAME_KEY, channelName);
+ }
+
+ @Override
+ protected void readDataFromJson(JsonObject jsonObject) {
+ channelServiceId = jsonObject.getInt(JSON_CHANNEL_SERVICE_ID_KEY, -1);
+ channelUrl = jsonObject.getString(JSON_CHANNEL_URL_KEY, "");
+ channelName = jsonObject.getString(JSON_CHANNEL_NAME_KEY, "");
+ }
+
+ public int getChannelServiceId() {
+ return channelServiceId;
+ }
+
+ public String getChannelUrl() {
+ return channelUrl;
+ }
+
+ public String getChannelName() {
+ return channelName;
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsJsonHelper.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsJsonHelper.java
new file mode 100644
index 000000000..332e244c8
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsJsonHelper.java
@@ -0,0 +1,114 @@
+package org.schabi.newpipe.settings.tabs;
+
+import android.support.annotation.Nullable;
+
+import com.grack.nanojson.JsonArray;
+import com.grack.nanojson.JsonObject;
+import com.grack.nanojson.JsonParser;
+import com.grack.nanojson.JsonParserException;
+import com.grack.nanojson.JsonStringWriter;
+import com.grack.nanojson.JsonWriter;
+
+import org.schabi.newpipe.settings.tabs.Tab.Type;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.schabi.newpipe.extractor.ServiceList.YouTube;
+
+/**
+ * Class to get a JSON representation of a list of tabs, and the other way around.
+ */
+public class TabsJsonHelper {
+ private static final String JSON_TABS_ARRAY_KEY = "tabs";
+
+ protected static final List FALLBACK_INITIAL_TABS_LIST = Collections.unmodifiableList(Arrays.asList(
+ new Tab.KioskTab(YouTube.getServiceId(), "Trending"),
+ Type.SUBSCRIPTIONS.getTab(),
+ Type.BOOKMARKS.getTab()
+ ));
+
+ public static class InvalidJsonException extends Exception {
+ private InvalidJsonException() {
+ super();
+ }
+
+ private InvalidJsonException(String message) {
+ super(message);
+ }
+
+ private InvalidJsonException(Throwable cause) {
+ super(cause);
+ }
+ }
+
+ /**
+ * Try to reads the passed JSON and returns the list of tabs if no error were encountered.
+ *
+ * If the JSON is null or empty, or the list of tabs that it represents is empty, the
+ * {@link #FALLBACK_INITIAL_TABS_LIST fallback list} will be returned.
+ *
+ * Tabs with invalid ids (i.e. not in the {@link Tab.Type} enum) will be ignored.
+ *
+ * @param tabsJson a JSON string got from {@link #getJsonToSave(List)}.
+ * @return a list of {@link Tab tabs}.
+ * @throws InvalidJsonException if the JSON string is not valid
+ */
+ public static List getTabsFromJson(@Nullable String tabsJson) throws InvalidJsonException {
+ if (tabsJson == null || tabsJson.isEmpty()) {
+ return FALLBACK_INITIAL_TABS_LIST;
+ }
+
+ final List returnTabs = new ArrayList<>();
+
+ final JsonObject outerJsonObject;
+ try {
+ outerJsonObject = JsonParser.object().from(tabsJson);
+ final JsonArray tabsArray = outerJsonObject.getArray(JSON_TABS_ARRAY_KEY);
+
+ if (tabsArray == null) {
+ throw new InvalidJsonException("JSON doesn't contain \"" + JSON_TABS_ARRAY_KEY + "\" array");
+ }
+
+ for (Object o : tabsArray) {
+ if (!(o instanceof JsonObject)) continue;
+
+ final Tab tab = Tab.from((JsonObject) o);
+
+ if (tab != null) {
+ returnTabs.add(tab);
+ }
+ }
+ } catch (JsonParserException e) {
+ throw new InvalidJsonException(e);
+ }
+
+ if (returnTabs.isEmpty()) {
+ return FALLBACK_INITIAL_TABS_LIST;
+ }
+
+ return returnTabs;
+ }
+
+ /**
+ * Get a JSON representation from a list of tabs.
+ *
+ * @param tabList a list of {@link Tab tabs}.
+ * @return a JSON string representing the list of tabs
+ */
+ public static String getJsonToSave(@Nullable List tabList) {
+ final JsonStringWriter jsonWriter = JsonWriter.string();
+ jsonWriter.object();
+
+ jsonWriter.array(JSON_TABS_ARRAY_KEY);
+ if (tabList != null) for (Tab tab : tabList) {
+ tab.writeJsonOn(jsonWriter);
+ }
+ jsonWriter.end();
+
+ jsonWriter.end();
+ return jsonWriter.done();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsManager.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsManager.java
new file mode 100644
index 000000000..a7d8dffa4
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsManager.java
@@ -0,0 +1,93 @@
+package org.schabi.newpipe.settings.tabs;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.widget.Toast;
+
+import org.schabi.newpipe.R;
+
+import java.util.List;
+
+public class TabsManager {
+ private final SharedPreferences sharedPreferences;
+ private final String savedTabsKey;
+ private final Context context;
+
+ public static TabsManager getManager(Context context) {
+ return new TabsManager(context);
+ }
+
+ private TabsManager(Context context) {
+ this.context = context;
+ this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ this.savedTabsKey = context.getString(R.string.saved_tabs_key);
+ }
+
+ public List getTabs() {
+ final String savedJson = sharedPreferences.getString(savedTabsKey, null);
+ try {
+ return TabsJsonHelper.getTabsFromJson(savedJson);
+ } catch (TabsJsonHelper.InvalidJsonException e) {
+ Toast.makeText(context, R.string.saved_tabs_invalid_json, Toast.LENGTH_SHORT).show();
+ return getDefaultTabs();
+ }
+ }
+
+ public void saveTabs(List tabList) {
+ final String jsonToSave = TabsJsonHelper.getJsonToSave(tabList);
+ sharedPreferences.edit().putString(savedTabsKey, jsonToSave).apply();
+ }
+
+ public void resetTabs() {
+ sharedPreferences.edit().remove(savedTabsKey).apply();
+ }
+
+ public List getDefaultTabs() {
+ return TabsJsonHelper.FALLBACK_INITIAL_TABS_LIST;
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Listener
+ //////////////////////////////////////////////////////////////////////////*/
+
+ public interface SavedTabsChangeListener {
+ void onTabsChanged();
+ }
+
+ private SavedTabsChangeListener savedTabsChangeListener;
+ private SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener;
+
+ public void setSavedTabsListener(SavedTabsChangeListener listener) {
+ if (preferenceChangeListener != null) {
+ sharedPreferences.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener);
+ }
+ savedTabsChangeListener = listener;
+ preferenceChangeListener = getPreferenceChangeListener();
+ sharedPreferences.registerOnSharedPreferenceChangeListener(preferenceChangeListener);
+ }
+
+ public void unsetSavedTabsListener() {
+ if (preferenceChangeListener != null) {
+ sharedPreferences.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener);
+ }
+ preferenceChangeListener = null;
+ savedTabsChangeListener = null;
+ }
+
+ private SharedPreferences.OnSharedPreferenceChangeListener getPreferenceChangeListener() {
+ return (sharedPreferences, key) -> {
+ if (key.equals(savedTabsKey)) {
+ if (savedTabsChangeListener != null) savedTabsChangeListener.onTabsChanged();
+ }
+ };
+ }
+
+}
+
+
+
+
+
+
+
diff --git a/app/src/main/java/org/schabi/newpipe/util/Constants.java b/app/src/main/java/org/schabi/newpipe/util/Constants.java
index a6aec96e2..b01b6df6a 100644
--- a/app/src/main/java/org/schabi/newpipe/util/Constants.java
+++ b/app/src/main/java/org/schabi/newpipe/util/Constants.java
@@ -6,7 +6,7 @@ public class Constants {
public static final String KEY_TITLE = "key_title";
public static final String KEY_LINK_TYPE = "key_link_type";
public static final String KEY_OPEN_SEARCH = "key_open_search";
- public static final String KEY_QUERY = "key_query";
+ public static final String KEY_SEARCH_STRING = "key_search_string";
public static final String KEY_THEME_CHANGE = "key_theme_change";
public static final String KEY_MAIN_PAGE_CHANGE = "key_main_page_change";
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 1897589c6..e445233c3 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java
@@ -37,9 +37,8 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
-import org.schabi.newpipe.extractor.search.SearchEngine;
-import org.schabi.newpipe.extractor.search.SearchResult;
-import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
+import org.schabi.newpipe.extractor.search.SearchInfo;
+import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
@@ -50,7 +49,6 @@ import java.util.List;
import io.reactivex.Maybe;
import io.reactivex.Single;
-import io.reactivex.annotations.NonNull;
public final class ExtractorHelper {
private static final String TAG = ExtractorHelper.class.getSimpleName();
@@ -66,29 +64,35 @@ public final class ExtractorHelper {
}
}
- public static Single searchFor(final int serviceId,
- final String query,
- final int pageNumber,
- final String contentCountry,
- final SearchEngine.Filter filter) {
+ public static Single searchFor(final int serviceId,
+ final String searchString,
+ final List contentFilter,
+ final String sortFilter,
+ final String contentCountry) {
checkServiceId(serviceId);
return Single.fromCallable(() ->
- SearchResult.getSearchResult(NewPipe.getService(serviceId).getSearchEngine(),
- query, pageNumber, contentCountry, filter)
- );
+ SearchInfo.getInfo(NewPipe.getService(serviceId),
+ NewPipe.getService(serviceId)
+ .getSearchQHFactory()
+ .fromQuery(searchString, contentFilter, sortFilter),
+ contentCountry));
}
public static Single getMoreSearchItems(final int serviceId,
- final String query,
- final int nextPageNumber,
- final String searchLanguage,
- final SearchEngine.Filter filter) {
+ final String searchString,
+ final List contentFilter,
+ final String sortFilter,
+ final String pageUrl,
+ final String contentCountry) {
checkServiceId(serviceId);
- return searchFor(serviceId, query, nextPageNumber, searchLanguage, filter)
- .map((@NonNull SearchResult searchResult) ->
- new InfoItemsPage(searchResult.resultList,
- nextPageNumber + "",
- searchResult.errors));
+ return Single.fromCallable(() ->
+ SearchInfo.getMoreItems(NewPipe.getService(serviceId),
+ NewPipe.getService(serviceId)
+ .getSearchQHFactory()
+ .fromQuery(searchString, contentFilter, sortFilter),
+ contentCountry,
+ pageUrl));
+
}
public static Single> suggestionsFor(final int serviceId,
@@ -233,7 +237,6 @@ public final class ExtractorHelper {
serviceId == -1 ? "none" : NewPipe.getNameOfService(serviceId), url + (optionalErrorMessage == null ? "" : optionalErrorMessage), errorId));
}
});
-
}
/**
diff --git a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java
index 4740b82e0..392892cef 100644
--- a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java
+++ b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java
@@ -24,7 +24,7 @@ import org.schabi.newpipe.R;
public class KioskTranslator {
public static String getTranslatedKioskName(String kioskId, Context c) {
- switch(kioskId) {
+ switch (kioskId) {
case "Trending":
return c.getString(R.string.trending);
case "Top 50":
@@ -35,4 +35,17 @@ public class KioskTranslator {
return kioskId;
}
}
+
+ public static int getKioskIcons(String kioskId, Context c) {
+ switch(kioskId) {
+ case "Trending":
+ return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
+ case "Top 50":
+ return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
+ case "New & hot":
+ return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
+ default:
+ return 0;
+ }
+ }
}
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 4f607b581..1a5bf14f7 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java
@@ -443,11 +443,11 @@ public final class ListHelper {
/**
* Are we connected to wifi?
* @param context App context
- * @return True if connected to wifi
+ * @return {@code true} if connected to wifi
*/
private static boolean isWifiActive(Context context)
{
ConnectivityManager manager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
- return manager.getActiveNetworkInfo().getType() == ConnectivityManager.TYPE_WIFI;
+ return manager != null && manager.getActiveNetworkInfo() != null && manager.getActiveNetworkInfo().getType() == ConnectivityManager.TYPE_WIFI;
}
}
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 ebbeb06f8..13767125d 100644
--- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
@@ -26,6 +26,7 @@ import org.schabi.newpipe.download.DownloadActivity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
+import org.schabi.newpipe.extractor.search.SearchExtractor;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
@@ -33,12 +34,14 @@ 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.local.bookmark.BookmarkFragment;
import org.schabi.newpipe.local.feed.FeedFragment;
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment;
import org.schabi.newpipe.local.history.StatisticsPlaylistFragment;
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
+import org.schabi.newpipe.local.subscription.SubscriptionFragment;
import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment;
import org.schabi.newpipe.player.BackgroundPlayer;
import org.schabi.newpipe.player.BackgroundPlayerActivity;
@@ -100,11 +103,13 @@ public class NavigationHelper {
final int repeatMode,
final float playbackSpeed,
final float playbackPitch,
+ final boolean playbackSkipSilence,
@Nullable final String playbackQuality) {
return getPlayerIntent(context, targetClazz, playQueue, playbackQuality)
.putExtra(BasePlayer.REPEAT_MODE, repeatMode)
.putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed)
- .putExtra(BasePlayer.PLAYBACK_PITCH, playbackPitch);
+ .putExtra(BasePlayer.PLAYBACK_PITCH, playbackPitch)
+ .putExtra(BasePlayer.PLAYBACK_SKIP_SILENCE, playbackSkipSilence);
}
public static void playOnMainPlayer(final Context context, final PlayQueue queue) {
@@ -281,9 +286,11 @@ public class NavigationHelper {
return fragmentManager.popBackStackImmediate(SEARCH_FRAGMENT_TAG, 0);
}
- public static void openSearchFragment(FragmentManager fragmentManager, int serviceId, String query) {
+ public static void openSearchFragment(FragmentManager fragmentManager,
+ int serviceId,
+ String searchString) {
defaultTransaction(fragmentManager)
- .replace(R.id.fragment_holder, SearchFragment.getInstance(serviceId, query))
+ .replace(R.id.fragment_holder, SearchFragment.getInstance(serviceId, searchString))
.addToBackStack(SEARCH_FRAGMENT_TAG)
.commit();
}
@@ -312,7 +319,11 @@ public class NavigationHelper {
.commit();
}
- public static void openChannelFragment(FragmentManager fragmentManager, int serviceId, String url, String name) {
+ public static void openChannelFragment(
+ FragmentManager fragmentManager,
+ int serviceId,
+ String url,
+ String name) {
if (name == null) name = "";
defaultTransaction(fragmentManager)
.replace(R.id.fragment_holder, ChannelFragment.getInstance(serviceId, url, name))
@@ -320,7 +331,10 @@ public class NavigationHelper {
.commit();
}
- public static void openPlaylistFragment(FragmentManager fragmentManager, int serviceId, String url, String name) {
+ public static void openPlaylistFragment(FragmentManager fragmentManager,
+ int serviceId,
+ String url,
+ String name) {
if (name == null) name = "";
defaultTransaction(fragmentManager)
.replace(R.id.fragment_holder, PlaylistFragment.getInstance(serviceId, url, name))
@@ -335,6 +349,20 @@ public class NavigationHelper {
.commit();
}
+ public static void openBookmarksFragment(FragmentManager fragmentManager) {
+ defaultTransaction(fragmentManager)
+ .replace(R.id.fragment_holder, new BookmarkFragment())
+ .addToBackStack(null)
+ .commit();
+ }
+
+ public static void openSubscriptionFragment(FragmentManager fragmentManager) {
+ defaultTransaction(fragmentManager)
+ .replace(R.id.fragment_holder, new SubscriptionFragment())
+ .addToBackStack(null)
+ .commit();
+ }
+
public static void openKioskFragment(FragmentManager fragmentManager, int serviceId, String kioskId) throws ExtractionException {
defaultTransaction(fragmentManager)
.replace(R.id.fragment_holder, KioskFragment.getInstance(serviceId, kioskId))
@@ -368,10 +396,10 @@ public class NavigationHelper {
// Through Intents
//////////////////////////////////////////////////////////////////////////*/
- public static void openSearch(Context context, int serviceId, String query) {
+ public static void openSearch(Context context, int serviceId, String searchString) {
Intent mIntent = new Intent(context, MainActivity.class);
mIntent.putExtra(Constants.KEY_SERVICE_ID, serviceId);
- mIntent.putExtra(Constants.KEY_QUERY, query);
+ mIntent.putExtra(Constants.KEY_SEARCH_STRING, searchString);
mIntent.putExtra(Constants.KEY_OPEN_SEARCH, true);
context.startActivity(mIntent);
}
@@ -465,7 +493,8 @@ public class NavigationHelper {
switch (linkType) {
case STREAM:
- rIntent.putExtra(VideoDetailFragment.AUTO_PLAY, PreferenceManager.getDefaultSharedPreferences(context)
+ rIntent.putExtra(VideoDetailFragment.AUTO_PLAY,
+ PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.autoplay_through_intent_key), false));
break;
}
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 d86f27f2f..7c781eb14 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java
@@ -5,7 +5,6 @@ import android.preference.PreferenceManager;
import android.support.annotation.DrawableRes;
import android.support.annotation.StringRes;
-import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
@@ -31,6 +30,18 @@ public class ServiceHelper {
}
}
+ public static String getTranslatedFilterString(String filter, Context c) {
+ switch(filter) {
+ case "all": return c.getString(R.string.all);
+ case "videos": return c.getString(R.string.videos);
+ case "channels": return c.getString(R.string.channels);
+ case "playlists": return c.getString(R.string.playlists);
+ case "tracks": return c.getString(R.string.tracks);
+ case "users": return c.getString(R.string.users);
+ default: return filter;
+ }
+ }
+
/**
* Get a resource string with instructions for importing subscriptions for each service.
*
diff --git a/app/src/main/java/us/shandian/giga/get/DownloadManagerImpl.java b/app/src/main/java/us/shandian/giga/get/DownloadManagerImpl.java
index ecd3ce562..3294f5164 100755
--- a/app/src/main/java/us/shandian/giga/get/DownloadManagerImpl.java
+++ b/app/src/main/java/us/shandian/giga/get/DownloadManagerImpl.java
@@ -1,10 +1,17 @@
package us.shandian.giga.get;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
+import org.schabi.newpipe.download.ExtSDDownloadFailedActivity;
+
import java.io.File;
import java.io.FilenameFilter;
+import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
@@ -23,7 +30,9 @@ public class DownloadManagerImpl implements DownloadManager {
private static final String TAG = DownloadManagerImpl.class.getSimpleName();
private final DownloadDataSource mDownloadDataSource;
- private final ArrayList mMissions = new ArrayList();
+ private final ArrayList mMissions = new ArrayList<>();
+ @NonNull
+ private final Context context;
/**
* Create a new instance
@@ -33,6 +42,13 @@ public class DownloadManagerImpl implements DownloadManager {
*/
public DownloadManagerImpl(Collection searchLocations, DownloadDataSource downloadDataSource) {
mDownloadDataSource = downloadDataSource;
+ this.context = null;
+ loadMissions(searchLocations);
+ }
+
+ public DownloadManagerImpl(Collection searchLocations, DownloadDataSource downloadDataSource, Context context) {
+ mDownloadDataSource = downloadDataSource;
+ this.context = context;
loadMissions(searchLocations);
}
@@ -277,10 +293,12 @@ public class DownloadManagerImpl implements DownloadManager {
}
private class Initializer extends Thread {
- private DownloadMission mission;
+ private final DownloadMission mission;
+ private final Handler handler;
public Initializer(DownloadMission mission) {
this.mission = mission;
+ this.handler = new Handler();
}
@Override
@@ -335,6 +353,13 @@ public class DownloadManagerImpl implements DownloadManager {
af.close();
mission.start();
+ } catch (IOException ie) {
+ if(context == null) throw new RuntimeException(ie);
+
+ if(ie.getMessage().contains("Permission denied")) {
+ handler.post(() ->
+ context.startActivity(new Intent(context, ExtSDDownloadFailedActivity.class)));
+ } else throw new RuntimeException(ie);
} catch (Exception e) {
// TODO Notify
throw new RuntimeException(e);
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 50975728f..59f5e2225 100755
--- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java
+++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java
@@ -81,7 +81,7 @@ public class DownloadManagerService extends Service {
ArrayList paths = new ArrayList<>(2);
paths.add(NewPipeSettings.getVideoDownloadPath(this));
paths.add(NewPipeSettings.getAudioDownloadPath(this));
- mManager = new DownloadManagerImpl(paths, mDataSource);
+ mManager = new DownloadManagerImpl(paths, mDataSource, this);
if (DEBUG) {
Log.d(TAG, "mManager == null");
Log.d(TAG, "Download directory: " + paths);
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 12c81c127..8127c3467 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
@@ -25,10 +25,13 @@ import android.widget.TextView;
import android.widget.Toast;
import org.schabi.newpipe.R;
+import org.schabi.newpipe.download.DeleteDownloadManager;
import java.io.File;
import java.lang.ref.WeakReference;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -52,18 +55,34 @@ public class MissionAdapter extends RecyclerView.Adapter mItemList;
private DownloadManagerService.DMBinder mBinder;
private int mLayout;
- public MissionAdapter(Activity context, DownloadManagerService.DMBinder binder, DownloadManager manager, boolean isLinear) {
+ public MissionAdapter(Activity context, DownloadManagerService.DMBinder binder, DownloadManager downloadManager, DeleteDownloadManager deleteDownloadManager, boolean isLinear) {
mContext = context;
- mManager = manager;
+ mDownloadManager = downloadManager;
+ mDeleteDownloadManager = deleteDownloadManager;
mBinder = binder;
mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-
mLayout = isLinear ? R.layout.mission_item_linear : R.layout.mission_item;
+
+ mItemList = new ArrayList<>();
+ updateItemList();
+ }
+
+ public void updateItemList() {
+ mItemList.clear();
+
+ for (int i = 0; i < mDownloadManager.getCount(); i++) {
+ DownloadMission mission = mDownloadManager.getMission(i);
+ if (!mDeleteDownloadManager.contains(mission)) {
+ mItemList.add(mDownloadManager.getMission(i));
+ }
+ }
}
@Override
@@ -102,7 +121,7 @@ public class MissionAdapter extends RecyclerView.Adapter= Build.VERSION_CODES.LOLLIPOP) {
- intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION);
- }
- //mContext.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
- Log.v(TAG, "Starting intent: " + intent);
- mContext.startActivity(intent);
- }
-
private void viewFileWithFileProvider(File file, String mimetype) {
String ourPackage = mContext.getApplicationContext().getPackageName();
Uri uri = FileProvider.getUriForFile(mContext, ourPackage + ".provider", file);
diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java
index 2ff83086f..14439f6c8 100644
--- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java
+++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java
@@ -10,6 +10,8 @@ import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@@ -19,13 +21,15 @@ import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
+import org.schabi.newpipe.download.DeleteDownloadManager;
+import io.reactivex.disposables.Disposable;
import us.shandian.giga.get.DownloadManager;
import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.ui.adapter.MissionAdapter;
public abstract class MissionsFragment extends Fragment {
- private DownloadManager mManager;
+ private DownloadManager mDownloadManager;
private DownloadManagerService.DMBinder mBinder;
private SharedPreferences mPrefs;
@@ -37,14 +41,19 @@ public abstract class MissionsFragment extends Fragment {
private GridLayoutManager mGridManager;
private LinearLayoutManager mLinearManager;
private Context mActivity;
+ private DeleteDownloadManager mDeleteDownloadManager;
+ private Disposable mDeleteDisposable;
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
mBinder = (DownloadManagerService.DMBinder) binder;
- mManager = setupDownloadManager(mBinder);
- updateList();
+ mDownloadManager = setupDownloadManager(mBinder);
+ if (mDeleteDownloadManager != null) {
+ mDeleteDownloadManager.setDownloadManager(mDownloadManager);
+ updateList();
+ }
}
@Override
@@ -55,6 +64,14 @@ public abstract class MissionsFragment extends Fragment {
};
+ public void setDeleteManager(@NonNull DeleteDownloadManager deleteDownloadManager) {
+ mDeleteDownloadManager = deleteDownloadManager;
+ if (mDownloadManager != null) {
+ mDeleteDownloadManager.setDownloadManager(mDownloadManager);
+ updateList();
+ }
+ }
+
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.missions, container, false);
@@ -104,10 +121,26 @@ public abstract class MissionsFragment extends Fragment {
mActivity = activity;
}
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ if (mDeleteDownloadManager != null) {
+ mDeleteDisposable = mDeleteDownloadManager.getUndoObservable().subscribe(mission -> {
+ if (mAdapter != null) {
+ mAdapter.updateItemList();
+ mAdapter.notifyDataSetChanged();
+ }
+ });
+ }
+ }
+
@Override
public void onDestroyView() {
super.onDestroyView();
getActivity().unbindService(mConnection);
+ if (mDeleteDisposable != null) {
+ mDeleteDisposable.dispose();
+ }
}
@Override
@@ -129,7 +162,7 @@ public abstract class MissionsFragment extends Fragment {
}
private void updateList() {
- mAdapter = new MissionAdapter((Activity) mActivity, mBinder, mManager, mLinear);
+ mAdapter = new MissionAdapter((Activity) mActivity, mBinder, mDownloadManager, mDeleteDownloadManager, mLinear);
if (mLinear) {
mList.setLayoutManager(mLinearManager);
@@ -143,7 +176,7 @@ public abstract class MissionsFragment extends Fragment {
mSwitch.setIcon(mLinear ? R.drawable.grid : R.drawable.list);
}
- mPrefs.edit().putBoolean("linear", mLinear).commit();
+ mPrefs.edit().putBoolean("linear", mLinear).apply();
}
protected abstract DownloadManager setupDownloadManager(DownloadManagerService.DMBinder binder);
diff --git a/app/src/main/res/drawable-hdpi/ic_add.png b/app/src/main/res/drawable-hdpi/ic_add.png
new file mode 100644
index 000000000..1ae5b2dc4
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_add.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_arrow_down_white.png b/app/src/main/res/drawable-hdpi/ic_arrow_down_white.png
new file mode 100644
index 000000000..33939600d
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_arrow_down_white.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_arrow_up_white.png b/app/src/main/res/drawable-hdpi/ic_arrow_up_white.png
new file mode 100644
index 000000000..0972a9bca
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_arrow_up_white.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_remove.png b/app/src/main/res/drawable-hdpi/ic_remove.png
new file mode 100644
index 000000000..75e65bc9c
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_remove.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_add.png b/app/src/main/res/drawable-mdpi/ic_add.png
new file mode 100644
index 000000000..d51f0ddad
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_add.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_arrow_down_white.png b/app/src/main/res/drawable-mdpi/ic_arrow_down_white.png
new file mode 100644
index 000000000..40a0f499e
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_arrow_down_white.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_arrow_up_white.png b/app/src/main/res/drawable-mdpi/ic_arrow_up_white.png
new file mode 100644
index 000000000..fe67b4673
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_arrow_up_white.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_remove.png b/app/src/main/res/drawable-mdpi/ic_remove.png
new file mode 100644
index 000000000..a1816d4c6
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_remove.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_add.png b/app/src/main/res/drawable-xhdpi/ic_add.png
new file mode 100644
index 000000000..9ea0eeb7e
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_add.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_arrow_down_white.png b/app/src/main/res/drawable-xhdpi/ic_arrow_down_white.png
new file mode 100644
index 000000000..86bc5db3b
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_arrow_down_white.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_arrow_up_white.png b/app/src/main/res/drawable-xhdpi/ic_arrow_up_white.png
new file mode 100644
index 000000000..dda36882e
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_arrow_up_white.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_remove.png b/app/src/main/res/drawable-xhdpi/ic_remove.png
new file mode 100644
index 000000000..ffbdaa6ed
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_remove.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_add.png b/app/src/main/res/drawable-xxhdpi/ic_add.png
new file mode 100644
index 000000000..75f192aab
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_add.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_arrow_down_white.png b/app/src/main/res/drawable-xxhdpi/ic_arrow_down_white.png
new file mode 100644
index 000000000..7e901e098
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_arrow_down_white.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_arrow_up_white.png b/app/src/main/res/drawable-xxhdpi/ic_arrow_up_white.png
new file mode 100644
index 000000000..bc71e23de
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_arrow_up_white.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_remove.png b/app/src/main/res/drawable-xxhdpi/ic_remove.png
new file mode 100644
index 000000000..d35469d3c
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_remove.png differ
diff --git a/app/src/main/res/drawable/background_oval_black_transparent.xml b/app/src/main/res/drawable/background_oval_black_transparent.xml
new file mode 100644
index 000000000..5db5969c6
--- /dev/null
+++ b/app/src/main/res/drawable/background_oval_black_transparent.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_add_black_24dp.xml b/app/src/main/res/drawable/ic_add_black_24dp.xml
new file mode 100644
index 000000000..0258249cc
--- /dev/null
+++ b/app/src/main/res/drawable/ic_add_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_add_white_24dp.xml b/app/src/main/res/drawable/ic_add_white_24dp.xml
new file mode 100644
index 000000000..e3979cd7f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_add_white_24dp.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_blank_page_black_24dp.xml b/app/src/main/res/drawable/ic_blank_page_black_24dp.xml
new file mode 100644
index 000000000..e8c60a1a2
--- /dev/null
+++ b/app/src/main/res/drawable/ic_blank_page_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_blank_page_white_24dp.xml b/app/src/main/res/drawable/ic_blank_page_white_24dp.xml
new file mode 100644
index 000000000..86a68484f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_blank_page_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_brightness_high_white_72dp.xml b/app/src/main/res/drawable/ic_brightness_high_white_72dp.xml
new file mode 100644
index 000000000..12d0084a8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_brightness_high_white_72dp.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_brightness_low_white_72dp.xml b/app/src/main/res/drawable/ic_brightness_low_white_72dp.xml
new file mode 100644
index 000000000..9c4f2f71e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_brightness_low_white_72dp.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_brightness_medium_white_72dp.xml b/app/src/main/res/drawable/ic_brightness_medium_white_72dp.xml
new file mode 100644
index 000000000..fc100086f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_brightness_medium_white_72dp.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_settings_backup_restore_black_24dp.xml b/app/src/main/res/drawable/ic_settings_backup_restore_black_24dp.xml
new file mode 100644
index 000000000..aa424c0d4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_settings_backup_restore_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_settings_backup_restore_white_24dp.xml b/app/src/main/res/drawable/ic_settings_backup_restore_white_24dp.xml
new file mode 100644
index 000000000..03a26f550
--- /dev/null
+++ b/app/src/main/res/drawable/ic_settings_backup_restore_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_volume_down_white_72dp.xml b/app/src/main/res/drawable/ic_volume_down_white_72dp.xml
new file mode 100644
index 000000000..a7fafb3a5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_volume_down_white_72dp.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_volume_mute_white_72dp.xml b/app/src/main/res/drawable/ic_volume_mute_white_72dp.xml
new file mode 100644
index 000000000..1a8ab7e86
--- /dev/null
+++ b/app/src/main/res/drawable/ic_volume_mute_white_72dp.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_volume_off_white_72dp.xml b/app/src/main/res/drawable/ic_volume_off_white_72dp.xml
new file mode 100644
index 000000000..07f24d7aa
--- /dev/null
+++ b/app/src/main/res/drawable/ic_volume_off_white_72dp.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_volume_up_white_72dp.xml b/app/src/main/res/drawable/ic_volume_up_white_72dp.xml
new file mode 100644
index 000000000..b2fb235a6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_volume_up_white_72dp.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/progress_circular_white.xml b/app/src/main/res/drawable/progress_circular_white.xml
new file mode 100644
index 000000000..daa6649bc
--- /dev/null
+++ b/app/src/main/res/drawable/progress_circular_white.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout-v21/drawer_header.xml b/app/src/main/res/layout-v21/drawer_header.xml
new file mode 100644
index 000000000..4cdc2b30e
--- /dev/null
+++ b/app/src/main/res/layout-v21/drawer_header.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 99c637389..b70d73250 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -4,7 +4,8 @@
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
-
\ No newline at end of file
+
diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml
index f2cf85802..3dccc5c4c 100644
--- a/app/src/main/res/layout/activity_main_player.xml
+++ b/app/src/main/res/layout/activity_main_player.xml
@@ -41,7 +41,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
- android:scaleType="centerInside"
android:visibility="gone"
tools:background="@android:color/white"
tools:ignore="ContentDescription"
@@ -472,10 +471,11 @@
android:id="@+id/controlAnimationView"
android:layout_width="100dp"
android:layout_height="100dp"
- android:src="@drawable/ic_action_av_fast_rewind"
+ android:background="@drawable/background_oval_black_transparent"
android:visibility="gone"
tools:ignore="ContentDescription"
- tools:visibility="visible"/>
+ tools:src="@drawable/ic_action_av_fast_rewind"
+ tools:visibility="visible" />
@@ -504,43 +504,57 @@
android:layout_toRightOf="@+id/loading_panel"
tools:ignore="RtlHardcoded">
-
+ tools:visibility="visible">
-
+
+
+
+
+
+ tools:visibility="visible">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+ android:layout_height="wrap_content"
+ android:checked="false"
+ android:clickable="true"
+ android:focusable="true"
+ android:text="@string/skip_silence_checkbox"
+ android:maxLines="1"
+ android:layout_centerHorizontal="true"
+ android:layout_below="@id/unhookCheckbox"/>
diff --git a/app/src/main/res/layout/drawer_header.xml b/app/src/main/res/layout/drawer_header.xml
new file mode 100644
index 000000000..c119c302a
--- /dev/null
+++ b/app/src/main/res/layout/drawer_header.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/drawer_layout.xml b/app/src/main/res/layout/drawer_layout.xml
index c0186a02c..4732df719 100644
--- a/app/src/main/res/layout/drawer_layout.xml
+++ b/app/src/main/res/layout/drawer_layout.xml
@@ -1,81 +1,22 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:focusable="true"
+ >
+ app:elevation="0dp"
+ android:background="?attr/android:windowBackground"
+ app:headerLayout="@layout/drawer_header"/>
-
-
-
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_choose_tabs.xml b/app/src/main/res/layout/fragment_choose_tabs.xml
new file mode 100644
index 000000000..0097a409a
--- /dev/null
+++ b/app/src/main/res/layout/fragment_choose_tabs.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_video_detail.xml b/app/src/main/res/layout/fragment_video_detail.xml
index 2b1f94c4a..7c6568b67 100644
--- a/app/src/main/res/layout/fragment_video_detail.xml
+++ b/app/src/main/res/layout/fragment_video_detail.xml
@@ -101,7 +101,8 @@
+ android:layout_height="match_parent"
+ android:background="?android:windowBackground">
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_choose_tabs_dialog.xml b/app/src/main/res/layout/list_choose_tabs_dialog.xml
new file mode 100644
index 000000000..8c6574e6d
--- /dev/null
+++ b/app/src/main/res/layout/list_choose_tabs_dialog.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/player_popup.xml b/app/src/main/res/layout/player_popup.xml
index f866cf002..7a92c6712 100644
--- a/app/src/main/res/layout/player_popup.xml
+++ b/app/src/main/res/layout/player_popup.xml
@@ -9,7 +9,6 @@
tools:layout_height="84dp"
tools:layout_width="@dimen/popup_minimum_width">
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/player_popup_close_overlay.xml b/app/src/main/res/layout/player_popup_close_overlay.xml
new file mode 100644
index 000000000..f7aceadec
--- /dev/null
+++ b/app/src/main/res/layout/player_popup_close_overlay.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/statistic_playlist_control.xml b/app/src/main/res/layout/statistic_playlist_control.xml
index 8dc4e8c08..ff2b70524 100644
--- a/app/src/main/res/layout/statistic_playlist_control.xml
+++ b/app/src/main/res/layout/statistic_playlist_control.xml
@@ -4,7 +4,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:background="?attr/selectableItemBackground"
android:orientation="vertical">
+ android:focusable="true"
+ android:background="?attr/selectableItemBackground">
+
-
-
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/main_menu.xml b/app/src/main/res/menu/main_menu.xml
index b6372433e..05920099a 100644
--- a/app/src/main/res/menu/main_menu.xml
+++ b/app/src/main/res/menu/main_menu.xml
@@ -1,6 +1,6 @@
\ 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 8711f2712..fdb859609 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -1,8 +1,8 @@
- جاري التشغيل في الخلفية
+ يتم التشغيل في الخلفية
إلغاء
- إختر متصفح
+ اختر المتصفح
مظلم
صيغة الصوت الإفتراضية
الدقة الإفتراضية
@@ -25,7 +25,6 @@
تطبيق Kore غير موجود. هل تريد تثبيته ؟
مضيء
صور معاينة الفيديو
- M4A — جودة أفضل
خطأ في الشبكة
الفيديو التالي
لا يوجد مشغل فيديو. هل تريد تثبيت VLC ؟
@@ -42,7 +41,7 @@
الفيديو والصوتيات
مشاركة
مشاركة بواسطة
- عرض\'مقاطع\'الفيديو\'التالية\'و\'المشابهة\'
+ عرض الفديوهات \'التالية\'و\'المماثلة\'
عرض خيار لتشغيل الفيديو بواسطة Kodi Media Center
عرض خيار التشغيل بواسطة Kodi
السمة
@@ -51,20 +50,19 @@
استخدام مشغل صوت خارجي
استخدام مشغل فيديو خارجي
(إختبارية) إجراء التنزيلات من خلال استخدام بروكسي Tor لزيادة الخصوصية ( تشغيل الفيديو المباشر غير مدعوم حتى الأن ).
- استخدام Tor
+ استخدام تور
مشاهدات %1$s
- تنسيق حر — WebM
تم حجبه بواسطة GEMA
المحتوى غير متاح
- لم يتمكن من تحميل كل صور المعاينة
+ تعذرت عملية تحميل كافة صور المعاينة
خطأ
تعذرت عملية تحليل الموقع
تعذر فك تشفير توقيع رابط الفيديو
اضغط بحث للبدء
- إشتراك
+ اشتراك
مشترك
الرئيسية
- الإشتراكات
+ الاشتراكات
ما الجديد
@@ -72,7 +70,7 @@
تشغيل تلقائي
اسود
التاريخ وذاكرة التخزين المؤقت
- التأريخ و ذاكرة التخزين المؤقتة
+ التاريخ و ذاكرة التخزين المؤقتة
المحتوى
التنزيلات
التنزيلات
@@ -84,24 +82,24 @@
عن التطبيق
التاريخ
التاريخ
- فتح في وضع النوافذ المنبثقة
- "بعض القرارات لن يكون الصوت عند تمكين هذا الخيار "
+ فتح في وضع النافذة المنبثقة
+ "بعض الخيارات الدقة لن تحتوي على صوت عند تمكين هذا الخيار "
وضع النوافذ المنبثقة NewPipe
- تم إلغاء اشتراك القناة
- تعذر تغيير في الاشتراك
+ تم إلغاء الاشتراك في القناة
+ تعذر تغيير حالة الاشتراك
تعذر تحديث الاشتراك
نافذة
تشغيل مقطع الفيديو عند إستدعاء NewPipe من تطبيق آخر
- "دقة النوافذ المنبثقة الافتراضية "
+ الدقة الافتراضية لنوافذ المنبثقة
"عرض أعلى جودة "
بعض الأجهزة فقط تدعم تشغيل مقاطع الفيديو 2K / 4K
تنسيق الفيديو الافتراضي
تذكر حجم النافذة و وضعها
- تذكر آخر حجم ومكان للنافذة
+ تذكر آخر مكان و حجم للنافذة المنبثقة
اعدادات إيماءة المشغل
- استخدم إيماءات للتحكم في سطوع وحجم المشغل
+ استخدم إيماءات التحكم في سطوع وصوت المشغل
اقتراحات البحث
عرض الاقتراحات عند البحث
سجل البحث
@@ -114,7 +112,7 @@
المشغل
السلوك
المنبثق
- التشغيل في الوضع المنبثق
+ يتم التشغيل في الوضع المنبثق
تم وضعه على قائمة الانتظار في مشغل الخلفية
تم وضعه على قائمة الانتظار في مشغل النافذة المنبثقة
عرض المحتوى المقيّد بحسب العُمر
@@ -138,11 +136,11 @@
[غير معروف]
- لايمكن تحليل الموقع بشكل كلي
- يتعذر إعداد قائمة التنزيل
- هذا هو بث مباشر ، وهو غير معتمد حتى الآن.
- يتعذر الحصول على أي بث
- تعذر تحميل الصورة
+ لا يمكن تحليل الموقع بشكل كلي
+ تعذرت عملية إعداد قائمة التنزيل
+ هذا بث مباشر، وهو غير معتمد الآن.
+ تعذر الحصول على أي بث
+ تعذرت عملية تحميل الصورة
تعطل التطبيق / واجهة المستخدم
لا يمكن تشغيل هذا البث
حدث خطأ للمشغل غير قابل للاسترداد
@@ -156,7 +154,7 @@
ماذا حدث:
ماذا:\\nطلب:\\nيحتوى اللغة:\\nSالخدمات:\\nتوقيت غرينتش:\\nحزمة:\\nالإصدار:\\nOS إصدار نظام التشغيل:
تعليقك (باللغة الإنجليزية):
- تفاصيل:
+ التفاصيل:
الإبلاغ عن خطأ
@@ -170,41 +168,41 @@
استخدام المشغل القديم
المشغل القديم المدمج في إطار Mediaframework
- ك
- م
- ب
+ ألف
+ مليون
+ بليون
- صفر لا تقم با الإختيار (في بعض اللغات) لأنها ليست \"حالة خاصة\" للأندرويد
+ ليس هناك مشترِكون
- - صفر
- - %s مشترك
- - اثنان
- - قليل
- - كثير
+ - صفر مشترِك
+ - %s مشترِك
+ - مشترِكان
+ - %s مشترك
+ - %s مشترك
- %s مشتركون
- لاتوجد مشاهدات
- لاتوجد فديوهات
- بداية
+ دون مشاهدات
+ لاتوجد فيديوهات
+ تشغيل
إيقاف
شغل
حذف
التوقيع
مهمة جديدة
- حسنا
+ حسناً
اسم الملف
- العمليات
+ الخيوط
الخطأ
الخادم غير معتمد
الملف موجود مسبقا
- العنوان غير صحيح أو ان الإنترنت غير متوفر
- NewPipe يقوم بالتحميل
+ العنوان غير صحيح أو الإنترنت غير متوفر
+ يقوم نيوبايب بالتنزيل
انقر للحصول على التفاصيل
- أرجو الإنتظار…
- نسخ إلى الحافظة
+ يُرجى الإنتظار…
+ تم نسخه إلى الحافظة
الرجاء تحديد مجلد لحفظ التنزيلات
هذا الإذن مطلوب
\nللفتح في وضع النافذة المنبثقة
@@ -217,28 +215,28 @@
الحروف والأرقام
معظم الأحرف الخاصة
- معلومات عن NewPipe
+ عن تطبيق نيوپايپ
الإعدادات
تراخيص الجهات الخارجية
تعذر تحميل الترخيص
فتح الموقع
المساهمون
التراخيص
- بث مباشر على يوتيوب مجاني خفيف الوزن لنظام أندرويد.
+ تطبيق حُر و خفيف لالتقاط البث على نظام الأندرويد.
ساهم
إذا كانت لديك أفكار؛ أو ترجمة، أو تغييرات تخص التصميم، أو تنظيف و تحسين الشفرة البرمجية ، أو تعديلات عميقة عليها، فتذكر أنّ مساعدتك دائما موضع ترحيب. وكلما أتممنا شيئا كلما كان ذلك أفضل !
عرض على GitHub
تبرع
- "يتم تطوير NewPipe من قبل المتطوعين الذين يقضون وقت فراغهم لتقديم أفضل تجربة لك. حان الوقت لرد المساعدة مع المطورين وجعل NewPipe أكثر و أفضل أثناء الاستمتاع بفنجان من القهوة "
+ يتم تطوير NewPipe من قبل متطوعين يقضون وقت فراغهم لتقديم أفضل تجربة لك. حان الوقت لرد المساعدة مع المطورين وجعل NewPipe أكثر و أفضل بينما تستمتع بفنجان من القهوة.
تبرع
- الموقع
- قم بزيارة موقع NewPipe لمزيد من المعلومات والأخبار.
+ موقع الويب
+ قم بزيارة موقع NewPipe لمزيد من المعلومات والمستجدات.
تراخيص NewPipe
- قراءة الترخيص
+ قراءة الرخصة
- البحث
- شاهد
+ التي تم البحث عنها
+ تمت مشاهدتها
تم تعطيل السجل
التاريخ فارغ
تم مسح التاريخ
@@ -250,36 +248,36 @@
صفحة الاشتراك
صفحة الخلاصة
صفحة القناة
- حدد قناة
- لم يتم الاشتراك في القناة بعد
- الترند
+ اختر قناة
+ لم يتم الاشتراك في أي قناة بعد
+ الشائعة
أفضل 50
- جديد & وساخن
+ جديد وساخن
مشغل الخلفية
المشغل المنبثق
حذف
- تفاصيل
- إعدادات الصوت
- بدء التشغيل هنا
+ التفاصيل
+ الإعدادات الصوتية
+ ابدء التشغيل هنا
تشغيل هنا في وضع النافذة المنبثقة
تحدي ريكابتشا
- اضغط للإدراج بقائمة الانتظار
+ اضغط للإدراج في قائمة الانتظار
- - لم تتم المشاهدة
+ - لاتوجد مشاهدة
- %s مشاهدة
- - "اثنان مشاهدات "
+ - مشاهدتين
- %s مشاهدات كثيرة
- %s عدد المشاهدات
- %s أقصى مشاهدات
- - صفر
- - واحد
- - اثنان
- - قليل
- - عدد كثير
- - "أخرى "
+ - %s فيديو
+ - %s فيديو
+ - %s فيديوهات
+ - %s فيديوهات
+ - %s فيديوهات
+ - %s فيديوهات
إعادة طلب كلمة التحقق
@@ -288,35 +286,35 @@
صفحة الكشك
حدد كشك
- كشك
- إدراج بقائمة الانتظار على خلفية
- إدراج بقائمة الانتظار على المنبثقة
- ابدأ هنا على خلفية المصدر
+ الكشك
+ إدراج في قائمة الانتظار في الخلفية
+ إدراج في قائمة الانتظار على المنبثقة
+ ابدأ هنا في الخلفية
المحتوى الإفتراضي حسب البلد
تغيير الإتجاه
- الإنتقال إلى الخلفية
- الإنتقال إلى نافذة منبثقة
- التحول إلى الرئيسية
+ الإنتقال إلى التشغيل في الخلفية
+ الإنتقال إلى التشغيل في النافذة المنبثقة
+ الإنتقال إلى الرئيسية
الخدمة
فتح الدرج
إغلاق الدرج
- دائمًا
+ دائماً
مرة واحدة فقط
العنوان خاطئ
- لم يتم العثور على مشغل بث (يمكنك تثبيت VLC لتشغيله)
+ لم يتم العثور على مشغل الفديو (يمكنك تثبيت VLC لتشغيله)
استيراد قاعدة البيانات
تصدير قاعدة البيانات
"سيقوم بالكتابة على سجل التاريخ والاشتراكات الحالية "
- تصدير سجل, الاشتراكات وقوائم التشغيل.
+ تصدير السجل، الإشتراكات وقوائم التشغيل
عرض المعلومات
إضافة إلى
تحليل
مٌباشِر
- لم يتم العثور على أي بث
+ لم يتم العثور على أي بث مرئي
لم يتم العثور على أي بث صوتي
إسحب للقيام بالترتيب
@@ -333,8 +331,8 @@
سوف يظهر شيء هنا قريبا ;D
- مشغل الفديو
- السؤال دائمًا
+ مشغل الفيديو
+ السؤال دائماً
الحصول على المعلومات …
تحميل المحتوى المطلوب
@@ -345,11 +343,11 @@
التسمية
إضافة إلى قائمة تشغيل
هل تريد حذف قائمة التشغيل هذه ؟
- قائمة التشغيل التي تم إنشاؤها
+ تم إنشاء قائمة التشغيل
تمت إضافتها إلى قائمة التشغيل
لا يمكن حذف قائمة التشغيل
- ملئ
+ ملئ الشاشة
تكبير
حجم خط التسمية
@@ -359,7 +357,7 @@
مُزامَنة
- تنزيل الملف البث.
+ تنزيل ملف البث.
الإشارات مرجعية
استعمال التقديم السريع الغير دقيق
@@ -370,14 +368,14 @@
مجلد غير صالح
لا يمكن ترك إسم الملف فارغا
- طرأ هناك خطأ : %1$s
- ملف مضغوط غير صالح
- إزالة الإشارات المرجعية
+ طرأ هناك خطأ: %1$s
+ ملف مضغوط ZIP غير صالح
+ إزالة الفواصل المرجعية
تناسب مع الشاشة
توليد تلقائي
- إستيراد و تصدير
+ إستيراد وتصدير
إستيراد
إستعادة مِن
تصدير إلى
@@ -389,51 +387,51 @@
"معرفك , soundcloud.com/ الخاص بك "
إفتراضي
-تعطيل إيقاف جميع الصور المصغرة من تحميل البيانات واستخدام الذاكرة وحفظها. سيؤدي التغيير هذا إلى محو-ذاكرة التخزين المؤقت في الذاكرة-وذاكرة على القرص.
+تعطيل إيقاف جميع الصور المصغرة من تحميل البيانات واستخدام الذاكرة وحفظها. سيؤدي التغيير هذا إلى محو-ذاكرة التخزين المؤقت في الذاكرة-وذاكرة على القرص
امسح البيانات الوصفية المخزنة مؤقتًا
إزالة جميع بيانات صفحات الويب المخزنة مؤقتًا
تم محو ذاكرة التخزين المؤقت للبيانات الوصفية
وضع البث القادم تلقائيا في قائمة الإنتظار
- رفض البث المشابه في حال كون البث السابق يعمل في حالة عدم التكرار.
- إضافة إلى قائمة التشغيل كصورة مصغرة
+ رفض البث المشابه في حال كون البث السابق يعمل في حالة عدم التكرار
+ إضافة صورة مصغرة إلى قائمة التشغيل
- قائمة التشغيل المخزنة
+ تفضيل قائمة التشغيل
تم تغيير الصورة المصغرة لقائمة التشغيل
بدون تسميات توضيحية
تسميات توضيحية
- تعديل مشغل نص التسمية التوضيحية وأنماط الخلفية. يتطلب إعادة تشغيل التطبيق ليصبح ساري المفعول.
+ تعديل مشغل نص التسمية التوضيحية وأنماط الخلفية. يتطلب إعادة تشغيل التطبيق لتصبح التغييرات سارية المفعول
تمكين LeakCanary
- قد يتسبب مراقبة في تسرب الذاكرة في عدم استجابة التطبيق عند تفريغ السجلات
+ قد تتسبب مراقبة تسرب الذاكرة في عدم استجابة التطبيق عند تفريغ السجلات
تقرير الأخطاء خارج دورة الحياة
فرض الإبلاغ عن استثناءات Rx غير القابلة للتسليم خارج دورة حياة الجزء أو النشاط بعد التخلص منها
محو سجل المشاهدة
- احذف محفوظات التدفقات التي تم تشغيلها.
+ احذف سِجل الفيديوهات التي تم تشغيلها
حذف سجل المشاهدة بالكامل.
- سجل المشاهدة المحذوف.
+ تم حذف سجل المشاهدة.
محو سجل البحث
- يحذف تاريخ البحث عن الكلمات الرئيسية.
- حذف محفوظات البحث بالكامل.
- سجل البحث المحذوف.
+ يحذف تاريخ البحث عن الكلمات الرئيسية
+ حذف سِجل البحث بالكامل.
+ تم حذف سجل البحث.
المشغل الخارجي لا يدعم هذه الأنواع من الروابط
- مصدر ملف / مصدر غير صالح
- الملف غير موجود أو ليس لديه الإذن الكافي للقراءة أو الكتابة إليه
+ ملف غير صالح / محتوى غير صالح
+ الملف غير موجود أو ليس لديك الإذن الكافي للقراءة أو الكتابة فيه
لا توجد تدفقات متاحة للتنزيل
تم حذف عنصر واحد.
لم يتم تثبيت أي تطبيق لتشغيل هذا الملف
- NewPipe هو برنامج مفتوح المصدر حقوق متروكة: يمكنك استخدام الكود ودراسته وتحسينه بإرادتك. و على وجه التحديد يمكنك إعادة توزيعه / أو تعديله تحت شروط رخصة GNU العمومية والتي نشرتها مؤسسة البرمجيات الحرة، سواء الإصدار 3 من الرخصة، أو (باختيارك) أي إصدار لاحق.
+ NewPipe هو برنامج مفتوح المصدر حقوق متروكة: يمكنك استخدام الكود ودراسته وتحسينه كما شئت. و على وجه التحديد يمكنك إعادة توزيعه / أو تعديله تحت شروط رخصة GNU العمومية والتي نشرتها مؤسسة البرمجيات الحرة، سواء الإصدار 3 من الرخصة، أو (باختيارك) أي إصدار جديد.
هل تريد حذف هذا العنصر من سجل المشاهدة؟
هل أنت متأكد من أنك تريد حذف كل العناصر من السجل؟
آخر ما تم تشغيله
الأكثر تشغيلا
- هذا سوف يتجاوز الإعداد الحالي الخاص بك.
+ هذا سوف يُزيل إعداداتك الحالية.
طريقة \'التشغيل\' المفضلة
"الإجراء الافتراضي عند فتح المحتوى — %s"
@@ -445,32 +443,32 @@
تعذر استيراد الاشتراكات
لا يمكن تصدير الاشتراكات
- استيراد اشتراكات YouTube عن طريق تنزيل ملف التصدير:
-\n
-\n1. انتقل إلى عنوان URL هذا: %1$s
-\n2. تسجيل الدخول عندما يتطلب
-\n3. يجب أن يبدأ تنزيل (وهذا ملف التصدير)
- قم باستيراد ملف تعريف SoundCloud عن طريق كتابة عنوان URL أو معرفك:
-\n
-\n1. تمكين \"وضع سطح المكتب\" في متصفح الويب (الموقع غير متاح للأجهزة المحمولة)
-\n2. انتقل إلى عنوان URL هذا: %1$s
-\n3. تسجيل الدخول عندما يتطلب
+ استيراد اشتراكات YouTube عن طريق تنزيل ملف التصدير:
+\n
+\n1. انتقل إلى عنوان URL هذا: %1$s
+\n2. تسجيل الدخول عندما يطلب منك
+\n3. يجب أن يبدأ التنزيل (وهذا ملف التصدير)
+ قم باستيراد ملف تعريف SoundCloud عن طريق كتابة عنوان URL أو معرفك:
+\n
+\n1. تمكين \"وضع سطح المكتب\" في متصفح الويب (الموقع غير متاح للأجهزة المحمولة)
+\n2. انتقل إلى عنوان URL هذا: %1$s
+\n3. تسجيل الدخول عندما يطلب منك
\n4. انسخ عنوان URL للملف الشخصي الذي تمت إعادة توجيهك إليه.
- ضع في اعتبارك أن هذه العملية يمكن أن تكون باهظة الثمن.
-\n
+ ضع في اعتبارك أن هذه العملية يمكن أن تكون مكلفة اذا كنت تستخدم بيانات اشتراك انترنت.
+\n
\nهل تريد الاستمرار؟
ضوابط سرعة التشغيل
سرعة الأداء
تردد الصوت
- "النزع (قد يسبب تشويه)"
+ نزع الإرتباط (قد يسبب تشويه)
تعديل الايقاع Nightcore
هل تريد أيضا استيراد الإعدادات؟
"سياسة الخصوصية في NewPipe "
يأخذ مشروع NewPipe خصوصيتك على محمل الجد. لذلك ، لا يجمع التطبيق أي بيانات دون موافقتك.
\nتوضح سياسة خصوصية NewPipe بالتفصيل البيانات التي يتم إرسالها وتخزينها عند إرسال تقرير الأعطال.
- قراءة سياسة الخصوصية
+ الإطلاع على سياسة الخصوصية
من أجل الامتثال للائحة الأوروبية العامة لحماية البيانات (GDPR) ، فإننا نلفت انتباهك إلى سياسة خصوصية NewPipe. يرجى قراءتها بعناية.
\nو يجب عليك قبولها لإرسال تقرير الأخطاء إلينا.
قبول
@@ -478,4 +476,18 @@
لا حدود
الحد من جودة الفيديو عند استخدام بيانات الهاتف المحمول
+ تسريع إلى الأمام أثناء الصمت
+ خطوة
+ إعادة تعيين
+
+ تصغير عند تبديل التطبيق
+ الإجراء عند التبديل إلى تطبيق آخر من مشغل الفيديو الرئيسي — %s
+ لاشيء
+ تصغير إلى مشغل الخلفية
+ تصغير إلى مشغل منبثق
+
+القنوات
+ قوائم التشغيل
+ المسارات
+ المستخدمين
diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml
index 61a57ec26..d39e46d5a 100644
--- a/app/src/main/res/values-az/strings.xml
+++ b/app/src/main/res/values-az/strings.xml
@@ -48,7 +48,6 @@
Audio
Defolt audio formatı
Defolt video formatı
- M4A — daha yaxşı keyfiyyət
Mövzu
Açıq
Qaranlıq
diff --git a/app/src/main/res/values-b+ast/strings.xml b/app/src/main/res/values-b+ast/strings.xml
index ff0d96016..a8ee6260c 100644
--- a/app/src/main/res/values-b+ast/strings.xml
+++ b/app/src/main/res/values-b+ast/strings.xml
@@ -33,8 +33,6 @@
Amuesa una opción pa reproducir un videu per Kodi
Audiu
Formatu por defeutu d\'audiu
- WebM — formatu llibre
- M4A — calidá meyor
Tema
Escuru
Claru
diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml
new file mode 100644
index 000000000..8a1d35df3
--- /dev/null
+++ b/app/src/main/res/values-be/strings.xml
@@ -0,0 +1,466 @@
+
+Націсніце пошук, каб пачаць
+ %1$s праглядаў
+ Апублікавана %1$s
+ Патокавы плэер не знойдзены. Усталяваць VLC?
+ Патокавы плэер не знойдзены (вы можаце ўсталяваць VLC)
+ Усталяваць
+ Скасаваць
+ Адкрыць у браўзэры
+ Адкрыць у асобным акне
+ Падзяліцца
+ Спампаваць
+ Загрузка файла прамой трансляцыі.
+ Пошук
+ Налады
+ Магчыма, вы мелі на ўвазе: %1$s?
+ Падзяліцца з дапамогай
+ Выбраць браўзэр
+ паварот
+ Знешні відэаплэер
+ У некаторых разрозненнях НЕ будзе гуку, калі гэтая опцыя абраная
+ Знешні аўдыяплэер
+ NewPipe ў акне
+ Падпісацца
+ Вы падпісаныя
+ Падпіска адменена
+ Не атрымалася змяніць падпіску
+ Не атрымалася абнавіць падпіску
+ Паказаць звесткі
+
+ Галоўная
+ Падпіскі
+ Закладкі
+
+ Што новага
+
+ У фоне
+ У акне
+ Дадаць да
+
+ Шлях загрузкі відэа
+ Папка для захоўвання загружаных відэа
+ Увядзіце шлях да папкі для загрузкі відэа
+
+ Шлях загрузкі аўдыё
+ Папка для захоўвання загружаных аўдыё
+ Увядзіце шлях да папкі для загрузкі аўдыё
+
+ Аўтапрайграванне
+ Прайграваць відэа пры выкліку NewPipe з іншага прыкладання
+ Разрознянне па змаўчанні
+ Разрозненне усплываючага акна
+ Высокія разрозненні
+ Толькі некаторыя прылады могуць прайграваць відэа ў 2K/4K
+ Прайграць у Kodi
+ Дадатак Kore не знойдзены. Усталяваць яго?
+ \"Прайграць у Kodi\"
+ Паказаць опцыю прайгравання відэа праз медыяцэнтр Kodi
+ Аўдыё
+ Фармат аўдыё па змаўчанні
+ Фармат відэа па змаўчанні
+ WebM - свабодны
+ M4A - вышэй якасць
+ Тэма
+ Светлая
+ Цёмная
+ Чорная
+ Аднавіць акно
+ Запамінаць памер і становішча ўсплываючага акна
+ Хуткі пошук пазіцыі
+ Недакладны пошук дазваляе плэеру шукаць пазіцыю хутчэй, але менш дакладна
+ Загружаць мініяцюры
+ Адключыце, каб не загружаць мініяцюры і зэканоміць трафік і памяць. Змена налады ачысьціць кэш малюнкаў у памяці і носьбіце
+ Кэш малюнкаў ачышчаны
+ Ачысціць кэш метададзеных
+ Выдаліць усе загружаныя дадзеныя вэб-старонак
+ Кэш метададзеных ачышчаны
+ Аўтадапаўненне чаргі
+ Дадаваць падобныя патокі ў чаргу пры прайграванні апошняга, калі не ўключаны паўтор
+ Кіраванне жэстамі
+ Змяняць яркасць і гучнасць жэстамі
+ Варыянты пошуку
+ Адлюстроўваць падказкі пры пошуку
+ Гісторыя пошуку
+ Захоўваць пошукавыя запыты лакальна
+ Гісторыя і кэш
+ Запамінаць прагледжаныя відэа
+ Аднавіць пры фокусе
+ Аднаўляць прайграванне пасля перапынкаў (напрыклад, тэлефонных званкоў)
+ Спампаваць
+ Наступнае відэа
+ \"Наступнае\" и \"Прапанаваныя\"
+ \"Зацісніце, каб дадаць\"
+ Паказаць падказку пры націсканні \"У акне\" ці \"У фоне\" на старонцы звестак аб відэа
+ URL не падтрымліваецца
+ Краіна кантэнту па змаўчанні
+ Сэрвіс
+ Мова кантэнту па змаўчанні
+ Плэер
+ Павядзенне
+ Відэа і аўдыё
+ Гісторыя і кэш
+ Усплываючае акно
+ Знешні выгляд
+ Іншае
+ Адладка
+ Прайграванне ў фонавым рэжыме
+ Прайграванне ва ўсплываючым акне
+ Дададзена ў чаргу ў фоне
+ Дададзена ў чаргу ў акне
+ Прайграць
+ Кантэнт
+ Кантэнт 18+
+ Відэа з узроставымі абмежаваннямі. Дазволіць падобны кантэнт можна ў наладах.
+ ужывую
+ Загрузкі
+ Загрузкі
+ Справаздача пра памылку
+ Усё
+ Канал
+ Каналы
+ Плэйліст
+ Плэйлісты
+ Дарожкі
+ Карыстальнікі
+ Так
+ Пазней
+ Адключана
+ Фільтр
+ Абнавіць
+ Ачысціць
+ Змена памеру
+ Лепшае разрозненне
+ Скасаваць
+ Прайграць усё
+ Заўсёды
+ Толькі цяпер
+ Файл
+
+ Апавяшчэнне NewPipe
+ Апавяшчэнні для NewPipe ў фоне і ва ўсплываючым акне
+
+ [Невядома]
+
+ Пераключыць арыентацыю
+ Перайсці ў фон
+ Перайсці ў акно
+ Перайсці ў галоўнае акно
+
+ Імпарт дадзеных
+ Экспарт дадзеных
+ Ваша бягучая гісторыя і падпіскі перазапішуцца
+ Экспарт гісторыі, падпісак і плэйлістоў
+ Ачысціць гісторыю праглядаў
+ Выдаліць гісторыю прайграных патокаў
+ Выдаліць усю гісторыю праглядаў.
+ Гісторыя праглядаў выдалена.
+ Ачысціць гісторыю пошуку
+ Выдаліць гісторыю пошукавых запытаў
+ Выдаліць усю гісторыю пошуку.
+ Гісторыя пошуку выдалена.
+ Памылка
+ Памылка сеткі
+ Не атрымалася загрузіць усе мініяцюры
+ Не атрымалася расшыфраваць подпіс URL у відэа
+ Не атрымалася разабраць вэб-сайт
+ Не атрымалася цалкам разабраць вэб-сайт
+ Кантэнт недаступны
+ Заблакавана GEMA
+ Не атрымалася стварыць меню загрузкі
+ Гэта прамая трансляцыя, яны пакуль не падтрымліваюцца.
+ Не атрымалася знайсці ні аднаго патока
+ Не атрымалася загрузіць малюнак
+ Падзенне прыкладання/UI
+ Не атрымалася прайграць гэты паток
+ Памылка плэера без магчымасці аднаўлення
+ Аднаўленне пасля памылкі плэера
+ Знешнія плэеры не падтрымліваюць спасылкі гэтых тыпаў
+ Няправільная спасылка
+ Патокі відэа не знойдзены
+ Патокі аўдыё не знойдзены
+ Няправільная папка
+ Няправільны файл або крыніца кантэнту
+ Файл не існуе або няма дазволу на яго чытанне або запіс
+ Імя файла не можа быць пустым
+ Адбылася памылка: %1$s
+ Няма патокаў, даступных для загрузкі
+
+ Прабачце, гэта не павінна было адбыцца.
+ Адправіць справаздачу па e-mail
+ Прабачце, адбыліся памылкі.
+ СПРАВАЗДАЧА
+ Інфармацыя:
+ Што адбылося:
+ Што:\\nЗапыт:\\nМова кантэнту:\\nСэрвіс:\\nЧас па Грынвічы:\\nПакет:\\nВерсія:\\nВерсія АС:
+ Ваш каментар (English):
+ Падрабязнасці:
+
+
+ Мініяцюра відэа-прэв\'ю
+ Мініяцюра відэа-прэв\'ю
+ Мініяцюра аватара карыстальніка
+ Спадабалася
+ Не спадабалася
+ Выкарыстоўваць Tor
+ (Эксперыментальна) Загружаць праз Tor для павышэння прыватнасці (прамыя трансляцыі пакуль не падтрымліваюцца).
+ Паведаміць пра памылку
+ Паведаміць аб парушэнні
+ Няма вынікаў
+ Нічога няма
+ Перацягніце, каб змяніць парадак
+
+ Не атрымалася стварыць папку для загрузкі \"%1$s\"
+ Створана папка для загрузак \"%1$s\"
+
+ Відэа
+ Аўдыё
+ Паспрабаваць зноў
+ Няма доступу да носьбіта
+ Стары плэер
+ Стары убудаваны плэер на Mediaframework
+
+ тыс.
+ млн.
+ млрд.
+
+ Няма падпісчыкаў
+
+ - %s падпісчык
+ - %s падпісчыка
+ - %s падпісчыкаў
+
+
+ Няма праглядаў
+
+ - %s прагляд
+ - %s прагляда
+ - %s праглядаў
+
+
+ Няма відэа
+
+ - %s відэа
+ - %s відэа
+ - %s відэа
+
+
+ Пачаць
+ Паўза
+ Прайграць
+ Стварыць
+ Выдаліць
+ Выдаліць адно
+ Выдаліць усё
+ Кантрольная сума
+ Адхіліць
+ Перайменаваць
+
+ Новая мэта
+ ОК
+
+ Імя файла
+ Патокі
+ Памылка
+ Сервер не падтрымліваецца
+ Файл ужо існуе
+ Няправільны URL або няма доступу да інтэрнэту
+ NewPipe спампоўвае
+ Падрабязнасці
+ Пачакайце…
+ Скапіявана ў буфер абмену
+ Выберыце даступную папку для загрузкі
+ Гэтае разрозненне трэба для
+\nпрайгравання ў акне
+ 1 элемент выдалены.
+
+ reCAPTCHA
+ Запыт reCAPTCHA
+ Запытаны ўвод reCAPTCHA
+
+ Загрузкі
+ Дапушчальныя сімвалы назвы файлаў
+ Недапушчальныя сімвалы замяняюцца на гэтыя
+ Сімвал для замены
+
+ Літары і лічбы
+ Большасць спецзнакаў
+
+ Прыкладанне для прайгравання гэтага файла не ўстаноўлена
+
+ Аб NewPipe
+ Налады
+ Аб дадатку
+ Іншыя ліцэнзіі
+ © %1$s %2$s пад ліцэнзіяй %3$s
+ Не атрымалася загрузіць ліцэнзію
+ Адкрыць вэб-сайт
+ Аб дадатку
+ Удзельнікі
+ Ліцэнзіі
+ Свабоднае легкавагавае патокавае прайграванне на Android.
+ Дапамога праекту
+ Вітаецца ўсё - ідэі, пераклад, змены дызайну, чыстка кода або велізарныя змены ў кодзе. Чым больш зроблена, тым лепш!
+ Адкрыць на GitHub
+ Ахвяраваць
+ Распрацоўшчыкі NewPipe цаной свайго вольнага часу робяць ваша жыццё крышачку зручней. Адплаціце ім тым жа - атрымліваючы асалоду ад кубачка кавы, яны змогуць зрабіць NewPipe яшчэ лепей.
+ Аддаць належнае
+ Вэб-сайт
+ Дзеля атрымання больш падрабязнай інфармацыі і апошніх навін аб NewPipe наведайце наш вэб-сайт.
+ Палітыка прыватнасці NewPipe
+ Праект NewPipe вельмі сур\'ёзна ставіцца да вашай прыватнасці. Таму прыкладанне не збірае ніякіх дадзеных без вашай згоды.
+\nПалітыка прыватнасці NewPipe падрабязна тлумачыць, якія дадзеныя адпраўляюцца і захоўваюцца пры адпраўцы справаздачы аб збоях.
+ Прачытаць палітыку
+ Ліцэнзія NewPipe
+ NewPipe - свабоднае праграмнае забеспячэнне: вы можаце выкарыстоўваць, вывучаць і паляпшаць яго па сваім меркаванні. У прыватнасці, вы можаце распаўсюджваць і / або змяняць яго ў адпаведнасці з умовамі GNU General Public License, апублікаванай Free Software Foundation, альбо версіі 3, альбо (па вашаму выбару) любой больш позняй версіі.
+ Прачытаць ліцэнзію
+
+
+ Гісторыя
+ Гісторыя пошуку
+ Прагледжана
+ Гісторыя адключаная
+ Гісторыя
+ Гісторыя пустая
+ Гісторыя ачышчана
+ Элемент выдалены
+ Выдаліць гэты элемент з гісторыі пошуку?
+ Выдаліць гэты элемент з гісторыі пошуку?
+ Выдаліць усе элементы з гісторыі?
+ Нядаўна прайграныя
+ Часта прайграваемыя
+
+ Кантэнт галоўнай старонкі
+ Пустая старонка
+ Старонка кіёска
+ Старонка падпіскі
+ Старонка стужкі
+ Старонка канала
+ Выберыце канал
+ Пакуль няма падпісак на каналы
+ Выберыце кіёск
+ Экспарт завершаны
+ Імпарт завершаны
+ Няма правільнага Zip-файла
+ Увага: не ўсе файлы былі імпартаваныя.
+ Бягучыя дадзеныя будуць замененыя.
+ Хочаце імпартаваць налады?
+
+ Кіёск
+ Трэнды
+ Топ 50
+ Новае і гарачае
+ У фоне
+ У акне
+ Выдаліць
+ Падрабязнасці
+ Налады аўдыё
+ Зацісніце, каб дадаць у чаргу
+ У чаргу \"У фоне\"
+ У чаргу \"У фоне\"
+ Відэаплэер
+ Фонавы плэер
+ Плэер у акне
+
+ Адкрыць бакавую панэль
+ Зачыніць бакавую панэль
+ Хутка тут сёе-тое з\'явіцца ;D
+
+
+ Пры адкрыцці кантэнту
+ Пры адкрыцці спасылкі на кантэнт — %s
+
+ Відэаплэер
+ Фонавы плэер
+ Плэер у акне
+ Заўсёды пытацца
+
+ Атрыманне звестак…
+ Загрузка запытанага кантэнту
+
+ Стварыць плэйліст
+ Выдаліць плэйліст
+ Перайменаваць плэйліст
+ Імя
+ Дадаць у плэйліст
+ На мініяцюру плэйліста
+
+ Дадаць плэйліст у закладкі
+ Выдаліць закладку
+
+ Выдаліць гэты плэйліст?
+ Плэйліст створаны
+ Дададзена ў плэйліст
+ Мініяцюра плэйліста зменена
+ Не атрымалася выдаліць плэйліст
+
+ Без тытраў
+
+ Падагнаць
+ Запоўніць
+ Наблізіць
+
+ Створаны аўтаматычна
+
+ Тытры
+ Змяніць памер і фон тытраў. Змены ўступяць у сілу пасля перазапуску
+
+ Уключыць LeakCanary
+ Маніторынг уцечкі памяці можа прывесці да завісання прыкладання
+
+ Паведамляць пра памылкі жыццёвага цыклу
+ Прымусова паведамляць пра недастаўляемыя Rx-выключэнні па-за фрагментам або жыццёвым цыкле пасля выдалення
+
+ Імпарт/Экспарт
+ Імпарт
+ Імпарт з
+ Экспарт у
+
+ Імпарт…
+ Экспарт…
+
+ Імпарт файла
+ Папярэдні экспарт
+
+ Не атрымалася імпартаваць падпіскі
+ Не атрымалася экспартаваць падпіскі
+
+ Імпарт падпісак з YouTube загрузкай файла экспарту:
+\n
+\n1. Перайдзіце на: %1$s
+\n2. Увайдзіце, калі неабходна
+\n3. Павінна пачацца загрузка (гэта будзе файл экспарту)
+ Імпарт падпісак з SoundCloud набраўшы альбо URL, альбо ваш ID:
+\n
+\n1. Уключыце \"рэжым працоўнага стала\" у браўзэры (сайт недаступны на тэлефоне)
+\n2. Перайдзіце на: %1$s
+\n3. Увайдзіце, калі неабходна
+\n4. Скапіруйце адрас з адраснага радка.
+ вашID, soundcloud.com/вашID
+
+ Гэтае дзеянне можа выклікаць вялікі расход трафіку.
+\n
+\nПрацягнуць?
+
+ Кіраванне хуткасцю прайгравання
+ Тэмп
+ Тон
+ Незалежна (скажэнні)
+ Прапускаць цішыню
+ Крок
+ Скід
+
+ У адпаведнасці з Агульным рэгламентам па абароне дадзеных ЕС (GDPR), звяртаем вашу ўвагу на палітыку прыватнасці NewPipe. Калі ласка, уважліва азнаёмцеся з ёй.
+\nВам неабходна прыняць яе ўмовы, каб адправіць нам справаздачу пра памылку.
+ Прыняць
+ Адмовіцца
+
+ Без абмежаванняў
+ Ліміт разрознення ў мабільнай сетцы
+ Пры згортванні плэера
+ Пры пераключэнні са стандартнага плэера на іншае прыкладанне — %s
+ Нічога не рабіць
+ Фонавы плэер
+ Плэер у акне
+
+
diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml
index 8ab7ee9dc..5adc10a1d 100644
--- a/app/src/main/res/values-bg/strings.xml
+++ b/app/src/main/res/values-bg/strings.xml
@@ -1,14 +1,14 @@
-Натисни на търсене за да започнеш
+Докоснете търсачката, за да започнете
%1$s гледания
Публикувано на %1$s
Не е намерен стрийм плейър. Желаете ли да инсталирате VLC?
Инсталирай
- Затвори
- Отвори в браузъра
- Отвори в подпрозорец
+ Отмени
+ Отвори в браузър
+ Отвори в прозорец
Сподели
- Сваляне
+ Изтегли
Търси
Настройки
Може би имахте в предвид: %1$s ?
@@ -16,81 +16,79 @@
Избери браузър
ориентация
Използвай външен видео плейър
- Някои резолюции няма да имат вграден аудио поток ако изберете тази опция
+ Някои резолюции няма да имат вграден аудио поток, ако тази опция е избрана
Използвай външен аудио плейър
- Режим с подпрозорец на NewPipe
+ NewPipe в прозорец
Абониране
Абониран
Премахнат абонамент за канала
- Не мога да променя абонаментите
- Не мога да обновя абонаментите
+ Неуспешна промяна на абонамента
+ Неуспешно обновление на абонамента
Абонаменти
- Новости
+ Обновления
- Фон
- Подпрозорец
+ Във фонов режим
+ В прозорец
- Локация за запис на клиповете
- Път за съхранение на свалени клипове
- Въведи път за съхранение на свалени клипове
+ Директория за изтегляне на видео
+ Папка за съхранение на изтеглените видеота
+ Въведете път за съхранение на изтеглените видеота
- Локация за запис на аудио
- Път за съхранение на свалено аудио
- Въведи път за съхранение на свалено аудио
+ Директория за изтегляне на аудио
+ Папка за съхранение на изтеглено аудио
+ Въведете път за съхранение на изтеглено аудио
Автоматично възпроизвеждане
- Автоматично въпроизвеждане на видео, когато NewPipe е повикан от друго приложение
+ Въпроизвежда видео, когато NewPipe е повикан от друго приложение
Резолюция по подразбиране
- Резолюция по подразбиране на подпрозореца
+ Резолюция по подразбиране на прозореца
Покажи по-високи резолюции
Само някои устройства поддържат възпроизвеждане на 2K/4K клипове
Въпроизвеждане с Kodi
- Приложението Kore не е намерено. Желаете ли да го инсталирате?
- Покажи опция \"Въпроизвеждане с Kodi\"
- Показване на опция за въпроизвеждане на видео чрез Kodi media center
+ Приложението „Kore“ не е намерено. Желаете ли да го инсталирате?
+ Покажи „Възпроизвеждане с Kodi“
+ Показване на опция за възпроизвеждане на видео чрез „Kodi media center“
Аудио
Аудио формат по подразбиране
Видео формат по подразбиране
- WebM — свободен формат
- M4A — по-добро качество
- Тема
+ Тема на външния вид
Светла
Тъмна
Черна
- Помни размера и позицията на подпрозореца
- Запомни последния размер и позиция на подпрозореца
+ Помни размера и позицията на прозореца
+ Използвай размера и позицията на прозореца от предишния път
Контролиране на плейъра чрез жестове
- Използвай жестове за да контролираш яркостта и силата на звука на плейъра
+ Позволи използване на жестове за контрол на яркостта и силата на звука на плейъра
Предложения за търсене
- Покажи предположения по време на търсене
+ Показвай предложения за търсене
История на търсенията
Съхранявай заявките за търсене локално
- История
- Следи изгледаните клипове
- Поднови клип при възобновяване на фокуса
- Продължава въпроизвеждането след прекъсване (например, телефонно обаждане)
- Свалени
+ История и кеш-памет
+ Запаметявай кои видеота са гледани
+ Възобнови при връщане на фокус
+ Продължавай възпроизвеждането след прекъсване (например телефонно обаждане)
+ Изтегли
Следващ клип
- Показвай следващия и подобни клипове
- Показвай поле със съвет \"Задръж за добавяне\"
- Неподдържан URL
+ Показвай „следващ“ и „подобни“
+ Показвай съвет „задръж за добавяне“
+ Непознат URL
Език на съдържанието по подразбиране
Плейър
Поведение
Видео & Аудио
- История
- Подпрозорец
- Вид
+ История и кеш-памет
+ Нов прозорец
+ Външност
Други
- Въпроизвеждане във фонов режим
- Въпроизвеждане в подпрозорец
+ Възпроизвеждане във фонов режим
+ Възпроизвеждане в подпрозорец
Включен в опашката на фоновия плейър
- Включен в опашката за възпроизвеждане в подпрозорец
+ Включен в опашката в нов прозорец
Възпроизвеждане
Съдържание
- Покажи съдържание с ограничение във възрастта
+ Покажи съдържание за възрастни
Съдържание за възрастни. Разрешаването на такова съдържание става от Настройки.
на живо
Изтегляния
@@ -111,17 +109,17 @@
Възпроизведи всички
Известия от NewPipe
- Известия за фоновия плейър и плейъра на подпрозореца на NewPipe
+ Известия за фоновия плейър и плейъра в отделен прозорец на NewPipe
[Неизвестен]
Грешка
Проблем с мрежата
Не мога да заредя всички миниатюри
- Не мога да интерпретирам правилно уебсайта
- Не мога да интерпретирам изцяло правилно уебсайта
+ Неуспешно пресъздаване на уебсайта
+ Не мога да пресъздам изцяло уебсайта
Съдържанието не е налично
- Блокирано от GEMA
+ Блокирано от „GEMA“
Не мога да настроя меню за сваляне
Това е предаване на живо, което все още не се поддържа.
Не мога да достъпя нито един поток
@@ -131,7 +129,7 @@
Критичен проблем с плейъра
Опит за възстановяване от възникналия проблем с плейъра
- Съжаляваме, но това не би трябвало да се случва.
+ Съжаляваме, това не би трябвало да се случва.
Докладвай за грешката чрез имейл
Съжаляваме, възникнаха някои грешки.
ДОКЛАД
@@ -141,23 +139,23 @@
Подробности:
- Миниатюра на клипа
- Миниатюра на аватаря на качилия клипа
+ Миниатюра на видео
+ Миниатюра на аватара на качилия видео
Харесвания
Нехаресвания
- Използвай Tor
- (Експериментално) Пренасочвай трафика при сваляне към мрежата на Tor за по-високо ниво на поверителност (стриймването на клипове все още не се поддържа).
+ Използвай „Tor“
+ (Експериментално) Пренасочвай трафика при сваляне към мрежата на „Tor“ за по-високо ниво на поверителност (стриймването на видео все още не се поддържа).
Докладвай за грешка
Няма резултати
- Тук все още няма нищо
+ Тук няма нищо
- Не мога да създам директория за свалянията в \'%1$s\'
- Създадох директория за свалянията в \'%1$s\'
+ Неуспешно създаване на директория за изтеглени в „%1$s“
+ Бе създадена директория за свалянията в „%1$s“
Видео
Аудио
Опитай отново
- Отказан достъп до хранилището (провери правата)
+ Достъпа до хранилището е отказан
Използвай стария плейър
Използваю стария вграден Mediaframewoek плейър
@@ -181,34 +179,286 @@
Пауза
Начало
Изтрий
- Чексума
+ Контролна сума
- Готово
+ ОК
Име на файла
Нишки
Грешка
Сървърът не се поддържа
Файлът вече съществува
- Неправилен формат на URL-а или няма връзка
- NewPipe Сваляне
- Натисни за подробности
+ Неправилен хиперлинк или няма връзка с Интернет
+ NewPipe Изтегляне
+ Докосни за подробности
Моля, изчакайте…
Копирано в клипборда
Позволени символи в името на файловете
Невалидните символи се заменят с тази стойност
- Стойност за замяна
+ Символ за замяна
Букви и цифри
За NewPipe
Настройки
Относно приложението
Лицензи от трети страни
- Не мога да заредя лицензите
+ Неуспешно зареждане на лиценза
Отвори уебсайта
Относно приложението
Допринесли
- Лицензии
- Безплатен и лек YouTube фронтенд за Android.
+ Лицензи
+ Безплатно и леко поточно предаване за Android.
Виж в GitHub
+ Изтегли стрийм файл.
+ Покажи инфо
+
+ Главен
+ Отметки
+
+ Добави към
+
+ Използвай бързо, но неточно превъртане
+ Неточното превъртане позволява на плейъра да превърта кадри по-бързо, с намалена прецизност
+ Зареждай миниатюри
+ Кеш-паметта с изображения е изтрита
+ Изтрий кешираните метаданни
+ Премахни всички метаданни за уебстраници от кеш-паметта
+ Кеш-паметта с метаданни бе изтрита
+ Автоматично нареди на опашка следващия
+ "Автоматично прибавяне на сродно съдържание при неповтарящ се преглед "
+ Държава, за която да бъде показвано съдържание
+ Услуга
+ Отстраняване на грешки
+ Винаги
+ Само веднъж
+ Файл
+
+ Смени ориентацията
+ Мини във фонов режим
+ Мини към нов прозорец
+ Мини в основен режим
+
+ Импортиране на база данни
+ Експортиране на база данни
+ Ще замени текущите история и абонаменти
+ Експортиране на историята, абонаментите и плейлистите
+ Изтрий историята с изгледани
+ Изтрий цялата история с изгледани.
+ Историята с изгледани е изтрита.
+ Изтрий историята на търсенията
+ Изтрива историята с въвежданите за търсене ключови думи
+ Изтрий цялата история на търсенията.
+ Историята на търсенията е изтрита.
+ URL подписът на клипа не можа да бъде дешифрован
+ Външните плейъри не поддържат този вид линкове
+ Невалиден URL
+ Невалидна директория
+ Невалиден файл или източник на съдържание
+ Файлът не съществува или липсва разрешение за четене и/или запис
+ Името на файла не може да бъде празно
+ Възникна грешка: %1$s
+ Не са налични източници за изтегляне
+
+ хил.
+ млн.
+ млрд.
+
+ Няма абонати
+ Създай
+ Изтрий всички
+ Откажи
+ Няма инсталирано приложение, което да изпълни този файл
+
+ © %1$s от %2$s под лиценза %3$s
+ Съдействайте
+ За всичко, което се сетите: превод, промени по дизайна, изчистване на кода или много сериозни промени по кода – помощта е винаги добре дошла. Колкото повече развитие, толкова по-добре!
+ Направете дарение
+ NewPipe се разработва от доброволци, които отделят от своето време, за да ви доставят най-доброто преживяване. Дайте от себе си в замяна, за да помогнете на разработчиците да направят NewPipe още по-добро приложение, докато се наслаждават на едно кафе от вас.
+ Дари
+ Уебсайт
+ Посетете сайта на NewPipe за повече информация и новини.
+ Политиката на NewPipe за личните данни
+ Проектът NewPipe се отнася много сериозно към вашата поверителност. За това, приложението не събира никакви данни без вашето съгласие.
+\nНашата политика за личните данни обяснява подробно какви данни изпращате и къде се съхраняват, когато изпращате съобщения за грешки.
+ Прочетете нашата политика за поверителност
+ Лицензът на NewPipe
+ Липсва стрийм плейър (можете да изтеглите VLC, за да пуснете стрийма)
+ Изключете, за да спрете зареждането на всички миниатюри, спестявайки трафик и памет. При промяна на тази настройка, текущата кеш-памет на изображенията ще бъде изтрита
+ Показвай подсказка, когато е избран фонов режим или режим в прозорец на страницата с детайли на съответния клип
+ Изтрива историята на възпроизвежданите стриймове
+ Не са намерени видео стриймове
+ Не са намерени аудио стриймове
+ "Какво:\\nЗаявка:\\nЕзик на съдържанието:\\nУслуга:\\nВреме по GMT:\\nПакет:\\nВерсия:\\nОС версия: "
+ Миниатюра на видео
+ Потребителски доклад
+ Пренареди чрез плъзгане
+
+ Начало
+ Изтрий един
+ Преименувай
+
+ Нова цел
+ Моля, изберете достъпна папка за изтегляния
+ Това разрешение се изисква за
+\nвъзпроизвеждане в отделен прозорец
+ 1 елемент е изтрит.
+
+ reCAPTCHA
+ reCAPTCHA заявка
+ Изисква се въвеждане на reCAPTCHA
+
+ Изтегляне
+ Повечето специални символи
+
+ NewPipe е безплатен „copyleft“ софтуер: Можете да го използвате, изучавате, споделяте и подобрявате по ваше усмотрение. В частност, Вие можете да препубликувате и/или модифицирате приложението според правилата на Главния обществен лиценз на ГНУ, издаден от Фондацията за свободен софтуер – версия 3 на лиценза или по-нова.
+ Прочетете лиценза
+
+
+ История
+ Търсения
+ Гледани
+ Историята е изключена
+ История
+ Историята е празна
+ Историята е изчистена
+ Елементът е изтрит
+ Искате ли да изтриете този елемент от историята на търсенията?
+ Искате ли да изтриете този елемент от историята на гледанията?
+ Окончателно ли искате да изтриете всички елементи от историята на гледанията?
+ Последно възпроизвеждани
+ Най-възпроизвеждани
+
+ Съдържание на главната страница
+ Празна страница
+ Страница-павилион
+ Страница с абонаменти
+ Страница с акценти от вашите абонаменти
+ Страница на определен канал
+ Изберете канал
+ За момента нямате абонаменти
+ Изберете павилион
+ Експортирането приключи
+ Импортирането приключи
+ Невалиден ZIP файл
+ Внимание: не всички файлове бяха импортирани успешно.
+ Това ще замени текущата Ви инсталация.
+ Желаете ли също да импортирате настройките?
+
+ Павилион
+ Набиращи популярност
+ Топ 50
+ Ново и горещо
+ Във фонов режим
+ В прозорец
+ Премахни
+ Детайли
+ Аудио настройки
+ Задръжте, за да поставите на опашката
+ На опашката във „фонов режим“
+ На опашката в „режим в прозорец“
+ Възпроизвеждане от тук
+ Възпроизвеждане от тук във фонов режим
+ Възпроизвеждане от тук в прозорец
+
+ Отвори навигационната лента
+ Затвори навигационната лента
+ Тук нещо ще се появи скоро ;D
+
+
+ Действие при повикване от друго приложение
+ Действие по подразбиране при отваряне на съдържание — %s
+
+ Видео плейър
+ Във фонов режим
+ В прозорец
+ Винаги питай
+
+ Получаване на инфо…
+ Зареждане на заявеното съдържание
+
+ Създай Нов Плейлист
+ Изтрий Плейлист
+ Преименувай Прелист
+ Име
+ Добави Към Плейлист
+ Задай като миниатюра на плейлиста
+
+ Миниатюрата на плейлиста е сменена
+ Премахни отметката
+
+ Искате ли да изтриете този плейлист?
+ Плейлистът е създаден
+ Добавено към плейлист
+ Миниатюрата на плейлиста е сменена
+ Плейлистът не можа да бъде изтрит
+
+ Без надписи
+
+ Приспособи
+ Запълни
+ Увеличи
+
+ Авто-генерирани
+
+ Надписи
+ Модифицирай мащаба на текста и фона на надписите. Изисква рестарт на приложението, за да се приложат промените
+
+ Включи LeakCanary
+ Следенето за пропускане на памет може да направи приложението нестабилно
+
+ Докладвай за извънредни грешки
+ Импортиране/експортиране
+ Импортирай
+ Импортирай от
+ Експортирай в
+
+ Импортиране…
+ Експортиране…
+
+ Файл с данни за импортиране
+ Предишно експортиране
+
+ Неуспешно импортиране на абонатите
+ Неуспешно експортиране на абонатите
+
+ Импортирайте YouTube абонаментите, чрез изтегляне на нужния файл:
+\n
+\n1. Посетете тази връзка: %1$s
+\n2. Влезте в акаунта си, когато това се изиска
+\n3. Изтеглянето трябва да започне (това е експортирания файл)
+ Импортирайте SoundCloud профил чрез въвеждане на хипервръзката към него или чрез вашия ID:
+\n
+\n1. Включете „десктоп режим“ в браузър (сайтът е недостъпен за мобилни устройства)
+\n2. Посетете връзката: %1$s
+\n3. Влезте в профила си, ако се изисква
+\n4. Копирайте хипервръзката на профилната страница, към която сте насочени.
+ вашиятID, soundcloud.com/вашиятID
+
+ Това действие може да изразходва голямо количество данни от вашия трафик.
+\n
+\nЖелаете ли да продължите?
+
+ Управление скоростта на възпроизвеждане
+ Темпо
+ Височина
+ Бързо превъртане при тишина
+ От съображения към Общия европейски регламент относно защитата на данните, Ви привличаме вниманието към политиката за поверителност на NewPipe. Моля, прочетете я внимателно.
+\nТрябва да сте съгласни с условията, за да ни изпратите доклада за грешката.
+ Приеми
+ Откажи
+
+ Без ограничения
+ Ограничена резолюция при мобилни данни
+ Минимизирай при преход към друго приложение
+ Действие при преминаване към друго приложение от видео плейъра — %s
+ Без минимизиране
+ Минимизирай във фонов режим
+ Минимизирай в прозорец
+
+Канали
+ Плейлисти
+ Песни
+ Потребители
+ Възстанови
+
diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml
index 859e16aa9..f9ec7a07a 100644
--- a/app/src/main/res/values-bn-rBD/strings.xml
+++ b/app/src/main/res/values-bn-rBD/strings.xml
@@ -46,8 +46,6 @@
অডিও
ডিফল্ট অডিও ফরম্যাট
পছন্দসই ভিডিও ফরম্যাট
- WebM — বিনামূল্য/স্বাধীন ফরম্যাট
- m4a — ভালো মানের
থিম
উজ্জ্বল
অন্ধকার
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index b8b7dd699..df5ea0d87 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -23,8 +23,6 @@
Àudio
Format d\'àudio per defecte
Format de vídeo per defecte
- WebM — format lliure
- M4A — millor qualitat
Tema
Clar
Fosc
@@ -61,7 +59,7 @@
Importa una base de dades
Exporta una base de dades
- Exporta l\'historial, les subscripcions i les llistes de reproducció.
+ Exporta l\'historial, les subscripcions i les llistes de reproducció
Error
Error de xarxa
Vídeo
@@ -226,7 +224,7 @@
M\'agrada
No m\'agrada
Fes servir el Tor
- (En proves) Força el trànsit de baixada a través del Tot per a més privadesa (no compatible encara amb les emissions de vídeo).
+ (En proves) Força el trànsit de baixada a través del Tor per a més privadesa (no compatible encara amb les emissions de vídeo).
Informa sobre un error
Informe de l\'usuari
Cap resultat
@@ -310,7 +308,7 @@
Toqueu el botó de cerca per començar
En algunes resolucions NO hi haurà àudio quan aquesta opció estigui activada
Reproductor d\'àudio extern
- Desactiveu-ho per evitar que es carreguin les miniatures i estalviar dades i memòria. Si canvieu aquesta opció, s\'esborrarà la memòria cau d\'imatges tant de la memòria com de l\'emmagatzematge.
+ Desactiveu-ho per evitar que es carreguin les miniatures i estalviar dades i memòria. Si canvieu aquesta opció, s\'esborrarà la memòria cau d\'imatges tant de la memòria com de l\'emmagatzematge
Emmagatzema les cerques localment
Registra els vídeos visualitzats
Reprèn automàticament
@@ -390,7 +388,7 @@
Desvincula (pot provocar distorsió)
Nightcore
Elimina totes les dades de llocs web de la memòria cau
- Afegeix a la cua un vídeo relacionat quan es reprodueix l\'últim vídeo en una cua sense repetició.
+ Afegeix a la cua un vídeo relacionat quan es reprodueix l\'últim vídeo en una cua sense repetició
Mostra els missatges d\'ajuda
Mostra un missatge d\'ajuda quan el botó de mode en segon pla o emergent estigui premut a la pàgina de detalls d\'un vídeo
Què ha passat:\\nPetició:\\nIdioma del contingut:\\nServei:\\nHora GMT:\\nPaquet:\\nVersió:\\nVersió del SO:
@@ -425,16 +423,16 @@
No hi ha vídeos que es puguin baixar
Subtítols
- Modifica la mida del text i el fons dels subtítols. Cal reiniciar l\'aplicació per aplicar els canvis.
+ Modifica la mida del text i el fons dels subtítols. Cal reiniciar l\'aplicació per aplicar els canvis
No s\'ha trobat cap aplicació que pugui reproduir aquest fitxer
Esborra l\'historial de reproduccions
- Esborra l\'historial dels vídeos que s\'han reproduït.
+ Esborra l\'historial dels vídeos que s\'han reproduït
Esborra tot l\'historial de reproduccions.
S\'ha esborrat l\'historial de reproduccions.
Esborra l\'historial de cerca
- Esborra l\'historial de paraules cercades.
+ Esborra l\'historial de paraules cercades
Esborra tot l\'historial de cerca.
S\'ha esborrat l\'historial de cerca.
S\'ha esborrat 1 element.
@@ -452,4 +450,18 @@
Rebutja
Sense restriccions
Restringeix la resolució quan es facin servir dades mòbils
+ Minimitza en canviar d\'aplicació
+ Acció en canviar a una altra aplicació des del reproductor de vídeo principal — %s
+ Cap
+ Minimitza al reproductor en segon pla
+ Minimitza al reproductor emergent
+
+Avança ràpid durant el silenci
+ Pas
+ Reinicialitza
+
+ Canals
+ Llistes de reproducció
+ Pistes
+ Usuaris
diff --git a/app/src/main/res/values-cmn/strings.xml b/app/src/main/res/values-cmn/strings.xml
index a6b3daec9..ca495f134 100644
--- a/app/src/main/res/values-cmn/strings.xml
+++ b/app/src/main/res/values-cmn/strings.xml
@@ -1,2 +1,123 @@
-
-
\ No newline at end of file
+
+点按搜索即可开始
+ %1$s 意见
+ 发布 %1$s
+ 找不到流播放器。你想安装VLC吗?
+ 找不到流播放器(您可以安装VLC播放它)
+ 安装
+ 取消
+ 在浏览器中打开
+ 在弹出模式下打开
+ 分享
+ 下载
+ 下载流文件.
+ 搜索
+ 设置
+ 你的意思是: %1$s ?
+ 与某人分享
+ 选择浏览器
+ 回转
+ 使用外部视频播放器
+ 启用此选项时,某些分辨率将不会有音频
+ 使用外部音频播放器
+ NewPipe弹出模式
+ 订阅
+ 订阅
+ 取消订阅成功
+ 无法更改订阅
+ 无法更新订阅
+ 显示详情
+
+ 主页
+ 订阅
+ 书签
+
+ 新功能
+
+ 后台
+ 弹出窗口
+ 添加到
+
+ 视频下载路径
+ 储存视频文件的路径
+ 输入视频的下载地址
+
+ 音频下载路径
+ 储存音频的路径
+ 输入音频的下载路径
+
+ 自动播放
+ NewPipes被其它程序调用时播放视频
+ 默认分辨率
+ 默认弹出窗口分辨率
+ 显示更高的分辨率
+ 只有部分设备支持播放 2K/4K 视频
+ 用 Kodi 播放
+ 没找到 Kore 应用,需要安装它吗?
+ 显示“用 Kodi 播放”选项
+ 显示用 Kodi媒体中心 播放视频的选项
+ 音频
+ 默认音频格式
+ 默认视频格式
+ 主题
+ 亮色
+ 酷黑
+ 黑色
+ 记住弹出窗口的尺寸与位置
+ 记住上一次弹出窗口的位置以及大小
+ 清理照片内存
+ 最小化弹出播放器
+
+
+清除观看历史
+ 已删除搜索历史
+ 错误
+ 网络错误
+ 举报错误
+ 没有结果
+ 开始
+ 暂停
+ 播放
+ 创建
+ 删除
+ 删除所有
+ 新任务
+ 好
+\n
+
+ 错误
+\n
+ 服务器无法支持
+ 文件已存在
+ NewPipe 下载中
+ 请稍等…
+ 字母与数字
+ 最特别的字符
+
+ 这个文件里没有已下载应用程式
+
+ 关于NewPipe
+ 设置
+ 关于
+ 第三方执照
+ 打开网页
+ 删除书签
+
+ 请问你要删除这个列表吗?
+ 已创建播放列表
+ 已加入播放列表
+ 步骤
+ 重置
+
+ 为了遵守欧洲通用数据保护法规(GDPR,我们请你注意NewPipe的隐私政策.请仔细阅读.
+\n你必须接受它才能将错误报告发送给我们.
+ 接受
+ 拒绝
+
+ 没有限制
+ 使用移动数据时的解析度限制
+ 最小化应用程序切换
+ 从主视频播放器切换到其他应用程序时的操作 - %s
+ 没有
+ 最小化背景播放器
+
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index a1ee6ab1e..5684c4756 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -32,8 +32,6 @@
Zobrazit možnost přehrání videa pomocí multimediálního centra Kodi
Zvuk
Výchozí formát zvuku
- WebM — volný formát
- M4A — lepší kvalita
Téma
Tmavé
Světlé
@@ -314,7 +312,7 @@ otevření ve vyskakovacím okně
Importovat databázi
Exportovat databázi
Přepíše vaši dosavadní historii a odběry
- Exportuje historii, odběry a playlisty.
+ Exportuje historii, odběry a playlisty
Externí přehrávače nepodporují tyto druhy odkazů
Neplatná URL
Nenalezeny žádné video streamy
@@ -399,13 +397,13 @@ otevření ve vyskakovacím okně
Použít rychlé nepřesné hledání
Nepřesné hledání umožní přehrávači posouvat se rychleji, ale se sníženou přesností
Načítat náhledy
- Zakažte, chcete-li zabránit načítání všech náhledů a tím šetřit data a využití paměti. Změnou tohoto nastavení dojde k vyčištění mezipaměti obrázků.
+ Zakažte, chcete-li zabránit načítání všech náhledů a tím šetřit data a využití paměti. Změnou tohoto nastavení dojde k vyčištění mezipaměti obrázků
Mezipaměť obrázků vymazána
Vymazat metadata v mezipaměti
Odebrat všechna data uložená v mezipaměti
Mezipaměť metadat vymazána
Automatická fronta dalšího streamu
- Automaticky připojí související stream při přehrávání posledního streamu v neopakující se frontě.
+ Automaticky připojí související stream při přehrávání posledního streamu v neopakující se frontě
Soubor
Neplatný adresář
@@ -460,16 +458,16 @@ otevření ve vyskakovacím okně
Výchozí chování při otevírání obsahu — %s
Poznámky
- Upravuje velikost textu poznámek a styly pozadí. Změny se projeví po restartu aplikace.
+ Upravuje velikost textu poznámek a styly pozadí. Změny se projeví po restartu aplikace
K přehrání tohoto souboru chybí vhodná aplikace
Vymazat historii sledování
- Vymaže historii přehrávaných streamů.
+ Vymaže historii přehrávaných streamů
Vymazat celkovou historii sledování.
Historie sledování smazána.
Vymazat historii vyhledávání
- Vymaže historii vyhledávaných klíčových slov.
+ Vymaže historii vyhledávaných klíčových slov
Vymazat celkovou historii vyhledávání.
Historie vyhledávání smazána.
Jedna položka smazána.
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 560b0e8e4..37f892b4b 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -24,8 +24,6 @@
Zeigt eine Option an, über die man Videos mit Kodi abspielen kann
Audio
Bevorzugtes Audioformat
- WebM — freies Format
- M4A — bessere Qualität
Herunterladen
Nächstes Video
\'Nächste\' und \'ähnliche\' Videos anzeigen
@@ -96,7 +94,7 @@
Live
- „Suchen“ antippen um zu beginnen
+ „Suchen“ antippen, um zu beginnen
Downloads
Downloads
Fehlerbericht
@@ -286,7 +284,7 @@
Tipp anzeigen, wenn der Hintergrundwiedergabe- oder Pop-up-Button auf der Videodetailseite gedrückt gehalten wird
In der Warteschlange der Hintergrundwiedergabe
Neu & Heiß
- Zum Anfügen gedrückt halten
+ Halten, um zur Wiedergabeliste hinzuzufügen
\"Gedrückt halten, um Tipp hinzuzufügen\" anzeigen
[Unbekannt]
@@ -298,8 +296,8 @@
Spenden
Zurückgeben
Website
- Besuche die NewPipe Website für weitere Informationen und Neuigkeiten.
- NewPipe wird von Freiwilligen entwickelt, die ihre Freizeit dafür aufbringen, die beste Nutzererfahrung zu bieten. Gib etwas zurück, um Entwicklern zu helfen, NewPipe bei einer Tasse Kaffee noch besser zu machen.
+ Besuche die NewPipe-Website für weitere Informationen und Neuigkeiten.
+ NewPipe wird von Freiwilligen entwickelt, die ihre Freizeit dafür verwenden, dir die beste Nutzererfahrung zu bieten. Gib etwas zurück, um den Entwicklern zu helfen, NewPipe noch besser machen zu können, während sie sich an einer Tasse Kaffee erfreuen.
Service
Keinen Streamplayer gefunden (du kannst VLC installieren, um den Stream abzuspielen)
Standard-Land des Inhalts
@@ -325,11 +323,11 @@
Immer fragen
Informationen werden abgerufen…
- Die angeforderten Inhalte werden geladen
+ Gewünschten Inhalt laden
Datenbank importieren
Datenbank exportieren
Wird deinen Verlauf und deine Abos überschreiben
- Verlauf, Abos und Wiedergabelisten exportieren.
+ Verlauf, Abos und Wiedergabelisten exportieren
Keine gültige ZIP-Datei
Warnung: Nicht alle Dateien konnten importiert werden.
Dies wird deine aktuellen Einstellungen überschreiben.
@@ -358,7 +356,7 @@
Wiedergabeliste löschen
Wiedergabeliste umbenennen
Zur Wiedergabeliste hinzufügen
- Als Vorschaubild der Wiedergabeliste festlegen
+ Als Symbolbild der Wiedergabeliste festlegen
Lesezeichen entfernen
@@ -407,8 +405,8 @@
Metadatencache gelöscht
Debug
Ungültige Datei-/Inhaltsquelle
- Export fertiggestellt
- Import fertiggestellt
+ Export abgeschlossen
+ Import abgeschlossen
Name
Import/Export
Import
@@ -418,12 +416,12 @@
Wiedergabegeschwindigkeitsregler
Geschwindigkeit
Tonhöhe
- Aushaken (kann zu Verzerrungen führen)
+ Verknüpfung aufheben (kann zu Verzerrungen führen)
Nightcore
Standard
-Deaktiviere diese Option, um das Laden aller Miniaturansichten zu stoppen und Daten und Speicherverbrauch zu sparen. Wenn du dies änderst, wird sowohl der In-Memory- als auch der On-Disk-Image-Cache gelöscht.
+Deaktiviere diese Option, um das Laden aller Miniaturansichten zu stoppen und Daten und Speicherverbrauch zu sparen. Wenn du dies änderst, wird sowohl der In-Memory- als auch der On-Disk-Image-Cache gelöscht
Nächsten Stream automatisch einreihen
- Automatisches Anhängen eines verwandten Streams beim Abspielen des letzten Streams in einer nicht wiederholten Warteschlange.
+ Automatisches Anhängen eines verwandten Streams beim Abspielen des letzten Streams in einer nicht wiederholten Warteschlange
Hier wird bald etwas stehen ;D
@@ -456,16 +454,17 @@
Standardaktion beim Öffnen von Inhalten - %s
Untertitel
- Ändere Textgröße und Hintergrundstil des Untertitels im Player. Wird erst nach Neustart des App wirksam.
+ Ändere Textgröße und Hintergrundstil des Untertitels im Player. Wird erst nach Neustart des App wirksam
Keine App zum Abspielen dieser Datei installiert
Verlauf leeren
- Löscht den Verlauf der abgespielten Streams.
+ Löscht den Verlauf der abgespielten Streams
+
Löscht den ganzen Verlauf.
Verlauf gelöscht.
Suchverlauf löschen
- Lösche Verlauf der Suchschlüsselwörter.
+ Lösche Verlauf der Suchschlüsselwörter
Lösche gesamten Suchverlauf.
Suchverlauf gelöscht.
1 Element gelöscht.
@@ -479,8 +478,22 @@
Lies die Datenschutzbestimmungen
Akzeptieren
Ablehnen
-Um den Anforderungen der Datenschutz-Grundverordnung (DSGVO) genüge zu tun, möchten wir dich hiermit auf die Datenschutzbedingungen von NewPipe aufmerksam machen. Bitte lies sie sorgfältig durch.
-\nDu musst ihr zustimmen, um einen Bugreport an uns zu senden.
+Um der europäischen Datenschutz-Grundverordnung (DSGVO) gerecht zu werden, weisen wir hiermit auf NewPipe\'s Datenschutzerklärung hin. Bitte lies sie sorgfältig durch.
+\nDu musst den Datenschutzrichtlinien zustimmen, um den Fehlerbericht an uns zu senden.
Unbegrenzt
Auflösung bei Verwendung mobiler Daten begrenzen
+ Minimieren beim Anwendungswechsel
+ Aktion beim Umschalten auf eine andere Anwendung vom Haupt-Videoplayer - %s
+ Keine
+ Zum Hintergrund-Player minimieren
+ Zum Popup-Player minimieren
+
+Vorspulen während der Stille
+ Schritt
+ Zurücksetzen
+
+ Kanäle
+ Wiedergabelisten
+ Titel
+ Nutzer
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index fd89066aa..975ad4025 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -2,7 +2,7 @@
%1$s προβολές
Ανέβηκε στις %1$s
- Δεν βρέθηκε πρόγραμμα αναπαραγωγής. Εγκατάσταση του VLC;
+ Δεν βρέθηκε πρόγραμμα αναπαραγωγής. Θα θέλατε να εγκαταστήσετε το VLC;
Εγκατάσταση
Ακύρωση
Άνοιγμα στον browser
@@ -14,52 +14,454 @@
Κοινοποίηση με
Επιλέξτε browser
περιστροφή
- Χρήση εξωτερικού video player
- Χρήση εξωτερικού audio player
+ Χρήση εξωτερικής εφαρμογής αναπαραγωγής βίντεο
+ Χρήση εξωτερικής συσκευής αναπαραγωγής ήχου
- Διαδρομή λήψης video
- Διαδρομή για αποθήκευση των video.
- Εισάγετε διαδρομή για λήψη των video
+ Διαδρομή λήψης βίντεο
+ Διαδρομή για αποθήκευση των βίντεο
+ Εισάγετε διαδρομή για λήψη των βίντεο
Διαδρομή λήψης αρχείων ήχου
Διαδρομή για αποθήκευση αρχείων ήχου
- Εισάγετε διαδρομή για λήψη αρχείων ήχου.
+ Εισάγετε διαδρομή για λήψη αρχείων ήχου
Προεπιλεγμένη ανάλυση
Αναπαραγωγή με το Kodi
- Η εφαρμογή Kore δεν βρέθηκε. Εγκατάσταση;
+ Η εφαρμογή Kore δεν βρέθηκε. Εγκατάσταση της;
Εμφάνιση της επιλογής \"Αναπαραγωγή με το Kodi\"
- Προβολή μιας επιλογής για αναπαραγωγή με το Kodi media center.
+ Προβολή μιας επιλογής για αναπαραγωγή με το Kodi media center
Ήχος
Προεπιλεγμένη μορφή ήχου
- WebM — δωρεάν format
- m4a — καλύτερη ποιότητα
+ WebM — δωρεάν μορφή
+ Μ4Α — καλύτερη ποιότητα
Θέμα
Σκοτεινό
Φωτεινό
Λήψη
- Επόμενο video
- Προβολή επόμενου και σχετικών video
+ Επόμενο βίντεο
+ Εμφάνιση \"επόμενου\" και \"σχετικών\" βίντεο
Δεν υποστηρίζεται η διεύθυνση URL
- Προτιμώμενη γλώσσα περιεχομένου
- Video & Ήχος
+ Προεπιλεγμένη γλώσσα περιεχομένου
+ Βίντεο & Ήχος
Εμφάνιση
Άλλα
Αναπαραγωγή στο υπόβαθρο
Αναπαραγωγή
Σφάλμα δικτύου
- Εικόνα προεπισκόπησης video
- Εικόνα προεπισκόπησης video
- Εικόνα προφίλ του uploader
+ Μικρογραφία προεπισκόπισης βίντεο
+ Μικρογραφία προεπισκόπησης βίντεο
+ Μικρογραφία εικόνας προφίλ του χρήστη
Like
Dislike
Χρήση του Tor
- Αναγκάζει την κίνηση λήψης μέσω Tor για αυξημένη ανωνυμία (η αναπαραγωγή δεν υποστηρίζεται ακόμη)
+ (Πειραματικό) Αναγκάζει την κίνηση λήψης μέσω Tor για αυξημένη προστασία προσωπικών δεδομένων (η αναπαραγωγή δεν υποστηρίζεται ακόμη).
Δεν μπόρεσε να δημιουργηθεί ο φάκελος \'%1$s\'
Δημιουργήθηκε ο φάκελος \'%1$s\'
Δ
-
+ Άνοιγμα σε αναδυόμενο παράθυρο
+ Εγγραφή
+ Εγγεγραμμένος
+ Μόνο μερικές συσκευές υποστηρίζουν την αναπαραγωγή 2K/4K βίντεο
+ Μαύρο
+ Προτάσεις αναζήτησης
+ Ιστορικό αναζήτησης
+ Λήψεις
+ Λήψεις
+ Όλα
+ Κανάλι
+ Ναι
+ Αργότερα
+ Φίλτρο
+ Σφάλμα
+ ΑΝΑΦΟΡΑ
+ Πληροφορίες:
+ Τι συνέβη:
+ Το σχόλιό σας (στα Αγγλικά):
+ Λεπτομέρειες:
+
+
+ Αναφορά Σφάλματος
+ Βίντεο
+ Ήχος
+ Παύση
+ Διαγραφή
+ Σφάλμα
+ Το αρχείο υπάρχει ήδη
+ Πατήστε για λεπτομέρειες
+ Λήψη
+ Γράμματα και ψηφία
+ Ρυθμίσεις
+ Άνοιγμα ιστοσελίδας
+ Άδειες
+ Ιστορικό
+ Ιστορικό
+ Εμφάνιση πληροφοριών
+
+ Πατήστε στην αναζήτηση για να ξεκινήσετε
+ Δε βρέθηκε πρόγραμμα αναπαραγωγής ροής δεδομένων (μπορείτε να εγκαταστήσετε το VLC για να κάνετε αναπαραγωγή)
+ Κατέβασμα του αρχείου ροής.
+ Κάποιες αναλύσεις ΔΕΝ θα περιλαμβάνουν ήχο όταν επιλεχθεί αυτή η επιλογή
+ NewPipe σε λειτουργία αναδυόμενου παραθύρου
+ Έγινε διαγραφή από το κανάλι
+ Αδύνατη η αλλαγή της εγγραφής
+ Αδύνατη η ενημέρωση της εγγραφής
+ Κύριο
+ Εγγραφές
+ Σελιδοδείκτες
+
+ Νέα
+
+ Στο παρασκήνιο
+ Αναδυόμενο παράθυρο
+ Προσθήκη σε
+
+ Αυτόματη αναπαραγωγή
+ Αναπαραγωγή του βίντεο όταν το NewPipe καλείται από άλλη εφαρμογή
+ Προεπιλεγμένη ανάλυση αναδυόμενου παραθύρου
+ Εμφάνιση υψηλότερων αναλύσεων
+ Προεπιλεγμένη μορφή βίντεο
+ Ενθύμιση του μεγέθους και της θέσης του αναδυόμενου παραθύρου
+ Ενθύμιση του τελευταίου μεγέθους και θέσης του παραθύρου
+ Χρήση γρήγορης μη-ακριβούς αναζήτησης
+ Η μη-ακριβής αναζήτηση επιτρέπει στην εφαρμογή να αναζητεί θέσεις στο βίντεο γρηγορότερα με μειωμένη ακρίβεια
+ Φόρτωση thumbnails
+ Απενεργοποιήστε για να σταματήσετε τη φόρτωση των μικρογραφιών και να εξοικονομήσετε δεδομένα και χρήση μνήμης. Αλλάζοντας αυτή τη ρύθμιση θα εκκαθαρίσει τα προσωρινά αποθηκευμένα δεδομένα στη μνήμη και τον αποθηκευτικό χώρο
+ Εκκαθαρίστηκε η προσωρινή μνήμη εικονών
+ Εκκαθάριση προσωρινά αποθηκευμένων μεταδεδομένων
+ Αφαίρεση όλων των προσωρινά αποθηκευμένων δεδομένων ιστοσελίδων
+ Η προσωρινή μνήμη μεταδεδομένων εκκαθαρίστηκε
+ Αυτόματη πρόσθεση της επόμενης ροής στην ουρά
+ Αυτόματη πρόσθεση μιας σχετικής ροής όταν αναπαράγεται η προηγούμενη ροή σε μια μη-επαναλαμβανόμενη ουρά
+ Έλεγχος αναπαραγωγής με χειρονομίες
+ Χρήση χειρονομιών για τον έλεγχο της φωτεινότητας και της έντασης ήχου της εφαρμογής
+ Εμφάνιση υποδείξεων ενώ κάνετε αναζήτηση
+ Αποθήκευση αναζητήσεων στη συσκευή
+ Ιστορικό & Προσωρινή Αποθήκευση
+ Κρατήστε ιστορικό των βίντεο που έχετε δει
+ Συνέχεια όταν η εφαρμογή έρθει σε πρώτο πλάνο
+ Συνέχιση της αναπαραγωγής έπειτα από διακοπές (π.χ.: κλήσεις)
+ Εμφάνιση της βοήθειας \"πιέστε παρατεταμένα για πρόσθεση\"
+ Εμφάνιση της βοήθειας όταν έχει πατηθεί το κουμπί Παρασκηνίου ή Αναδυόμενου παραθύρου στη σελίδα λεπτομερειών του βίντεο
+ Προεπιλεγμένη χώρα περιεχομένου
+ Υπηρεσία
+ Συσκευή Αναπαραγωγής
+ Συμπεριφορά
+ Ιστορικό & Προσωρινή Αποθήκευση
+ Αναδυόμενο παράθυρο
+ Απασφαλμάτωση
+ Αναπαραγωγή σε αναδυόμενο παράθυρο
+ Προστέθηκε στη λίστα αναπαραγωγής παρασκηνίου
+ Προστέθηκε στη λίστα αναπαραγωγής αναδυόμενου παραθύρου
+ Περιεχόμενο
+ Εμφάνιση περιεχομένου περιορισμένης πρόσβασης
+ Βίντεο περιορισμένης πρόσβασης. Για να επιτρέπετε περιερχόμενο τέτοιου είδους, ενεργοποιήστε το στις Ρυθμίσεις.
+ Ζωντανά
+ Αναφορά σφαλμάτων
+ Κανάλια
+ Λίστα αναπαραγωγής
+ Λίστες αναπαραγωγής
+ Κομμάτια
+ Χρήστες
+ Απενεργοποιημένο
+ Ανανέωση
+ Εκκαθάριση
+ Αλλαγή μεγέθους
+ Βέλτιστη ανάλυση
+ Αναίρεση
+ Αναπαραγωγή όλων
+ Πάντα
+ Μόνο μία φορά
+ Αρχείο
+
+ Ειδοποίηση NewPipe
+ Ειδοποιήσεις για την αναπαραγωγή Παρασκηνίου και Αναδυόμενου Παραθύρου
+
+ [Άγνωστο]
+
+ Αλλαγή προσανατολισμού
+ Αλλαγή σε Παρασκήνιο
+ Αλλαγή σε Αναδυόμενο Παράθυρο
+ Αλλαγή σε Κύριο
+
+ Εισαγωγή βάσης δεδομένων
+ Εξαγωγή βάσης δεδομένων
+ Θα παρακάμψει το τρέχον ιστορικό και εγγραφές σας
+ Εξαγωγή ιστορικού, εγγραφών και λιστών αναπαραγωγής
+ Εκκαθάριση ιστορικού προβολής
+ Διαγράφει το ιστορικό των ροών που έχουν αναπαραχθεί
+ Διαγραφή ολόκληρου του ιστορικού προβολής.
+ Το στορικό προβολής διαγράφηκε.
+ Διαγραφή ιστορικού αναζητήσεων
+ Διαγράφει το ιστορικό αναζητήσεών σας
+ Διαγραφή ολόκληρου του ιστορικού αναζητήσεων.
+ Το ιστορικό αναζητήσεων διαγράφηκε.
+ Δεν ήταν δυνατή η φόρτωση όλων των εικονιδίων
+ Δεν ήταν δυνατή η αποκρυπτογράφηση της υπογραφής του URL του βίντεο
+ Δεν ήταν δυνατή η ανάλυση του ιστοτόπου
+ Δεν ήταν δυνατή η ανάλυση ολόκληρου του ιστοτόπου
+ Το περιεχόμενο δεν είναι διαθέσιμο
+ "Έχει αποκλειστεί από την GEMA"
+ Δεν ήταν δυνατή η ρύθμιση του μενού λήψεων
+ Αυτή είναι μια Ζωντανή Ροή, που δεν υποστηρίζεται ακόμα.
+ Δεν ήταν δυνατή η λήψη καμίας ροής
+ Δεν ήταν δυνατή η φόρτωση της εικόνας
+ Η εφαρμογή κράσαρε
+ Δεν ήταν δυνατή η αναπαραγωγή αυτής της ροής
+ Συνέβη ένα μη ανακτήσιμο σφάλμα στη συσκευή αναπαραγωγής
+ Ανάκτηση από σφάλμα της συσκευής αναπαραγωγής
+ Οι εξωτερικές συσκευές αναπαραγωγής δεν υποστηρίζουν αυτού του είδους συνδέσμους
+ Μη έγκυρο URL
+ Δε βρέθηκαν ροές βίντεο
+ Δε βρέθηκαν ροές ήχου
+ Μη έγκυρη τοποθεσία
+ Μη έγκυρο αρχείο ή πηγή περιεχομένου
+ Το αρχείο δεν υπάρχει ή δεν έχουμε επαρκή εξουσιοδότηση για να διαβάσουμε ή να γράψουμε σε αυτό
+ Το όνομα αρχείου δεν μπορεί να είναι κενό
+ Προέκυψε ένα σφάλμα: %1$s
+ Δεν υπάρχουν διαθέσιμες ροές για λήψη
+
+ Λυπούμαστε, αυτό δεν έπρεπε να έχει συμβεί.
+ Αναφορά σφάλματος με ηλεκτρονικό ταχυδρομίο
+ Λυπούμαστε, συνέβησαν κάποια σφάλματα.
+ What:\\nΑίτημα:\\nΓλώσσα περιεχομένου:\\nΥπηρεσία:\\nΏρα GMT:\\nΠακέτο:\\nΈκδοση:\\nΈκδοση λειτουργικού:
+ Αναφορά χρήστη
+ Κανένα αποτέλεσμα
+ Τρεις κι ο κούκος
+ Σύρετε για ταξινόμηση
+
+ Προσπάθεια εκ νέου
+ Δεν δώθηκε άδεια εγγραφής στην εσωτερική μνήμη
+ Χρήση παλαιάς συσκευής αναπαραγωγής
+ Παλαιά συσκευή αναπαραγωγής Mediaframework
+
+ Κ
+ Μ
+ Κανένας εγγεγραμένος χρήστης
+
+ - %s εγγεγραμένος χρήστης
+ - %s εγγεγραμένοι χρήστες
+
+
+ Καμία προβολή
+
+ - %s προβολή
+ - %s προβολές
+
+
+ Κανένα βίντεο
+
+ - %s βίντεο
+ - %s βίντεο
+
+
+ Εκκίνηση
+ Αναπαραγωγή
+ Δημιουργία
+ Διαγραφή ενός
+ Διαγραφή όλων
+ Checksum
+ Αγνόηση
+ Μετονομασία
+
+ Νέα αποστολη
+ ΟΚ
+
+ Όνομα αρχείου
+ Νήματα
+ Ο εξυπηρετητής δεν υποστηρίζεται
+ Λάθος μορφή URL ή δεν υπάρχει σύνδεση στο διαδίκτυο
+ Λήψη NewPipe
+ Παρακαλώ περιμένετε…
+ Αντιγράφηκε στο πρόχειρο
+ Παρακαλώ επιλέξτε έναν διαθέσιμο φάκελο λήψεων
+ Αυτή η άδεια είναι απαραίτητη για
+\nτο άνοιγμα αναδυόμενων παραθύρων
+ 1 αντικείμενο διαγράφηκε.
+
+ reCAPTCHA
+ Πρόκληση reCAPTCHA
+ Ζητήθηκε πρόκληση reCAPTCHA
+
+ Επιτρεπτοί χαρακτήρες σε ονόματα αρχείων
+ Οι μη έγκυροι χαρακτήρες αντικαθίστανται με αυτήν την τιμή
+ Αντικαταστάτης χαρακτήρας
+
+ Οι περισσότεροι ειδικοί χαρακτήρες
+
+ Δεν υπάρχει εφαρμογή εγκατεστημένη για την αναπαραγωγή αυτού του αρχείου
+
+ Σχετικά με το NewPipe
+ Περί
+ Άδειες Τρίτων
+ © %1$s από %2$s υπό %3$s
+ Δεν ήταν δυνατή η φόρτωση της άδειας
+ Περί
+ Συνεισφέροντες
+ Ελέυθερη και ελαφριά εφαρμογή αναπαραγωγής ροών στο Android.
+ Συνεισφέρετε
+ Αν έχετε ιδέες για μετάφραση, αλλαγή σχεδιασμού, εκκαθάριση ή ριζικές αλλαγές κώδικα της εφαρμογής—η βοήθεια σας είναι πάντα ευπρόσδεκτη. Όσο περισσότερη έχουμε, τόσο καλύτεροι γινόμαστε!
+ Δείτε το στο GitHub
+ Κάντε μια δωρεά
+ Το NewPipe αναπτύσσεται από εθελοντές που δαπανούν χρόνο για να σας προσφέρουν τη βέλτιστη δυνατή εμπειρία της εφαρμογής. Μπορείτε να συνεισφέρετε στους προγραμματιστές του NewPipe να το κάνουν ακόμα καλύτερο, προσφέροντάς τους έναν καφέ.
+ Προσφέρτε
+ Ιστότοπος
+ Επισκευτείτε τον ιστότοπο του NewPipe για περισσότερες πληροφορίες και νέα.
+ Η πολιτική ιδιωτικού απόρρητου του NewPipe
+ Το NewPipe παίρνει πολύ σοβαρά το ιδιωτικό σας απόρρητο. Έτσι, η εφαρμογή αυτή δεν συλλέγει δεδομένα από εσάς χωρίς τη συγκατάθεσή σας.
+\nΗ πολιτική ιδιωτικού απόρρητου του NewPipe εξηγεί λεπτομερώς ποια δεδομένα αποστέλλονται και αποθηκεύονται όταν επιλέγετε να στείλετε μια αναφορά σφαλμάτων.
+ Διαβάστε την πολιτική ιδιωτικού απόρρητου
+ Η άδεια του NewPipe
+ Το NewPipe είναι copylelft ελεύθερο λογισμικό: Μπορείτε να το χρησιμοποιήσετε, να το μελετήσετε, να το μοιραστείτε και να το βελτιώσετε κατά βούληση. Ειδικότερα, μπορείτε να το αναδιανείμετε ή/και να το τροποποιήσετε υπό την άδεια GNU General Public Licence όπως αυτή εκδόθηκε από το Free Software Foundation, είτε υπό την έκδοση 3 της Άδειας είτε (προεραιτικά) υπό οποιαδήποτε μεταγενέστερη άδεια.
+ Διαβάστε την άδεια
+
+
+ Αναζητημένα
+ Έχει γίνει προβολή
+ Το ιστορικό έχει απενεργοποιηθεί
+ Το ιστορικό είναι κενό
+ Το ιστορικό εκκαθαρίστηκε
+ Το αντικείμενο διαγράφηκε
+ Θέλετε να σβήσετε αυτό το αντικείμενο από το ιστορικό αναζήτησης;
+ Θέλετε να σβήσετε αυτό το αντικείμενο από το ιστορικό προβολής;
+ Είστε σίγουροι ότι θέλετε να σβήσετε όλα τα αντικείμενα από το ιστορικό;
+ Τελευταία αναπαραγωγή
+ Αναπαράχθηκε περισσότερο
+
+ Περιεχόμενο της κεντρικής σελίδας
+ Κενή σελίδα
+ Σελίδα περιπτέρου
+ Σελίδα εγγραφών
+ Σελίδα καναλιών
+ Επιλέξτε ένα κανάλι
+ Δεν έχει γίνει εγγραφή σε κάποιο κανάλι ακόμα
+ Επιλέξτε ένα περίπτερο
+ Η εξαγωγή ολοκληρώθηκε
+ Η εισαγωγή ολοκληρώθηκε
+ Μη έγκυρο αρχείο ZIP
+ Προσοχή: Δεν ήταν δυνατή η εισαγωγή όλων των αρχείων.
+ Αυτό θα παρακάμψει τις τρέχουσες ρυθμίσεις σας.
+ Θέλετε επίσης να εισάγετε ρυθμίσεις;
+
+ Περίπτερο
+ Συσκευή αναπαραγωγής Παρασκηνίου
+ Συσκευή αναπαραγωγής Αναδυόμενου παραθύρου
+ Αφαίρεση
+ Λεπτομέρειες
+ Ρυθμίσεις ήχου
+ Πιέστε παρατεταμένα για να προστεθεί στην ουρά
+ Προσθήκη στην ουρά Παρασκηνίου
+ Προσθήκη στην ουρά Αναδυόμενου παραθύρου
+ Εκκίνηση Αναπαραγωγής εδώ
+ Εκκίνηση εδώ στο Παρασκήνιο
+ Εκκίνηση εδώ στο Αναδυόμενο Παράθυρο
+
+ Άνοιγμα Συρταριού
+ Κλείσιμο Συρταριού
+ Κάτι θα παιχτεί εδω σύντομα ;D
+
+
+ Τοπ 50
+ Καινούρια & δημοφιλή
+ Προτιμώμενη ενέργεια ανοίγματος
+ Προεπιλεγμένη ενέργεια για το άνοιγμα περιεχομένου — %s
+
+ Συσκευή αναπαραγωγής βίντεο
+ Αναπαραγωγή Παρασκηνίου
+ Αναπαραγωγή σε Αναδυόμενο Παράθυρο
+ Πάντα ερώτηση
+
+ Γίνεται λήψη πληροφοριών…
+ Γίνεται φόρτωση του ζητούμενου περιεχομένου
+
+ Δημιουργία νέας Λίστας Αναπαραγωγής
+ Διαγραφή Λίστας
+ Μετονομασία Λίστας
+ Όνομα
+ Προσθήκη στη Λίστα
+ Ορισμός ως μικρογραφία λίστας αναπαραγωγής
+
+ Προσθήκη Σελιδοδείκτη στη Λίστα
+ Διαγραφή Σελιδοδείκτη
+
+ Επιθυμείτε να διαγράψετε αυτή τη λίστα αναπαραγωγής;
+ Η λίστα αναπαραγωγής δημιουργήθηκε
+ Προστέθηκε στη λίστα
+ Η μικρογραφία της λίστας αναπαραγωγής άλλαξε
+ Δεν ήταν δυνατή η διαγραφή της λίστας
+
+ Δεν υπάρχουν υπότιτλοι
+
+ Προσαρμογή
+ Γέμισμα
+ Μεγέθυνση
+
+ Αυτόματοι
+
+ Υπότιτλοι
+ Τροποποίηση του μεγέθους και του φόντου των υπότιτλων. Απαιτεί επανεκκίνηση της εφαρμογής
+
+ Ενεργοποίηση του LeakCanary
+ Η παρακολούθηση των διαρροών μνήμης μπορεί να κάνει την εφαρμογή να μην αποκρίνεται κατά το heap dumping
+
+
+
+ Εισαγωγή/Εξαγωγή
+ Εισαγωγή
+ Εισαγωγή από
+ Εξαγωγή σε
+
+ Γίνεται εισαγωγή…
+ Γίνεται εξαγωγή…
+
+ Εισαγωγή αρχείου
+ Προηγούμενη εξαγωγή
+
+ Δεν ήταν δυνατή η εισαγωγή των εγγραφών
+ Δεν ήταν δυνατή η εισαγωγή των εγγραφών
+
+ Κάντε εισαγωγή των εγγραφών σας στο YouTube κατεβάζοντας το εξής αρχείο:
+\n
+\n1. Πλοηγηθήτε στο: %1$s
+\n2. Εισέλθετε στο λογαριασμό σας, όταν σας ζητηθεί
+\n3. Η λήψη του αρχείου των εγγραφών σας θα ξεκινήσει
+ Για να εισάγετε τον λογαριασμό SoundCloud σας, πληκτρολογήστε τον σύνδεσμο ή το ID σας:
+\n
+\n1. Ενεργοποιήστε τη λειτουργία \"Desktop mode\" στον φυλλομετρητή σας (καθώς η ιστοσελίδα δεν είναι διαθέσιμη για κινητά)
+\n2. Πλοηθηθείτε στο %1$s
+\n3. Εισέλθετε στο λογαριασμό σας, όταν σας ζητηθεί
+\n4. Αντιγράψτε τον σύνδεσμο του λογαριαμού στον οποίο ανακατευθυνθήκατε.
+ Αυτή η διαδικασία μπορεί να χρησιμοποιήσει μεγάλο όγκο δεδομένων.
+\n
+\nΕπιθυμείτε να συνεχίσετε;
+
+ Έλεγχος ταχύτητας αναπαραγωγής
+ Τέμπο
+ Τόνος
+ Ενέργεια κατά τη μετάβαση σε άλλη εφαρμογή — %s
+ Σελίδαρ Ροής
+ Δημοφιλή
+ Αναφορά σφαλμάτων εκτός κύκλου ζωής
+ Το όνομα χρήστη σας, soundcloud.com/όνομαχρήστη
+
+ Αποσύνδεση (μπορεί να προκαλέσει παραμόρφωση)
+ Επιτάχυνση αναπαραγωγής κατά τη διάρκεια σιωπής
+ Βήμα
+ Μηδενισμός
+
+ Προς συμμόρφωση με τον Ευρωπαϊκό Γενικό Κανονισμό για την Προστασία Δεδομένων (GDPR), σας επιστούμε την προσοχή στην πολιτική προστασίας προσωπικών δεδομένων του NewPipe. Παραλούμε, διαβάστε την προσεκτικά.
+\nΘα πρέπει να την αποδεχτέιτε προκειμένου να μας αποστείλετε την αναφορά σφάλματος.
+ Αποδοχή
+ Απόρριψη
+
+ Χωρίς όριο
+ Περιορισμός της ανάλυσης όταν γίνεται χρήση δεδομένων
+ Ελαχιστοποίηση κατά την εναλλαγή εφαρμογών
+ Καμία
+ Ελαχιστοποίηση στο παρασκήνιο
+ Ελαχιστοποίηση σε αναδυόμενο παράθυρο
+
+
diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml
index fdae53d50..78faeed8f 100644
--- a/app/src/main/res/values-eo/strings.xml
+++ b/app/src/main/res/values-eo/strings.xml
@@ -21,8 +21,6 @@
Montri \"Ludi per Kodi\"-opcion
Sono
Defaŭlta sondosierformo
- WebM — libera dosierformo
- m4a — pli bona kvalito
Etoso
Malluma
Luma
@@ -38,8 +36,8 @@
Ludi
Eraro
Reteraro
- Enhavo ne estas disponebla.
- Blokita de GEMA.
+ Enhavo ne estas disponebla
+ Blokita de GEMA
Ŝatoj
Malŝatoj
@@ -48,8 +46,8 @@
La aplikaĵo Kore ne estas trovita. Ĉu instali ĝin?
Montri la sekvan videon kaj similajn videojn
Ĉiuj miniaturoj ne ŝargeblas
- La subskribo de la ligilo de la video ne malĉifreblas.
- La retejo ne analizeblas.
+ La subskribo de la ligilo de la video ne malĉifreblas
+ La retejo ne analizeblas
Miniaturo de la antaŭrigardo de la video
Miniaturo de la antaŭrigardo de la video
Miniaturo de la bildo de la alŝutinto
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index bfd0f3e6e..02a9924a5 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -24,8 +24,6 @@
Mostrar una opción para reproducir vídeo con Kodi Media Center
Audio
Formato de audio por defecto
- WebM — formato libre
- M4A — mejor calidad
Descargar
Siguiente vídeo
URL no soportada
@@ -330,7 +328,7 @@ abrir en modo popup
Importar base de datos
Exportar base de datos
Reemplazará su historial actual y sus suscripciones
- Exportar historial, suscripciones y listas de reproducción.
+ Exportar historial, suscripciones y listas de reproducción
Exportación completa
Importación completa
Archivo ZIP no válido
@@ -403,7 +401,7 @@ abrir en modo popup
Usar búsqueda rápida inexacta
La búsqueda inexacta permite al reproductor buscar posiciones más rápido con menor precisión
Auto-encolar la siguiente transmisión
- Auto-añadir un vídeo relacionado al reproducir el último vídeo en una cola no repetitiva.
+ Auto-añadir un vídeo relacionado al reproducir el último vídeo en una cola no repetitiva
DIRECTO
SINCRONIZAR
Archivo
@@ -445,7 +443,7 @@ abrir en modo popup