kopia lustrzana https://github.com/TeamNewPipe/NewPipe
Merge pull request #6434 from litetex/playerSeekbarPreview
Player seekbar thumbnail previewpull/6689/head
commit
d57bfde604
|
@ -186,7 +186,7 @@ dependencies {
|
||||||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||||
// This works thanks to JitPack: https://jitpack.io/
|
// This works thanks to JitPack: https://jitpack.io/
|
||||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:c38a06e8dcd9c206a52b622704b138b78d633274'
|
implementation 'com.github.litetex:NewPipeExtractor:playerSeekbarPreview-SNAPSHOT'
|
||||||
|
|
||||||
/** Checkstyle **/
|
/** Checkstyle **/
|
||||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||||
|
|
|
@ -27,6 +27,7 @@ import android.util.DisplayMetrics;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.ContextThemeWrapper;
|
import android.view.ContextThemeWrapper;
|
||||||
|
import android.view.Gravity;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
|
@ -123,6 +124,8 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
|
||||||
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
|
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
|
||||||
import org.schabi.newpipe.player.resolver.MediaSourceTag;
|
import org.schabi.newpipe.player.resolver.MediaSourceTag;
|
||||||
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
|
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
|
||||||
|
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
|
||||||
|
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
|
@ -379,6 +382,8 @@ public final class Player implements
|
||||||
@NonNull private final SharedPreferences prefs;
|
@NonNull private final SharedPreferences prefs;
|
||||||
@NonNull private final HistoryRecordManager recordManager;
|
@NonNull private final HistoryRecordManager recordManager;
|
||||||
|
|
||||||
|
@NonNull private final SeekbarPreviewThumbnailHolder seekbarPreviewThumbnailHolder =
|
||||||
|
new SeekbarPreviewThumbnailHolder();
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -1676,12 +1681,67 @@ public final class Player implements
|
||||||
@Override // seekbar listener
|
@Override // seekbar listener
|
||||||
public void onProgressChanged(final SeekBar seekBar, final int progress,
|
public void onProgressChanged(final SeekBar seekBar, final int progress,
|
||||||
final boolean fromUser) {
|
final boolean fromUser) {
|
||||||
if (DEBUG && fromUser) {
|
// Currently we don't need method execution when fromUser is false
|
||||||
|
if (!fromUser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onProgressChanged() called with: "
|
Log.d(TAG, "onProgressChanged() called with: "
|
||||||
+ "seekBar = [" + seekBar + "], progress = [" + progress + "]");
|
+ "seekBar = [" + seekBar + "], progress = [" + progress + "]");
|
||||||
}
|
}
|
||||||
if (fromUser) {
|
|
||||||
binding.currentDisplaySeek.setText(getTimeString(progress));
|
binding.currentDisplaySeek.setText(getTimeString(progress));
|
||||||
|
|
||||||
|
// Seekbar Preview Thumbnail
|
||||||
|
SeekbarPreviewThumbnailHelper
|
||||||
|
.tryResizeAndSetSeekbarPreviewThumbnail(
|
||||||
|
getContext(),
|
||||||
|
seekbarPreviewThumbnailHolder.getBitmapAt(progress),
|
||||||
|
binding.currentSeekbarPreviewThumbnail,
|
||||||
|
binding.subtitleView::getWidth);
|
||||||
|
|
||||||
|
adjustSeekbarPreviewContainer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void adjustSeekbarPreviewContainer() {
|
||||||
|
try {
|
||||||
|
// Should only be required when an error occurred before
|
||||||
|
// and the layout was positioned in the center
|
||||||
|
binding.bottomSeekbarPreviewLayout.setGravity(Gravity.NO_GRAVITY);
|
||||||
|
|
||||||
|
// Calculate the current left position of seekbar progress in px
|
||||||
|
// More info: https://stackoverflow.com/q/20493577
|
||||||
|
final int currentSeekbarLeft =
|
||||||
|
binding.playbackSeekBar.getLeft()
|
||||||
|
+ binding.playbackSeekBar.getPaddingLeft()
|
||||||
|
+ binding.playbackSeekBar.getThumb().getBounds().left;
|
||||||
|
|
||||||
|
// Calculate the (unchecked) left position of the container
|
||||||
|
final int uncheckedContainerLeft =
|
||||||
|
currentSeekbarLeft - (binding.seekbarPreviewContainer.getWidth() / 2);
|
||||||
|
|
||||||
|
// Fix the position so it's within the boundaries
|
||||||
|
final int checkedContainerLeft =
|
||||||
|
Math.max(
|
||||||
|
Math.min(
|
||||||
|
uncheckedContainerLeft,
|
||||||
|
// Max left
|
||||||
|
binding.playbackWindowRoot.getWidth()
|
||||||
|
- binding.seekbarPreviewContainer.getWidth()
|
||||||
|
),
|
||||||
|
0 // Min left
|
||||||
|
);
|
||||||
|
|
||||||
|
// See also: https://stackoverflow.com/a/23249734
|
||||||
|
final LinearLayout.LayoutParams params =
|
||||||
|
new LinearLayout.LayoutParams(
|
||||||
|
binding.seekbarPreviewContainer.getLayoutParams());
|
||||||
|
params.setMarginStart(checkedContainerLeft);
|
||||||
|
binding.seekbarPreviewContainer.setLayoutParams(params);
|
||||||
|
} catch (final Exception ex) {
|
||||||
|
Log.e(TAG, "Failed to adjust seekbarPreviewContainer", ex);
|
||||||
|
// Fallback - position in the middle
|
||||||
|
binding.bottomSeekbarPreviewLayout.setGravity(Gravity.CENTER);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1702,6 +1762,8 @@ public final class Player implements
|
||||||
showControls(0);
|
showControls(0);
|
||||||
animate(binding.currentDisplaySeek, true, DEFAULT_CONTROLS_DURATION,
|
animate(binding.currentDisplaySeek, true, DEFAULT_CONTROLS_DURATION,
|
||||||
AnimationType.SCALE_AND_ALPHA);
|
AnimationType.SCALE_AND_ALPHA);
|
||||||
|
animate(binding.currentSeekbarPreviewThumbnail, true, DEFAULT_CONTROLS_DURATION,
|
||||||
|
AnimationType.SCALE_AND_ALPHA);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override // seekbar listener
|
@Override // seekbar listener
|
||||||
|
@ -1717,6 +1779,7 @@ public final class Player implements
|
||||||
|
|
||||||
binding.playbackCurrentTime.setText(getTimeString(seekBar.getProgress()));
|
binding.playbackCurrentTime.setText(getTimeString(seekBar.getProgress()));
|
||||||
animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA);
|
animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA);
|
||||||
|
animate(binding.currentSeekbarPreviewThumbnail, false, 200, AnimationType.SCALE_AND_ALPHA);
|
||||||
|
|
||||||
if (currentState == STATE_PAUSED_SEEK) {
|
if (currentState == STATE_PAUSED_SEEK) {
|
||||||
changeState(STATE_BUFFERING);
|
changeState(STATE_BUFFERING);
|
||||||
|
@ -2866,6 +2929,10 @@ public final class Player implements
|
||||||
binding.titleTextView.setText(tag.getMetadata().getName());
|
binding.titleTextView.setText(tag.getMetadata().getName());
|
||||||
binding.channelTextView.setText(tag.getMetadata().getUploaderName());
|
binding.channelTextView.setText(tag.getMetadata().getUploaderName());
|
||||||
|
|
||||||
|
this.seekbarPreviewThumbnailHolder.resetFrom(
|
||||||
|
this.getContext(),
|
||||||
|
tag.getMetadata().getPreviewFrames());
|
||||||
|
|
||||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
||||||
notifyMetadataUpdateToListeners();
|
notifyMetadataUpdateToListeners();
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
package org.schabi.newpipe.player.seekbarpreview;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import androidx.annotation.IntDef;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
|
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.IntSupplier;
|
||||||
|
|
||||||
|
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||||
|
import static org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper.SeekbarPreviewThumbnailType.HIGH_QUALITY;
|
||||||
|
import static org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper.SeekbarPreviewThumbnailType.LOW_QUALITY;
|
||||||
|
import static org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper.SeekbarPreviewThumbnailType.NONE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for the seekbar preview.
|
||||||
|
*/
|
||||||
|
public final class SeekbarPreviewThumbnailHelper {
|
||||||
|
|
||||||
|
// This has to be <= 23 chars on devices running Android 7 or lower (API <= 25)
|
||||||
|
// or it fails with an IllegalArgumentException
|
||||||
|
// https://stackoverflow.com/a/54744028
|
||||||
|
public static final String TAG = "SeekbarPrevThumbHelper";
|
||||||
|
|
||||||
|
private SeekbarPreviewThumbnailHelper() {
|
||||||
|
// No impl pls
|
||||||
|
}
|
||||||
|
|
||||||
|
@Retention(SOURCE)
|
||||||
|
@IntDef({HIGH_QUALITY, LOW_QUALITY,
|
||||||
|
NONE})
|
||||||
|
public @interface SeekbarPreviewThumbnailType {
|
||||||
|
int HIGH_QUALITY = 0;
|
||||||
|
int LOW_QUALITY = 1;
|
||||||
|
int NONE = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Settings Resolution
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@SeekbarPreviewThumbnailType
|
||||||
|
public static int getSeekbarPreviewThumbnailType(@NonNull final Context context) {
|
||||||
|
final String type = PreferenceManager.getDefaultSharedPreferences(context).getString(
|
||||||
|
context.getString(R.string.seekbar_preview_thumbnail_key), "");
|
||||||
|
if (type.equals(context.getString(R.string.seekbar_preview_thumbnail_none))) {
|
||||||
|
return NONE;
|
||||||
|
} else if (type.equals(context.getString(R.string.seekbar_preview_thumbnail_low_quality))) {
|
||||||
|
return LOW_QUALITY;
|
||||||
|
} else {
|
||||||
|
return HIGH_QUALITY; // default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void tryResizeAndSetSeekbarPreviewThumbnail(
|
||||||
|
@NonNull final Context context,
|
||||||
|
@NonNull final Optional<Bitmap> optPreviewThumbnail,
|
||||||
|
@NonNull final ImageView currentSeekbarPreviewThumbnail,
|
||||||
|
@NonNull final IntSupplier baseViewWidthSupplier) {
|
||||||
|
|
||||||
|
if (!optPreviewThumbnail.isPresent()) {
|
||||||
|
currentSeekbarPreviewThumbnail.setVisibility(View.GONE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSeekbarPreviewThumbnail.setVisibility(View.VISIBLE);
|
||||||
|
final Bitmap srcBitmap = optPreviewThumbnail.get();
|
||||||
|
|
||||||
|
// Resize original bitmap
|
||||||
|
try {
|
||||||
|
Objects.requireNonNull(srcBitmap);
|
||||||
|
|
||||||
|
final int srcWidth = srcBitmap.getWidth() > 0 ? srcBitmap.getWidth() : 1;
|
||||||
|
final int newWidth = Math.max(
|
||||||
|
Math.min(
|
||||||
|
// Use 1/4 of the width for the preview
|
||||||
|
Math.round(baseViewWidthSupplier.getAsInt() / 4f),
|
||||||
|
// Scaling more than that factor looks really pixelated -> max
|
||||||
|
Math.round(srcWidth * 2.5f)
|
||||||
|
),
|
||||||
|
// Min width = 10dp
|
||||||
|
DeviceUtils.dpToPx(10, context)
|
||||||
|
);
|
||||||
|
|
||||||
|
final float scaleFactor = (float) newWidth / srcWidth;
|
||||||
|
final int newHeight = (int) (srcBitmap.getHeight() * scaleFactor);
|
||||||
|
|
||||||
|
currentSeekbarPreviewThumbnail.setImageBitmap(
|
||||||
|
Bitmap.createScaledBitmap(srcBitmap, newWidth, newHeight, true));
|
||||||
|
} catch (final Exception ex) {
|
||||||
|
Log.e(TAG, "Failed to resize and set seekbar preview thumbnail", ex);
|
||||||
|
currentSeekbarPreviewThumbnail.setVisibility(View.GONE);
|
||||||
|
} finally {
|
||||||
|
srcBitmap.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,252 @@
|
||||||
|
package org.schabi.newpipe.player.seekbarpreview;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.google.common.base.Stopwatch;
|
||||||
|
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.stream.Frameset;
|
||||||
|
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper.SeekbarPreviewThumbnailType;
|
||||||
|
|
||||||
|
public class SeekbarPreviewThumbnailHolder {
|
||||||
|
|
||||||
|
// This has to be <= 23 chars on devices running Android 7 or lower (API <= 25)
|
||||||
|
// or it fails with an IllegalArgumentException
|
||||||
|
// https://stackoverflow.com/a/54744028
|
||||||
|
public static final String TAG = "SeekbarPrevThumbHolder";
|
||||||
|
|
||||||
|
// Key = Position of the picture in milliseconds
|
||||||
|
// Supplier = Supplies the bitmap for that position
|
||||||
|
private final Map<Integer, Supplier<Bitmap>> seekbarPreviewData = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// This ensures that if the reset is still undergoing
|
||||||
|
// and another reset starts, only the last reset is processed
|
||||||
|
private UUID currentUpdateRequestIdentifier = UUID.randomUUID();
|
||||||
|
|
||||||
|
public synchronized void resetFrom(
|
||||||
|
@NonNull final Context context,
|
||||||
|
final List<Frameset> framesets) {
|
||||||
|
|
||||||
|
final int seekbarPreviewType =
|
||||||
|
SeekbarPreviewThumbnailHelper.getSeekbarPreviewThumbnailType(context);
|
||||||
|
|
||||||
|
final UUID updateRequestIdentifier = UUID.randomUUID();
|
||||||
|
this.currentUpdateRequestIdentifier = updateRequestIdentifier;
|
||||||
|
|
||||||
|
final ExecutorService executorService = Executors.newSingleThreadExecutor();
|
||||||
|
executorService.submit(() -> {
|
||||||
|
try {
|
||||||
|
resetFromAsync(seekbarPreviewType, framesets, updateRequestIdentifier);
|
||||||
|
} catch (final Exception ex) {
|
||||||
|
Log.e(TAG, "Failed to execute async", ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// ensure that the executorService stops/destroys it's threads
|
||||||
|
// after the task is finished
|
||||||
|
executorService.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetFromAsync(
|
||||||
|
final int seekbarPreviewType,
|
||||||
|
final List<Frameset> framesets,
|
||||||
|
final UUID updateRequestIdentifier) {
|
||||||
|
|
||||||
|
Log.d(TAG, "Clearing seekbarPreviewData");
|
||||||
|
seekbarPreviewData.clear();
|
||||||
|
|
||||||
|
if (seekbarPreviewType == SeekbarPreviewThumbnailType.NONE) {
|
||||||
|
Log.d(TAG, "Not processing seekbarPreviewData due to settings");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Frameset frameset = getFrameSetForType(framesets, seekbarPreviewType);
|
||||||
|
if (frameset == null) {
|
||||||
|
Log.d(TAG, "No frameset was found to fill seekbarPreviewData");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Frameset quality info: "
|
||||||
|
+ "[width=" + frameset.getFrameWidth()
|
||||||
|
+ ", heigh=" + frameset.getFrameHeight() + "]");
|
||||||
|
|
||||||
|
// Abort method execution if we are not the latest request
|
||||||
|
if (!isRequestIdentifierCurrent(updateRequestIdentifier)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateDataFrom(frameset, updateRequestIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Frameset getFrameSetForType(
|
||||||
|
final List<Frameset> framesets,
|
||||||
|
final int seekbarPreviewType) {
|
||||||
|
|
||||||
|
if (seekbarPreviewType == SeekbarPreviewThumbnailType.HIGH_QUALITY) {
|
||||||
|
Log.d(TAG, "Strategy for seekbarPreviewData: high quality");
|
||||||
|
return framesets.stream()
|
||||||
|
.max(Comparator.comparingInt(fs -> fs.getFrameHeight() * fs.getFrameWidth()))
|
||||||
|
.orElse(null);
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Strategy for seekbarPreviewData: low quality");
|
||||||
|
return framesets.stream()
|
||||||
|
.min(Comparator.comparingInt(fs -> fs.getFrameHeight() * fs.getFrameWidth()))
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateDataFrom(
|
||||||
|
final Frameset frameset,
|
||||||
|
final UUID updateRequestIdentifier) {
|
||||||
|
|
||||||
|
Log.d(TAG, "Starting generation of seekbarPreviewData");
|
||||||
|
final Stopwatch sw = Log.isLoggable(TAG, Log.DEBUG) ? Stopwatch.createStarted() : null;
|
||||||
|
|
||||||
|
int currentPosMs = 0;
|
||||||
|
int pos = 1;
|
||||||
|
|
||||||
|
final int frameCountPerUrl = frameset.getFramesPerPageX() * frameset.getFramesPerPageY();
|
||||||
|
|
||||||
|
// Process each url in the frameset
|
||||||
|
for (final String url : frameset.getUrls()) {
|
||||||
|
// get the bitmap
|
||||||
|
final Bitmap srcBitMap = getBitMapFrom(url);
|
||||||
|
|
||||||
|
// The data is not added directly to "seekbarPreviewData" due to
|
||||||
|
// concurrency and checks for "updateRequestIdentifier"
|
||||||
|
final Map<Integer, Supplier<Bitmap>> generatedDataForUrl = new HashMap<>();
|
||||||
|
|
||||||
|
// The bitmap consists of several images, which we process here
|
||||||
|
// foreach frame in the returned bitmap
|
||||||
|
for (int i = 0; i < frameCountPerUrl; i++) {
|
||||||
|
// Frames outside the video length are skipped
|
||||||
|
if (pos > frameset.getTotalCount()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the bounds where the frame is found
|
||||||
|
final int[] bounds = frameset.getFrameBoundsAt(currentPosMs);
|
||||||
|
generatedDataForUrl.put(currentPosMs, () -> {
|
||||||
|
// It can happen, that the original bitmap could not be downloaded
|
||||||
|
// In such a case - we don't want a NullPointer - simply return null
|
||||||
|
if (srcBitMap == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cut out the corresponding bitmap form the "srcBitMap"
|
||||||
|
return Bitmap.createBitmap(srcBitMap, bounds[1], bounds[2],
|
||||||
|
frameset.getFrameWidth(), frameset.getFrameHeight());
|
||||||
|
});
|
||||||
|
|
||||||
|
currentPosMs += frameset.getDurationPerFrame();
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we are still the latest request
|
||||||
|
// If not abort method execution
|
||||||
|
if (isRequestIdentifierCurrent(updateRequestIdentifier)) {
|
||||||
|
seekbarPreviewData.putAll(generatedDataForUrl);
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Aborted of generation of seekbarPreviewData");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sw != null) {
|
||||||
|
Log.d(TAG, "Generation of seekbarPreviewData took " + sw.stop().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bitmap getBitMapFrom(final String url) {
|
||||||
|
if (url == null) {
|
||||||
|
Log.w(TAG, "url is null; This should never happen");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Stopwatch sw = Log.isLoggable(TAG, Log.DEBUG) ? Stopwatch.createStarted() : null;
|
||||||
|
try {
|
||||||
|
final SyncImageLoadingListener syncImageLoadingListener =
|
||||||
|
new SyncImageLoadingListener();
|
||||||
|
|
||||||
|
Log.d(TAG, "Downloading bitmap for seekbarPreview from '" + url + "'");
|
||||||
|
|
||||||
|
// Ensure that everything is running
|
||||||
|
ImageLoader.getInstance().resume();
|
||||||
|
// Load the image
|
||||||
|
// Impl-Note:
|
||||||
|
// Ensure that your are not running on the main-Thread this will otherwise hang
|
||||||
|
ImageLoader.getInstance().loadImage(
|
||||||
|
url,
|
||||||
|
ImageDisplayConstants.DISPLAY_SEEKBAR_PREVIEW_OPTIONS,
|
||||||
|
syncImageLoadingListener);
|
||||||
|
|
||||||
|
// Get the bitmap within the timeout
|
||||||
|
final Bitmap bitmap =
|
||||||
|
syncImageLoadingListener.waitForBitmapOrThrow(30, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
if (sw != null) {
|
||||||
|
Log.d(TAG,
|
||||||
|
"Download of bitmap for seekbarPreview from '" + url
|
||||||
|
+ "' took " + sw.stop().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return bitmap;
|
||||||
|
} catch (final Exception ex) {
|
||||||
|
Log.w(TAG,
|
||||||
|
"Failed to get bitmap for seekbarPreview from url='" + url
|
||||||
|
+ "' in time",
|
||||||
|
ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRequestIdentifierCurrent(final UUID requestIdentifier) {
|
||||||
|
return this.currentUpdateRequestIdentifier.equals(requestIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Optional<Bitmap> getBitmapAt(final int positionInMs) {
|
||||||
|
// Check if the BitmapData is empty
|
||||||
|
if (seekbarPreviewData.isEmpty()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the closest frame to the requested position
|
||||||
|
final int closestIndexPosition =
|
||||||
|
seekbarPreviewData.keySet().stream()
|
||||||
|
.min(Comparator.comparingInt(i -> Math.abs(i - positionInMs)))
|
||||||
|
.orElse(-1);
|
||||||
|
|
||||||
|
// this should never happen, because
|
||||||
|
// it indicates that "seekbarPreviewData" is empty which was already checked
|
||||||
|
if (closestIndexPosition == -1) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the bitmap for the position (executes the supplier)
|
||||||
|
return Optional.ofNullable(seekbarPreviewData.get(closestIndexPosition).get());
|
||||||
|
} catch (final Exception ex) {
|
||||||
|
// If there is an error, log it and return Optional.empty
|
||||||
|
Log.w(TAG, "Unable to get seekbar preview", ex);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
package org.schabi.newpipe.player.seekbarpreview;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||||
|
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
||||||
|
|
||||||
|
import java.util.concurrent.CancellationException;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener for synchronously downloading of an image/bitmap.
|
||||||
|
*/
|
||||||
|
public class SyncImageLoadingListener extends SimpleImageLoadingListener {
|
||||||
|
|
||||||
|
private final CountDownLatch countDownLatch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
private Bitmap bitmap;
|
||||||
|
private boolean cancelled = false;
|
||||||
|
private FailReason failReason = null;
|
||||||
|
|
||||||
|
@SuppressWarnings("checkstyle:HiddenField")
|
||||||
|
@Override
|
||||||
|
public void onLoadingFailed(
|
||||||
|
final String imageUri,
|
||||||
|
final View view,
|
||||||
|
final FailReason failReason) {
|
||||||
|
|
||||||
|
this.failReason = failReason;
|
||||||
|
countDownLatch.countDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadingComplete(
|
||||||
|
final String imageUri,
|
||||||
|
final View view,
|
||||||
|
final Bitmap loadedImage) {
|
||||||
|
|
||||||
|
bitmap = loadedImage;
|
||||||
|
countDownLatch.countDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadingCancelled(final String imageUri, final View view) {
|
||||||
|
cancelled = true;
|
||||||
|
countDownLatch.countDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Bitmap waitForBitmapOrThrow(final long timeout, final TimeUnit timeUnit)
|
||||||
|
throws InterruptedException, TimeoutException {
|
||||||
|
|
||||||
|
// Wait for the download to finish
|
||||||
|
if (!countDownLatch.await(timeout, timeUnit)) {
|
||||||
|
throw new TimeoutException("Couldn't get the image in time");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCancelled()) {
|
||||||
|
throw new CancellationException("Download of image was cancelled");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getFailReason() != null) {
|
||||||
|
throw new RuntimeException("Failed to download image" + getFailReason().getType(),
|
||||||
|
getFailReason().getCause());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getBitmap() == null) {
|
||||||
|
throw new NullPointerException("Bitmap is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
return getBitmap();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Bitmap getBitmap() {
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCancelled() {
|
||||||
|
return cancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FailReason getFailReason() {
|
||||||
|
return failReason;
|
||||||
|
}
|
||||||
|
}
|
|
@ -56,5 +56,10 @@ public final class ImageDisplayConstants {
|
||||||
.showImageOnFail(R.drawable.dummy_thumbnail_playlist)
|
.showImageOnFail(R.drawable.dummy_thumbnail_playlist)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
public static final DisplayImageOptions DISPLAY_SEEKBAR_PREVIEW_OPTIONS =
|
||||||
|
new DisplayImageOptions.Builder()
|
||||||
|
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
||||||
|
.build();
|
||||||
|
|
||||||
private ImageDisplayConstants() { }
|
private ImageDisplayConstants() { }
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,8 +103,8 @@
|
||||||
android:padding="@dimen/player_main_buttons_padding"
|
android:padding="@dimen/player_main_buttons_padding"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:tint="@color/white"
|
|
||||||
app:srcCompat="@drawable/ic_close"
|
app:srcCompat="@drawable/ic_close"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="ContentDescription,RtlHardcoded" />
|
tools:ignore="ContentDescription,RtlHardcoded" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
@ -189,8 +189,8 @@
|
||||||
android:paddingBottom="3dp"
|
android:paddingBottom="3dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:tint="@color/white"
|
|
||||||
app:srcCompat="@drawable/ic_list"
|
app:srcCompat="@drawable/ic_list"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="ContentDescription,RtlHardcoded"
|
tools:ignore="ContentDescription,RtlHardcoded"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
@ -208,8 +208,8 @@
|
||||||
android:paddingBottom="3dp"
|
android:paddingBottom="3dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:tint="@color/white"
|
|
||||||
app:srcCompat="@drawable/ic_format_list_numbered"
|
app:srcCompat="@drawable/ic_format_list_numbered"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="ContentDescription,RtlHardcoded"
|
tools:ignore="ContentDescription,RtlHardcoded"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
@ -222,8 +222,8 @@
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:padding="@dimen/player_main_buttons_padding"
|
android:padding="@dimen/player_main_buttons_padding"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
app:tint="@color/white"
|
|
||||||
app:srcCompat="@drawable/ic_expand_more"
|
app:srcCompat="@drawable/ic_expand_more"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="ContentDescription,RtlHardcoded" />
|
tools:ignore="ContentDescription,RtlHardcoded" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -287,8 +287,8 @@
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:padding="@dimen/player_main_buttons_padding"
|
android:padding="@dimen/player_main_buttons_padding"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
app:tint="@color/white"
|
|
||||||
app:srcCompat="@drawable/ic_cast"
|
app:srcCompat="@drawable/ic_cast"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="RtlHardcoded" />
|
tools:ignore="RtlHardcoded" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
|
@ -302,8 +302,8 @@
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:padding="@dimen/player_main_buttons_padding"
|
android:padding="@dimen/player_main_buttons_padding"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
app:tint="@color/white"
|
|
||||||
app:srcCompat="@drawable/ic_language"
|
app:srcCompat="@drawable/ic_language"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="RtlHardcoded" />
|
tools:ignore="RtlHardcoded" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
|
@ -317,8 +317,8 @@
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:padding="@dimen/player_main_buttons_padding"
|
android:padding="@dimen/player_main_buttons_padding"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
app:tint="@color/white"
|
|
||||||
app:srcCompat="@drawable/ic_share"
|
app:srcCompat="@drawable/ic_share"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="RtlHardcoded" />
|
tools:ignore="RtlHardcoded" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
|
@ -331,8 +331,8 @@
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:padding="@dimen/player_main_buttons_padding"
|
android:padding="@dimen/player_main_buttons_padding"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
app:tint="@color/white"
|
|
||||||
app:srcCompat="@drawable/ic_volume_off"
|
app:srcCompat="@drawable/ic_volume_off"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="RtlHardcoded" />
|
tools:ignore="RtlHardcoded" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
|
@ -345,8 +345,8 @@
|
||||||
android:padding="@dimen/player_main_buttons_padding"
|
android:padding="@dimen/player_main_buttons_padding"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:tint="@color/white"
|
|
||||||
app:srcCompat="@drawable/ic_fullscreen"
|
app:srcCompat="@drawable/ic_fullscreen"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="ContentDescription,RtlHardcoded"
|
tools:ignore="ContentDescription,RtlHardcoded"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
@ -354,6 +354,50 @@
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/bottomSeekbarPreviewLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_above="@id/bottomControls"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/seekbarPreviewContainer"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="12dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/currentDisplaySeek"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="#60000000"
|
||||||
|
android:paddingLeft="5dp"
|
||||||
|
android:paddingRight="5dp"
|
||||||
|
android:paddingBottom="2dp"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:ignore="RtlHardcoded"
|
||||||
|
tools:text="1:06:29"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/currentSeekbarPreviewThumbnail"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="2dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:srcCompat="@drawable/dummy_thumbnail"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/bottomControls"
|
android:id="@+id/bottomControls"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -685,24 +729,6 @@
|
||||||
tools:src="@drawable/ic_brightness_high" />
|
tools:src="@drawable/ic_brightness_high" />
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/currentDisplaySeek"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerInParent="true"
|
|
||||||
android:layout_marginBottom="58dp"
|
|
||||||
android:background="#64000000"
|
|
||||||
android:paddingLeft="30dp"
|
|
||||||
android:paddingTop="10dp"
|
|
||||||
android:paddingRight="30dp"
|
|
||||||
android:paddingBottom="10dp"
|
|
||||||
android:textColor="@android:color/white"
|
|
||||||
android:textSize="26sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:ignore="RtlHardcoded"
|
|
||||||
tools:text="1:06:29"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
|
@ -101,8 +101,8 @@
|
||||||
android:padding="@dimen/player_main_buttons_padding"
|
android:padding="@dimen/player_main_buttons_padding"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:tint="@color/white"
|
|
||||||
app:srcCompat="@drawable/ic_close"
|
app:srcCompat="@drawable/ic_close"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="ContentDescription,RtlHardcoded" />
|
tools:ignore="ContentDescription,RtlHardcoded" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
@ -191,8 +191,8 @@
|
||||||
android:paddingBottom="3dp"
|
android:paddingBottom="3dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:tint="@color/white"
|
|
||||||
app:srcCompat="@drawable/ic_list"
|
app:srcCompat="@drawable/ic_list"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="ContentDescription,RtlHardcoded" />
|
tools:ignore="ContentDescription,RtlHardcoded" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
|
@ -209,8 +209,8 @@
|
||||||
android:paddingBottom="3dp"
|
android:paddingBottom="3dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:tint="@color/white"
|
|
||||||
app:srcCompat="@drawable/ic_format_list_numbered"
|
app:srcCompat="@drawable/ic_format_list_numbered"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="ContentDescription,RtlHardcoded" />
|
tools:ignore="ContentDescription,RtlHardcoded" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
|
@ -222,8 +222,8 @@
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:padding="@dimen/player_main_buttons_padding"
|
android:padding="@dimen/player_main_buttons_padding"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
app:tint="@color/white"
|
|
||||||
app:srcCompat="@drawable/ic_expand_more"
|
app:srcCompat="@drawable/ic_expand_more"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="ContentDescription,RtlHardcoded" />
|
tools:ignore="ContentDescription,RtlHardcoded" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -286,8 +286,8 @@
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:padding="@dimen/player_main_buttons_padding"
|
android:padding="@dimen/player_main_buttons_padding"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
app:tint="@color/white"
|
|
||||||
app:srcCompat="@drawable/ic_cast"
|
app:srcCompat="@drawable/ic_cast"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="RtlHardcoded" />
|
tools:ignore="RtlHardcoded" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
|
@ -301,8 +301,8 @@
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:padding="@dimen/player_main_buttons_padding"
|
android:padding="@dimen/player_main_buttons_padding"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
app:tint="@color/white"
|
|
||||||
app:srcCompat="@drawable/ic_language"
|
app:srcCompat="@drawable/ic_language"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="RtlHardcoded" />
|
tools:ignore="RtlHardcoded" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
|
@ -316,8 +316,8 @@
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:padding="@dimen/player_main_buttons_padding"
|
android:padding="@dimen/player_main_buttons_padding"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
app:tint="@color/white"
|
|
||||||
app:srcCompat="@drawable/ic_share"
|
app:srcCompat="@drawable/ic_share"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="RtlHardcoded" />
|
tools:ignore="RtlHardcoded" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
|
@ -330,8 +330,8 @@
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:padding="@dimen/player_main_buttons_padding"
|
android:padding="@dimen/player_main_buttons_padding"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
app:tint="@color/white"
|
|
||||||
app:srcCompat="@drawable/ic_volume_off"
|
app:srcCompat="@drawable/ic_volume_off"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="RtlHardcoded" />
|
tools:ignore="RtlHardcoded" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
|
@ -344,8 +344,8 @@
|
||||||
android:padding="@dimen/player_main_buttons_padding"
|
android:padding="@dimen/player_main_buttons_padding"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:tint="@color/white"
|
|
||||||
app:srcCompat="@drawable/ic_fullscreen"
|
app:srcCompat="@drawable/ic_fullscreen"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="ContentDescription,RtlHardcoded"
|
tools:ignore="ContentDescription,RtlHardcoded"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
@ -353,6 +353,50 @@
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/bottomSeekbarPreviewLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_above="@id/bottomControls"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/seekbarPreviewContainer"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="12dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/currentDisplaySeek"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="#60000000"
|
||||||
|
android:paddingLeft="5dp"
|
||||||
|
android:paddingRight="5dp"
|
||||||
|
android:paddingBottom="2dp"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:ignore="RtlHardcoded"
|
||||||
|
tools:text="1:06:29"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/currentSeekbarPreviewThumbnail"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="2dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:srcCompat="@drawable/dummy_thumbnail"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/bottomControls"
|
android:id="@+id/bottomControls"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -681,24 +725,6 @@
|
||||||
tools:src="@drawable/ic_brightness" />
|
tools:src="@drawable/ic_brightness" />
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/currentDisplaySeek"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerInParent="true"
|
|
||||||
android:layout_marginBottom="58dp"
|
|
||||||
android:background="#64000000"
|
|
||||||
android:paddingLeft="30dp"
|
|
||||||
android:paddingTop="10dp"
|
|
||||||
android:paddingRight="30dp"
|
|
||||||
android:paddingBottom="10dp"
|
|
||||||
android:textColor="@android:color/white"
|
|
||||||
android:textSize="26sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:ignore="RtlHardcoded"
|
|
||||||
tools:text="1:06:29"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
|
@ -422,6 +422,10 @@
|
||||||
<string name="grid">Raster</string>
|
<string name="grid">Raster</string>
|
||||||
<string name="auto">Automatisch</string>
|
<string name="auto">Automatisch</string>
|
||||||
<string name="switch_view">Ansicht wechseln</string>
|
<string name="switch_view">Ansicht wechseln</string>
|
||||||
|
<string name="seekbar_preview_thumbnail_title">Vorschaubild der Suchleiste</string>
|
||||||
|
<string name="high_quality_larger">Hohe Qualität (größer)</string>
|
||||||
|
<string name="low_quality_smaller">Niedrige Qualität (kleiner)</string>
|
||||||
|
<string name="dont_show">Nicht anzeigen</string>
|
||||||
<string name="app_update_notification_content_title">Eine NewPipe-Aktualisierung ist verfügbar!</string>
|
<string name="app_update_notification_content_title">Eine NewPipe-Aktualisierung ist verfügbar!</string>
|
||||||
<string name="app_update_notification_content_text">Zum Herunterladen antippen</string>
|
<string name="app_update_notification_content_text">Zum Herunterladen antippen</string>
|
||||||
<string name="missions_header_finished">Fertig</string>
|
<string name="missions_header_finished">Fertig</string>
|
||||||
|
|
|
@ -89,6 +89,21 @@
|
||||||
<item>@string/never</item>
|
<item>@string/never</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string name="seekbar_preview_thumbnail_key" translatable="false">seekbar_preview_thumbnail_key</string>
|
||||||
|
<string name="seekbar_preview_thumbnail_high_quality" translatable="false">seekbar_preview_thumbnail_high_quality</string>
|
||||||
|
<string name="seekbar_preview_thumbnail_low_quality" translatable="false">seekbar_preview_thumbnail_low_quality</string>
|
||||||
|
<string name="seekbar_preview_thumbnail_none" translatable="false">seekbar_preview_thumbnail_none</string>
|
||||||
|
<string-array name="seekbar_preview_thumbnail_type_key" translatable="false">
|
||||||
|
<item>@string/seekbar_preview_thumbnail_high_quality</item>
|
||||||
|
<item>@string/seekbar_preview_thumbnail_low_quality</item>
|
||||||
|
<item>@string/seekbar_preview_thumbnail_none</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="seekbar_preview_thumbnail_type_description" translatable="false">
|
||||||
|
<item>@string/high_quality_larger</item>
|
||||||
|
<item>@string/low_quality_smaller</item>
|
||||||
|
<item>@string/dont_show</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
<string name="default_resolution_key" translatable="false">default_resolution</string>
|
<string name="default_resolution_key" translatable="false">default_resolution</string>
|
||||||
<string name="default_resolution_value" translatable="false">720p60</string>
|
<string name="default_resolution_value" translatable="false">720p60</string>
|
||||||
<string name="show_higher_resolutions_key" translatable="false">show_higher_resolutions</string>
|
<string name="show_higher_resolutions_key" translatable="false">show_higher_resolutions</string>
|
||||||
|
|
|
@ -587,6 +587,11 @@
|
||||||
<string name="grid">Grid</string>
|
<string name="grid">Grid</string>
|
||||||
<string name="auto">Auto</string>
|
<string name="auto">Auto</string>
|
||||||
<string name="switch_view">Switch View</string>
|
<string name="switch_view">Switch View</string>
|
||||||
|
<!-- Seekbar Preview Thumbnail-->
|
||||||
|
<string name="seekbar_preview_thumbnail_title">Seekbar thumbnail preview</string>
|
||||||
|
<string name="high_quality_larger">High quality (larger)</string>
|
||||||
|
<string name="low_quality_smaller">Low quality (smaller)</string>
|
||||||
|
<string name="dont_show">Don\'t show</string>
|
||||||
<!-- App update notification -->
|
<!-- App update notification -->
|
||||||
<string name="app_update_notification_content_title">NewPipe update is available!</string>
|
<string name="app_update_notification_content_title">NewPipe update is available!</string>
|
||||||
<string name="app_update_notification_content_text">Tap to download</string>
|
<string name="app_update_notification_content_text">Tap to download</string>
|
||||||
|
|
|
@ -79,6 +79,16 @@
|
||||||
android:summary="@string/show_play_with_kodi_summary"
|
android:summary="@string/show_play_with_kodi_summary"
|
||||||
android:title="@string/show_play_with_kodi_title"
|
android:title="@string/show_play_with_kodi_title"
|
||||||
app:iconSpaceReserved="false" />
|
app:iconSpaceReserved="false" />
|
||||||
|
<ListPreference
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:defaultValue="seekbar_preview_thumbnail_high_quality"
|
||||||
|
android:entries="@array/seekbar_preview_thumbnail_type_description"
|
||||||
|
android:entryValues="@array/seekbar_preview_thumbnail_type_key"
|
||||||
|
android:key="@string/seekbar_preview_thumbnail_key"
|
||||||
|
android:summary="%s"
|
||||||
|
android:title="@string/seekbar_preview_thumbnail_title"
|
||||||
|
app:iconSpaceReserved="false" />
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue