diff --git a/README.md b/README.md
index 52c1159dd..b154fad58 100644
--- a/README.md
+++ b/README.md
@@ -77,19 +77,24 @@ The more is done the better it gets!
If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).
## Donate
-If you like NewPipe we'd be happy about a donation. You can either donate via Bitcoin or BountySource. For further information about donating to NewPipe, please visit our [website](https://newpipe.schabi.org/donate/).
+If you like NewPipe we'd be happy about a donation. You can either donate via Bitcoin, Bountysource or Liberapay. For further information about donating to NewPipe, please visit our [website](https://newpipe.schabi.org/donate).
-
-
-
-
16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh
-
-
-
-
-
-
+
+
+
+
16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh
+
+
+
+
+
+
+
+
+
+
+
## License
diff --git a/app/build.gradle b/app/build.gradle
index 814006051..3389fe4c4 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -8,8 +8,8 @@ android {
applicationId "org.schabi.newpipe"
minSdkVersion 15
targetSdkVersion 27
- versionCode 48
- versionName "0.12.0"
+ versionCode 49
+ versionName "0.13.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
@@ -48,14 +48,20 @@ android {
}
ext {
- supportLibVersion = '27.0.2'
+ supportLibVersion = '27.1.0'
+ exoPlayerLibVersion = '2.7.1'
+ roomDbLibVersion = '1.0.0'
+ leakCanaryLibVersion = '1.5.4'
+ okHttpLibVersion = '1.5.0'
+ icepickLibVersion = '3.2.0'
+ stethoLibVersion = '1.5.0'
}
dependencies {
androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') {
exclude module: 'support-annotations'
}
- implementation 'com.github.TeamNewPipe:NewPipeExtractor:b1130629bb'
+ implementation 'com.github.TeamNewPipe:NewPipeExtractor:f787b375e5fb6d'
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:1.10.19'
@@ -73,27 +79,28 @@ dependencies {
implementation 'de.hdodenhof:circleimageview:2.2.0'
implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1'
implementation 'com.nononsenseapps:filepicker:4.2.1'
- implementation 'com.google.android.exoplayer:exoplayer:2.7.0'
+ implementation "com.google.android.exoplayer:exoplayer:$exoPlayerLibVersion"
+ implementation "com.google.android.exoplayer:extension-mediasession:$exoPlayerLibVersion"
- debugImplementation 'com.facebook.stetho:stetho:1.5.0'
- debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0'
- debugImplementation 'com.android.support:multidex:1.0.2'
+ debugImplementation "com.facebook.stetho:stetho:$stethoLibVersion"
+ debugImplementation "com.facebook.stetho:stetho-urlconnection:$stethoLibVersion"
+ debugImplementation 'com.android.support:multidex:1.0.3'
- implementation 'io.reactivex.rxjava2:rxjava:2.1.7'
- implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
- implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
+ implementation 'io.reactivex.rxjava2:rxjava:2.1.10'
+ implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
+ implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
- implementation 'android.arch.persistence.room:runtime:1.0.0'
- implementation 'android.arch.persistence.room:rxjava2:1.0.0'
- annotationProcessor 'android.arch.persistence.room:compiler:1.0.0'
+ implementation "android.arch.persistence.room:runtime:$roomDbLibVersion"
+ implementation "android.arch.persistence.room:rxjava2:$roomDbLibVersion"
+ annotationProcessor "android.arch.persistence.room:compiler:$roomDbLibVersion"
- implementation 'frankiesardo:icepick:3.2.0'
- annotationProcessor 'frankiesardo:icepick-processor:3.2.0'
+ implementation "frankiesardo:icepick:$icepickLibVersion"
+ annotationProcessor "frankiesardo:icepick-processor:$icepickLibVersion"
- debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
- betaImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
- releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
+ debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryLibVersion"
+ betaImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion"
+ releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion"
implementation 'com.squareup.okhttp3:okhttp:3.9.1'
- debugImplementation 'com.facebook.stetho:stetho-okhttp3:1.5.0'
+ debugImplementation "com.facebook.stetho:stetho-okhttp3:$okHttpLibVersion"
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1be8c1f2c..1edd67d24 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -28,6 +28,12 @@
+
+
+
+
+
+
= INITIAL_RELATED_VIDEOS
? INITIAL_RELATED_VIDEOS
@@ -683,7 +682,7 @@ public class VideoDetailFragment
switch (id) {
case R.id.menu_item_share: {
if(currentInfo != null) {
- shareUrl(currentInfo.name, url);
+ shareUrl(currentInfo.getName(), url);
} else {
shareUrl(url, url);
}
@@ -1210,7 +1209,8 @@ public class VideoDetailFragment
spinnerToolbar.setVisibility(View.GONE);
break;
default:
- if (!info.video_streams.isEmpty() || !info.video_only_streams.isEmpty()) break;
+ if (!info.getVideoStreams().isEmpty()
+ || !info.getVideoOnlyStreams().isEmpty()) break;
detailControlsBackground.setVisibility(View.GONE);
detailControlsPopup.setVisibility(View.GONE);
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java
index 34f190032..a132213bf 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java
@@ -20,7 +20,7 @@ import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
public abstract class BaseListInfoFragment
- extends BaseListFragment {
+ extends BaseListFragment {
@State
protected int serviceId = Constants.NO_SERVICE_ID;
@@ -117,7 +117,7 @@ public abstract class BaseListInfoFragment
.subscribe((@NonNull I result) -> {
isLoading.set(false);
currentInfo = result;
- currentNextPageUrl = result.next_streams_url;
+ currentNextPageUrl = result.getNextPageUrl();
handleResult(result);
}, (@NonNull Throwable throwable) -> onError(throwable));
}
@@ -126,7 +126,7 @@ public abstract class BaseListInfoFragment
* Implement the logic to load more items
* You can use the default implementations from {@link org.schabi.newpipe.util.ExtractorHelper}
*/
- protected abstract Single loadMoreItemsLogic();
+ protected abstract Single loadMoreItemsLogic();
protected void loadMoreItems() {
isLoading.set(true);
@@ -135,9 +135,9 @@ public abstract class BaseListInfoFragment
currentWorker = loadMoreItemsLogic()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
- .subscribe((@io.reactivex.annotations.NonNull ListExtractor.InfoItemPage InfoItemPage) -> {
+ .subscribe((@io.reactivex.annotations.NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> {
isLoading.set(false);
- handleNextItems(InfoItemPage);
+ handleNextItems(InfoItemsPage);
}, (@io.reactivex.annotations.NonNull Throwable throwable) -> {
isLoading.set(false);
onError(throwable);
@@ -145,10 +145,10 @@ public abstract class BaseListInfoFragment
}
@Override
- public void handleNextItems(ListExtractor.InfoItemPage result) {
+ public void handleNextItems(ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
- currentNextPageUrl = result.nextPageUrl;
- infoListAdapter.addInfoItemList(result.infoItemList);
+ currentNextPageUrl = result.getNextPageUrl();
+ infoListAdapter.addInfoItemList(result.getItems());
showListFooter(hasMoreItems());
}
@@ -171,8 +171,8 @@ public abstract class BaseListInfoFragment
setTitle(name);
if (infoListAdapter.getItemsList().size() == 0) {
- if (result.related_streams.size() > 0) {
- infoListAdapter.addInfoItemList(result.related_streams);
+ if (result.getRelatedItems().size() > 0) {
+ infoListAdapter.addInfoItemList(result.getRelatedItems());
showListFooter(hasMoreItems());
} else {
infoListAdapter.clearStreamItemList();
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java
index 3261e6dad..dbc61961e 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java
@@ -27,10 +27,13 @@ import com.jakewharton.rxbinding2.view.RxView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
+import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
+import org.schabi.newpipe.extractor.stream.Stream;
+import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.info_list.InfoItemDialog;
@@ -41,9 +44,11 @@ import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.subscription.SubscriptionService;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ExtractorHelper;
+import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
+import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -388,7 +393,7 @@ public class ChannelFragment extends BaseListInfoFragment {
//////////////////////////////////////////////////////////////////////////*/
@Override
- protected Single loadMoreItemsLogic() {
+ protected Single loadMoreItemsLogic() {
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPageUrl);
}
@@ -415,8 +420,10 @@ public class ChannelFragment extends BaseListInfoFragment {
super.handleResult(result);
headerRootLayout.setVisibility(View.VISIBLE);
- imageLoader.displayImage(result.banner_url, headerChannelBanner, DISPLAY_BANNER_OPTIONS);
- imageLoader.displayImage(result.avatar_url, headerAvatarView, DISPLAY_AVATAR_OPTIONS);
+ imageLoader.displayImage(result.getBannerUrl(), headerChannelBanner,
+ ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
+ imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView,
+ ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
if (result.getSubscriberCount() != -1) {
headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount()));
@@ -427,8 +434,8 @@ public class ChannelFragment extends BaseListInfoFragment {
playlistCtrl.setVisibility(View.VISIBLE);
- if (!result.errors.isEmpty()) {
- showSnackBarError(result.errors, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
+ if (!result.getErrors().isEmpty()) {
+ showSnackBarError(result.getErrors(), UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}
if (disposables != null) disposables.clear();
@@ -436,24 +443,12 @@ public class ChannelFragment extends BaseListInfoFragment {
updateSubscription(result);
monitorSubscription(result);
- headerPlayAllButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- NavigationHelper.playOnMainPlayer(activity, getPlayQueue());
- }
- });
- headerPopupButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- NavigationHelper.playOnPopupPlayer(activity, getPlayQueue());
- }
- });
- headerBackgroundButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue());
- }
- });
+ headerPlayAllButton.setOnClickListener(
+ view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
+ headerPopupButton.setOnClickListener(
+ view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
+ headerBackgroundButton.setOnClickListener(
+ view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
}
private PlayQueue getPlayQueue() {
@@ -461,17 +456,23 @@ public class ChannelFragment extends BaseListInfoFragment {
}
private PlayQueue getPlayQueue(final int index) {
+ final List streamItems = new ArrayList<>();
+ for(InfoItem i : infoListAdapter.getItemsList()) {
+ if(i instanceof StreamInfoItem) {
+ streamItems.add((StreamInfoItem) i);
+ }
+ }
return new ChannelPlayQueue(
currentInfo.getServiceId(),
currentInfo.getUrl(),
currentInfo.getNextPageUrl(),
- infoListAdapter.getItemsList(),
+ streamItems,
index
);
}
@Override
- public void handleNextItems(ListExtractor.InfoItemPage result) {
+ public void handleNextItems(ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/feed/FeedFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/feed/FeedFragment.java
index 57841cb87..dabfd9e1b 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/feed/FeedFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/feed/FeedFragment.java
@@ -297,12 +297,12 @@ public class FeedFragment extends BaseListFragment, Voi
// Called only when response is non-empty
@Override
public void onSuccess(final ChannelInfo channelInfo) {
- if (infoListAdapter == null || channelInfo.getRelatedStreams().isEmpty()) {
+ if (infoListAdapter == null || channelInfo.getRelatedItems().isEmpty()) {
onDone();
return;
}
- final InfoItem item = channelInfo.getRelatedStreams().get(0);
+ final InfoItem item = channelInfo.getRelatedItems().get(0);
// Keep requesting new items if the current one already exists
boolean itemExists = doesItemExist(infoListAdapter.getItemsList(), item);
if (!itemExists) {
@@ -411,7 +411,7 @@ public class FeedFragment extends BaseListFragment, Voi
private boolean doesItemExist(final List items, final InfoItem item) {
for (final InfoItem existingItem : items) {
- if (existingItem.info_type == item.info_type &&
+ if (existingItem.getInfoType() == item.getInfoType() &&
existingItem.getServiceId() == item.getServiceId() &&
existingItem.getName().equals(item.getName()) &&
existingItem.getUrl().equals(item.getUrl())) return true;
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java
index 976bcced2..482f71cb4 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java
@@ -141,7 +141,7 @@ public class KioskFragment extends BaseListInfoFragment {
}
@Override
- public Single loadMoreItemsLogic() {
+ public Single loadMoreItemsLogic() {
String contentCountry = PreferenceManager
.getDefaultSharedPreferences(activity)
.getString(getString(R.string.content_country_key),
@@ -174,7 +174,7 @@ public class KioskFragment extends BaseListInfoFragment {
}
@Override
- public void handleNextItems(ListExtractor.InfoItemPage result) {
+ public void handleNextItems(ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java
index 324d3d7ef..3bcf9d322 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java
@@ -22,10 +22,12 @@ import org.reactivestreams.Subscription;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
+import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
+import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.fragments.local.RemotePlaylistManager;
@@ -35,9 +37,11 @@ import org.schabi.newpipe.playlist.PlaylistPlayQueue;
import org.schabi.newpipe.playlist.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;
+import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;
+import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -206,7 +210,7 @@ public class PlaylistFragment extends BaseListInfoFragment {
//////////////////////////////////////////////////////////////////////////*/
@Override
- protected Single loadMoreItemsLogic() {
+ protected Single loadMoreItemsLogic() {
return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPageUrl);
}
@@ -268,8 +272,10 @@ public class PlaylistFragment extends BaseListInfoFragment {
playlistCtrl.setVisibility(View.VISIBLE);
- imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar, DISPLAY_AVATAR_OPTIONS);
- headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos, (int) result.stream_count, (int) result.stream_count));
+ imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar,
+ ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
+ headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos,
+ (int) result.getStreamCount(), (int) result.getStreamCount()));
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
@@ -297,17 +303,23 @@ public class PlaylistFragment extends BaseListInfoFragment {
}
private PlayQueue getPlayQueue(final int index) {
+ final List infoItems = new ArrayList<>();
+ for(InfoItem i : infoListAdapter.getItemsList()) {
+ if(i instanceof StreamInfoItem) {
+ infoItems.add((StreamInfoItem) i);
+ }
+ }
return new PlaylistPlayQueue(
currentInfo.getServiceId(),
currentInfo.getUrl(),
currentInfo.getNextPageUrl(),
- infoListAdapter.getItemsList(),
+ infoItems,
index
);
}
@Override
- public void handleNextItems(ListExtractor.InfoItemPage result) {
+ public void handleNextItems(ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java
index 1ad31d06c..f7831e02d 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java
@@ -71,7 +71,9 @@ import io.reactivex.subjects.PublishSubject;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
-public class SearchFragment extends BaseListFragment implements BackPressable {
+public class SearchFragment
+ extends BaseListFragment
+ implements BackPressable {
/*//////////////////////////////////////////////////////////////////////////
// Search
@@ -759,12 +761,7 @@ public class SearchFragment extends BaseListFragment suggestions) {
if (DEBUG) Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
suggestionsRecyclerView.smoothScrollToPosition(0);
- suggestionsRecyclerView.post(new Runnable() {
- @Override
- public void run() {
- suggestionListAdapter.setItems(suggestions);
- }
- });
+ suggestionsRecyclerView.post(() -> suggestionListAdapter.setItems(suggestions));
if (errorPanelRoot.getVisibility() == View.VISIBLE) {
hideLoading();
@@ -822,10 +819,10 @@ public class SearchFragment extends BaseListFragment extends BaseStateFragment
@Override
public void showListFooter(final boolean show) {
- itemsList.post(() -> itemListAdapter.showFooter(show));
+ if (itemsList == null) return;
+ itemsList.post(() -> {
+ if (itemListAdapter != null) itemListAdapter.showFooter(show);
+ });
}
@Override
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java
index 21aceade8..b740cb15e 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java
@@ -79,7 +79,9 @@ public final class BookmarkFragment
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
- if (isVisibleToUser) setTitle(getString(R.string.tab_bookmarks));
+ if (activity != null && isVisibleToUser) {
+ setTitle(activity.getString(R.string.tab_bookmarks));
+ }
}
///////////////////////////////////////////////////////////////////////////
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalItemHolder.java
index e4087d8a8..2dffdbfdb 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalItemHolder.java
@@ -1,14 +1,8 @@
package org.schabi.newpipe.fragments.local.holder;
-import android.graphics.Bitmap;
-import android.support.annotation.DimenRes;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.ViewGroup;
-import android.widget.ImageView;
-
-import com.nostra13.universalimageloader.core.DisplayImageOptions;
-import com.nostra13.universalimageloader.core.process.BitmapProcessor;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
@@ -45,19 +39,4 @@ public abstract class LocalItemHolder extends RecyclerView.ViewHolder {
}
public abstract void updateFromItem(final LocalItem item, final DateFormat dateFormat);
-
- /*//////////////////////////////////////////////////////////////////////////
- // ImageLoaderOptions
- //////////////////////////////////////////////////////////////////////////*/
-
- /**
- * Base display options
- */
- public static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS =
- new DisplayImageOptions.Builder()
- .cacheInMemory(true)
- .cacheOnDisk(true)
- .bitmapConfig(Bitmap.Config.RGB_565)
- .resetViewBeforeLoading(false)
- .build();
}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java
index 1fbea6cc4..d9eb7caa5 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java
@@ -2,15 +2,11 @@ package org.schabi.newpipe.fragments.local.holder;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.TextView;
-import com.nostra13.universalimageloader.core.DisplayImageOptions;
-
-import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
+import org.schabi.newpipe.util.ImageDisplayConstants;
import java.text.DateFormat;
@@ -29,7 +25,8 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
itemStreamCountView.setText(String.valueOf(item.streamCount));
itemUploaderView.setVisibility(View.INVISIBLE);
- itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS);
+ itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
+ ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
super.updateFromItem(localItem, dateFormat);
}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java
index 0696f5f61..5f9555d9f 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java
@@ -1,6 +1,5 @@
package org.schabi.newpipe.fragments.local.holder;
-import android.graphics.Bitmap;
import android.support.v4.content.ContextCompat;
import android.view.MotionEvent;
import android.view.View;
@@ -8,14 +7,12 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
-import com.nostra13.universalimageloader.core.DisplayImageOptions;
-import com.nostra13.universalimageloader.core.assist.ImageScaleType;
-
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
+import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import java.text.DateFormat;
@@ -61,7 +58,8 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
}
// Default thumbnail is shown on error, while loading and if the url is empty
- itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS);
+ itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
+ ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) {
@@ -92,15 +90,4 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
return false;
};
}
-
- /**
- * Display options for stream thumbnails
- */
- private static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
- new DisplayImageOptions.Builder()
- .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
- .showImageOnFail(R.drawable.dummy_thumbnail)
- .showImageForEmptyUri(R.drawable.dummy_thumbnail)
- .showImageOnLoading(R.drawable.dummy_thumbnail)
- .build();
}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java
index cd0630b37..199158672 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java
@@ -6,13 +6,12 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
-import com.nostra13.universalimageloader.core.DisplayImageOptions;
-
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
+import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import java.text.DateFormat;
@@ -84,7 +83,8 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateFormat));
// Default thumbnail is shown on error, while loading and if the url is empty
- itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS);
+ itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
+ ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) {
@@ -100,15 +100,4 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
return true;
});
}
-
- /**
- * Display options for stream thumbnails
- */
- public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
- new DisplayImageOptions.Builder()
- .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
- .showImageOnFail(R.drawable.dummy_thumbnail)
- .showImageForEmptyUri(R.drawable.dummy_thumbnail)
- .showImageOnLoading(R.drawable.dummy_thumbnail)
- .build();
}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/PlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/PlaylistItemHolder.java
index bab76ddcb..57bc2a3cb 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/PlaylistItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/PlaylistItemHolder.java
@@ -4,8 +4,6 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
-import com.nostra13.universalimageloader.core.DisplayImageOptions;
-
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
@@ -48,15 +46,4 @@ public abstract class PlaylistItemHolder extends LocalItemHolder {
return true;
});
}
-
- /**
- * Display options for playlist thumbnails
- */
- public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
- new DisplayImageOptions.Builder()
- .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
- .showImageOnLoading(R.drawable.dummy_thumbnail_playlist)
- .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist)
- .showImageOnFail(R.drawable.dummy_thumbnail_playlist)
- .build();
}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/RemotePlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/RemotePlaylistItemHolder.java
index 0f7b00e6d..871138464 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/RemotePlaylistItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/RemotePlaylistItemHolder.java
@@ -6,6 +6,7 @@ import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
+import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import java.text.DateFormat;
@@ -26,7 +27,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
NewPipe.getNameOfService(item.getServiceId())));
itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView,
- DISPLAY_THUMBNAIL_OPTIONS);
+ ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
super.updateFromItem(localItem, dateFormat);
}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java
index 1e69732b7..610c74a01 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java
@@ -104,8 +104,8 @@ public class SubscriptionFragment extends BaseStateFragment items = new ArrayList<>();
for (final SubscriptionEntity subscription : subscriptions) items.add(subscription.toChannelInfoItem());
- Collections.sort(items, new Comparator() {
- @Override
- public int compare(InfoItem o1, InfoItem o2) {
- return o1.name.compareToIgnoreCase(o2.name);
- }
- });
+ Collections.sort(items,
+ (InfoItem o1, InfoItem o2) -> o1.getName().compareToIgnoreCase(o2.getName()));
return items;
}
diff --git a/app/src/main/java/org/schabi/newpipe/history/WatchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/WatchHistoryFragment.java
index 4830ed33b..4fe2b701d 100644
--- a/app/src/main/java/org/schabi/newpipe/history/WatchHistoryFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/history/WatchHistoryFragment.java
@@ -20,6 +20,7 @@ import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
+import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
@@ -147,7 +148,7 @@ public class WatchHistoryFragment extends HistoryFragment {
holder.uploader.setText(entry.uploader);
holder.duration.setText(Localization.getDurationString(entry.duration));
ImageLoader.getInstance().displayImage(entry.thumbnailUrl, holder.thumbnailView,
- StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS);
+ ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java
index 218895983..78867c81f 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java
@@ -60,7 +60,7 @@ public class InfoItemBuilder {
}
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, boolean useMiniVariant) {
- InfoItemHolder holder = holderFromInfoType(parent, infoItem.info_type, useMiniVariant);
+ InfoItemHolder holder = holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
holder.updateFromItem(infoItem);
return holder.itemView;
}
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java
index 4b9914397..9b3405484 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java
@@ -203,7 +203,7 @@ public class InfoListAdapter extends RecyclerView.Adapter= 0) {
- String formattedVideoAmount = Localization.localizeStreamCount(itemBuilder.getContext(), item.stream_count);
+ if (item.getStreamCount() >= 0) {
+ String formattedVideoAmount = Localization.localizeStreamCount(itemBuilder.getContext(),
+ item.getStreamCount());
if (!details.isEmpty()) {
details += " • " + formattedVideoAmount;
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java
index 48fb18517..643886da8 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java
@@ -1,15 +1,13 @@
package org.schabi.newpipe.info_list.holder;
-import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
-import com.nostra13.universalimageloader.core.DisplayImageOptions;
-
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
+import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import de.hdodenhof.circleimageview.CircleImageView;
@@ -40,34 +38,23 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
itemAdditionalDetailView.setText(getDetailLine(item));
itemBuilder.getImageLoader()
- .displayImage(item.thumbnail_url, itemThumbnailView, ChannelInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS);
+ .displayImage(item.getThumbnailUrl(),
+ itemThumbnailView,
+ ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
- itemView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- if (itemBuilder.getOnChannelSelectedListener() != null) {
- itemBuilder.getOnChannelSelectedListener().selected(item);
- }
+ itemView.setOnClickListener(view -> {
+ if (itemBuilder.getOnChannelSelectedListener() != null) {
+ itemBuilder.getOnChannelSelectedListener().selected(item);
}
});
}
protected String getDetailLine(final ChannelInfoItem item) {
String details = "";
- if (item.subscriber_count >= 0) {
- details += Localization.shortSubscriberCount(itemBuilder.getContext(), item.subscriber_count);
+ if (item.getSubscriberCount() >= 0) {
+ details += Localization.shortSubscriberCount(itemBuilder.getContext(),
+ item.getSubscriberCount());
}
return details;
}
-
- /**
- * Display options for channel thumbnails
- */
- public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
- new DisplayImageOptions.Builder()
- .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
- .showImageOnLoading(R.drawable.buddy_channel_item)
- .showImageForEmptyUri(R.drawable.buddy_channel_item)
- .showImageOnFail(R.drawable.buddy_channel_item)
- .build();
}
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java
index fb5aa2b7c..ebb5b4114 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java
@@ -4,8 +4,6 @@ import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.ViewGroup;
-import com.nostra13.universalimageloader.core.DisplayImageOptions;
-
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
@@ -38,16 +36,4 @@ public abstract class InfoItemHolder extends RecyclerView.ViewHolder {
}
public abstract void updateFromItem(final InfoItem infoItem);
-
- /*//////////////////////////////////////////////////////////////////////////
- // ImageLoaderOptions
- //////////////////////////////////////////////////////////////////////////*/
-
- /**
- * Base display options
- */
- public static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS =
- new DisplayImageOptions.Builder()
- .cacheInMemory(true)
- .build();
}
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java
index 50b551c61..b6bd2f389 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java
@@ -4,12 +4,11 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
-import com.nostra13.universalimageloader.core.DisplayImageOptions;
-
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
+import org.schabi.newpipe.util.ImageDisplayConstants;
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
public final ImageView itemThumbnailView;
@@ -40,7 +39,8 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
itemUploaderView.setText(item.getUploaderName());
itemBuilder.getImageLoader()
- .displayImage(item.thumbnail_url, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS);
+ .displayImage(item.getThumbnailUrl(), itemThumbnailView,
+ ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
@@ -56,15 +56,4 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
return true;
});
}
-
- /**
- * Display options for playlist thumbnails
- */
- public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
- new DisplayImageOptions.Builder()
- .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
- .showImageOnLoading(R.drawable.dummy_thumbnail_playlist)
- .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist)
- .showImageOnFail(R.drawable.dummy_thumbnail_playlist)
- .build();
}
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java
index 78954a2ee..0a7705427 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java
@@ -51,14 +51,14 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
private String getStreamInfoDetailLine(final StreamInfoItem infoItem) {
String viewsAndDate = "";
- if (infoItem.view_count >= 0) {
- viewsAndDate = Localization.shortViewCount(itemBuilder.getContext(), infoItem.view_count);
+ if (infoItem.getViewCount() >= 0) {
+ viewsAndDate = Localization.shortViewCount(itemBuilder.getContext(), infoItem.getViewCount());
}
- if (!TextUtils.isEmpty(infoItem.upload_date)) {
+ if (!TextUtils.isEmpty(infoItem.getUploadDate())) {
if (viewsAndDate.isEmpty()) {
- viewsAndDate = infoItem.upload_date;
+ viewsAndDate = infoItem.getUploadDate();
} else {
- viewsAndDate += " • " + infoItem.upload_date;
+ viewsAndDate += " • " + infoItem.getUploadDate();
}
}
return viewsAndDate;
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java
index 594a85582..048b907af 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java
@@ -6,13 +6,12 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
-import com.nostra13.universalimageloader.core.DisplayImageOptions;
-
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.InfoItemBuilder;
+import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
public class StreamMiniInfoItemHolder extends InfoItemHolder {
@@ -41,15 +40,17 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
final StreamInfoItem item = (StreamInfoItem) infoItem;
itemVideoTitleView.setText(item.getName());
- itemUploaderView.setText(item.uploader_name);
+ itemUploaderView.setText(item.getUploaderName());
- if (item.duration > 0) {
- itemDurationView.setText(Localization.getDurationString(item.duration));
- itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.duration_background_color));
+ if (item.getDuration() > 0) {
+ itemDurationView.setText(Localization.getDurationString(item.getDuration()));
+ itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
+ R.color.duration_background_color));
itemDurationView.setVisibility(View.VISIBLE);
- } else if (item.stream_type == StreamType.LIVE_STREAM) {
+ } else if (item.getStreamType() == StreamType.LIVE_STREAM) {
itemDurationView.setText(R.string.duration_live);
- itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.live_duration_background_color));
+ itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
+ R.color.live_duration_background_color));
itemDurationView.setVisibility(View.VISIBLE);
} else {
itemDurationView.setVisibility(View.GONE);
@@ -57,7 +58,9 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
// Default thumbnail is shown on error, while loading and if the url is empty
itemBuilder.getImageLoader()
- .displayImage(item.thumbnail_url, itemThumbnailView, StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS);
+ .displayImage(item.getThumbnailUrl(),
+ itemThumbnailView,
+ ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnStreamSelectedListener() != null) {
@@ -65,7 +68,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
}
});
- switch (item.stream_type) {
+ switch (item.getStreamType()) {
case AUDIO_STREAM:
case VIDEO_STREAM:
case LIVE_STREAM:
@@ -94,15 +97,4 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
itemView.setLongClickable(false);
itemView.setOnLongClickListener(null);
}
-
- /**
- * Display options for stream thumbnails
- */
- public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
- new DisplayImageOptions.Builder()
- .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
- .showImageOnFail(R.drawable.dummy_thumbnail)
- .showImageForEmptyUri(R.drawable.dummy_thumbnail)
- .showImageOnLoading(R.drawable.dummy_thumbnail)
- .build();
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java
index a43f434ff..ac070fb44 100644
--- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java
@@ -77,6 +77,7 @@ public final class BackgroundPlayer extends Service {
private BasePlayerImpl basePlayerImpl;
private LockManager lockManager;
+
/*//////////////////////////////////////////////////////////////////////////
// Service-Activity Binder
//////////////////////////////////////////////////////////////////////////*/
@@ -397,10 +398,10 @@ public final class BackgroundPlayer extends Service {
final MediaSource liveSource = super.sourceOf(item, info);
if (liveSource != null) return liveSource;
- final int index = ListHelper.getDefaultAudioFormat(context, info.audio_streams);
- if (index < 0 || index >= info.audio_streams.size()) return null;
+ final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams());
+ if (index < 0 || index >= info.getAudioStreams().size()) return null;
- final AudioStream audio = info.audio_streams.get(index);
+ final AudioStream audio = info.getAudioStreams().get(index);
return buildMediaSource(audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio),
MediaFormat.getSuffixById(audio.getFormatId()));
}
@@ -485,7 +486,7 @@ public final class BackgroundPlayer extends Service {
onClose();
break;
case ACTION_PLAY_PAUSE:
- onVideoPlayPause();
+ onPlayPause();
break;
case ACTION_REPEAT:
onRepeatClicked();
diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
index d5ba7bb86..cd1451d37 100644
--- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
@@ -57,11 +57,14 @@ import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import org.schabi.newpipe.Downloader;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfo;
+import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.history.HistoryRecordManager;
import org.schabi.newpipe.player.helper.AudioReactor;
import org.schabi.newpipe.player.helper.LoadController;
+import org.schabi.newpipe.player.helper.MediaSessionManager;
import org.schabi.newpipe.player.helper.PlayerDataSource;
import org.schabi.newpipe.player.helper.PlayerHelper;
+import org.schabi.newpipe.player.playback.BasePlayerMediaSession;
import org.schabi.newpipe.player.playback.CustomTrackSelector;
import org.schabi.newpipe.player.playback.MediaSourceManager;
import org.schabi.newpipe.player.playback.PlaybackListener;
@@ -147,8 +150,10 @@ public abstract class BasePlayer implements
protected SimpleExoPlayer simpleExoPlayer;
protected AudioReactor audioReactor;
+ protected MediaSessionManager mediaSessionManager;
- protected boolean isPrepared = false;
+ private boolean isPrepared = false;
+ private boolean isSynchronizing = false;
protected Disposable progressUpdateReactor;
protected CompositeDisposable databaseUpdateReactor;
@@ -193,11 +198,13 @@ public abstract class BasePlayer implements
final LoadControl loadControl = new LoadController(context);
final RenderersFactory renderFactory = new DefaultRenderersFactory(context);
simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl);
- audioReactor = new AudioReactor(context, simpleExoPlayer);
-
simpleExoPlayer.addListener(this);
simpleExoPlayer.setPlayWhenReady(true);
simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context));
+
+ audioReactor = new AudioReactor(context, simpleExoPlayer);
+ mediaSessionManager = new MediaSessionManager(context, simpleExoPlayer,
+ new BasePlayerMediaSession(this));
}
public void initListeners() {}
@@ -244,6 +251,7 @@ public abstract class BasePlayer implements
playQueue = queue;
playQueue.init();
+ if (playbackManager != null) playbackManager.dispose();
playbackManager = new MediaSourceManager(this, playQueue);
if (playQueueAdapter != null) playQueueAdapter.dispose();
@@ -259,8 +267,8 @@ public abstract class BasePlayer implements
}
if (isProgressLoopRunning()) stopProgressLoop();
if (playQueue != null) playQueue.dispose();
+ if (audioReactor != null) audioReactor.dispose();
if (playbackManager != null) playbackManager.dispose();
- if (audioReactor != null) audioReactor.abandonAudioFocus();
if (databaseUpdateReactor != null) databaseUpdateReactor.dispose();
if (playQueueAdapter != null) {
@@ -272,11 +280,11 @@ public abstract class BasePlayer implements
public void destroy() {
if (DEBUG) Log.d(TAG, "destroy() called");
destroyPlayer();
- clearThumbnailCache();
unregisterBroadcastReceiver();
trackSelector = null;
simpleExoPlayer = null;
+ mediaSessionManager = null;
}
/*//////////////////////////////////////////////////////////////////////////
@@ -314,11 +322,6 @@ public abstract class BasePlayer implements
if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " +
"imageUri = [" + imageUri + "], view = [" + view + "]");
}
-
- protected void clearThumbnailCache() {
- ImageLoader.getInstance().clearMemoryCache();
- }
-
/*//////////////////////////////////////////////////////////////////////////
// MediaSource Building
//////////////////////////////////////////////////////////////////////////*/
@@ -389,7 +392,7 @@ public abstract class BasePlayer implements
if (intent == null || intent.getAction() == null) return;
switch (intent.getAction()) {
case AudioManager.ACTION_AUDIO_BECOMING_NOISY:
- if (isPlaying()) onVideoPlayPause();
+ onPause();
break;
}
}
@@ -406,6 +409,7 @@ public abstract class BasePlayer implements
// States Implementation
//////////////////////////////////////////////////////////////////////////*/
+ public static final int STATE_PREFLIGHT = -1;
public static final int STATE_BLOCKED = 123;
public static final int STATE_PLAYING = 124;
public static final int STATE_BUFFERING = 125;
@@ -413,7 +417,7 @@ public abstract class BasePlayer implements
public static final int STATE_PAUSED_SEEK = 127;
public static final int STATE_COMPLETED = 128;
- protected int currentState = -1;
+ protected int currentState = STATE_PREFLIGHT;
public void changeState(int state) {
if (DEBUG) Log.d(TAG, "changeState() called with: state = [" + state + "]");
@@ -448,7 +452,6 @@ public abstract class BasePlayer implements
public void onPlaying() {
if (DEBUG) Log.d(TAG, "onPlaying() called");
if (!isProgressLoopRunning()) startProgressLoop();
- if (!isCurrentWindowValid()) seekToDefault();
}
public void onBuffering() {}
@@ -522,11 +525,9 @@ public abstract class BasePlayer implements
);
}
-
private Disposable getProgressReactor() {
return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
- .filter(ignored -> isProgressLoopRunning())
.subscribe(ignored -> triggerProgressUpdate());
}
@@ -541,16 +542,21 @@ public abstract class BasePlayer implements
(manifest == null ? "no manifest" : "available manifest") + ", " +
"timeline size = [" + timeline.getWindowCount() + "], " +
"reason = [" + reason + "]");
+ if (playQueue == null) return;
switch (reason) {
case Player.TIMELINE_CHANGE_REASON_RESET: // called after #block
case Player.TIMELINE_CHANGE_REASON_PREPARED: // called after #unblock
case Player.TIMELINE_CHANGE_REASON_DYNAMIC: // called after playlist changes
- if (playQueue != null && playbackManager != null &&
- // ensures MediaSourceManager#update is complete
- timeline.getWindowCount() == playQueue.size()) {
- playbackManager.load();
+ // Ensures MediaSourceManager#update is complete
+ final boolean isPlaylistStable = timeline.getWindowCount() == playQueue.size();
+ // Ensure dynamic/livestream timeline changes does not cause negative position
+ if (isPlaylistStable && !isCurrentWindowValid() && !isSynchronizing) {
+ if (DEBUG) Log.d(TAG, "Playback - negative time position reached, " +
+ "clamping position to 0ms.");
+ seekTo(/*clampToTime=*/0);
}
+ break;
}
}
@@ -600,49 +606,54 @@ public abstract class BasePlayer implements
}
break;
case Player.STATE_READY: //3
- maybeRecover();
+ maybeCorrectSeekPosition();
if (!isPrepared) {
isPrepared = true;
onPrepared(playWhenReady);
break;
}
- if (currentState == STATE_PAUSED_SEEK) break;
changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED);
break;
case Player.STATE_ENDED: // 4
- // Ensure the current window has actually ended
- // since single windows that are still loading may produce an ended state
- if (isCurrentWindowValid() &&
- simpleExoPlayer.getCurrentPosition() >= simpleExoPlayer.getDuration()) {
- changeState(STATE_COMPLETED);
- isPrepared = false;
- }
+ changeState(STATE_COMPLETED);
+ isPrepared = false;
break;
}
}
- private void maybeRecover() {
+ private void maybeCorrectSeekPosition() {
+ if (playQueue == null || simpleExoPlayer == null || currentInfo == null) return;
+
final int currentSourceIndex = playQueue.getIndex();
final PlayQueueItem currentSourceItem = playQueue.getItem();
+ if (currentSourceItem == null) return;
- // Check if already playing correct window
- final boolean isCurrentPeriodCorrect =
+ final long recoveryPositionMillis = currentSourceItem.getRecoveryPosition();
+ final boolean isCurrentWindowCorrect =
simpleExoPlayer.getCurrentPeriodIndex() == currentSourceIndex;
+ final long presetStartPositionMillis = currentInfo.getStartPosition() * 1000;
- // Check if recovering
- if (isCurrentPeriodCorrect && currentSourceItem != null) {
- /* Recovering with sub-second position may cause a long buffer delay in ExoPlayer,
- * rounding this position to the nearest second will help alleviate this.*/
- final long position = currentSourceItem.getRecoveryPosition();
-
- /* Skip recovering if the recovery position is not set.*/
- if (position == PlayQueueItem.RECOVERY_UNSET) return;
-
- if (DEBUG) Log.d(TAG, "Rewinding to recovery window: " + currentSourceIndex +
- " at: " + getTimeString((int)position));
- simpleExoPlayer.seekTo(currentSourceItem.getRecoveryPosition());
+ if (recoveryPositionMillis != PlayQueueItem.RECOVERY_UNSET && isCurrentWindowCorrect) {
+ // Is recovering previous playback?
+ if (DEBUG) Log.d(TAG, "Playback - Rewinding to recovery time=" +
+ "[" + getTimeString((int)recoveryPositionMillis) + "]");
+ seekTo(recoveryPositionMillis);
playQueue.unsetRecovery(currentSourceIndex);
+
+ } else if (isSynchronizing && simpleExoPlayer.isCurrentWindowDynamic()) {
+ if (DEBUG) Log.d(TAG, "Playback - Synchronizing livestream to default time");
+ // Is still synchronizing?
+ seekToDefault();
+
+ } else if (isSynchronizing && presetStartPositionMillis != 0L) {
+ if (DEBUG) Log.d(TAG, "Playback - Seeking to preset start " +
+ "position=[" + presetStartPositionMillis + "]");
+ // Has another start position?
+ seekTo(presetStartPositionMillis);
+ currentInfo.setStartPosition(0);
}
+
+ isSynchronizing = false;
}
/**
@@ -775,6 +786,16 @@ public abstract class BasePlayer implements
// Playback Listener
//////////////////////////////////////////////////////////////////////////*/
+ @Override
+ public boolean isNearPlaybackEdge(final long timeToEndMillis) {
+ // If live, then not near playback edge
+ if (simpleExoPlayer == null || simpleExoPlayer.isCurrentWindowDynamic()) return false;
+
+ final long currentPositionMillis = simpleExoPlayer.getCurrentPosition();
+ final long currentDurationMillis = simpleExoPlayer.getDuration();
+ return currentDurationMillis - currentPositionMillis < timeToEndMillis;
+ }
+
@Override
public void onPlaybackBlock() {
if (simpleExoPlayer == null) return;
@@ -796,7 +817,6 @@ public abstract class BasePlayer implements
if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING);
simpleExoPlayer.prepare(mediaSource);
- seekToDefault();
}
@Override
@@ -805,11 +825,26 @@ public abstract class BasePlayer implements
if (DEBUG) Log.d(TAG, "Playback - onPlaybackSynchronize() called with " +
(info != null ? "available" : "null") + " info, " +
"item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]");
+ if (simpleExoPlayer == null || playQueue == null) return;
+ final boolean onPlaybackInitial = currentItem == null;
final boolean hasPlayQueueItemChanged = currentItem != item;
final boolean hasStreamInfoChanged = currentInfo != info;
+
+ final int currentPlayQueueIndex = playQueue.indexOf(item);
+ final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex();
+ final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount();
+
+ // when starting playback on the last item when not repeating, maybe auto queue
+ if (info != null && currentPlayQueueIndex == playQueue.size() - 1 &&
+ getRepeatMode() == Player.REPEAT_MODE_OFF &&
+ PlayerHelper.isAutoQueueEnabled(context)) {
+ final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info, playQueue.getStreams());
+ if (autoQueue != null) playQueue.append(autoQueue.getStreams());
+ }
+ // If nothing to synchronize
if (!hasPlayQueueItemChanged && !hasStreamInfoChanged) {
- return; // Nothing to synchronize
+ return;
}
currentItem = item;
@@ -819,34 +854,31 @@ public abstract class BasePlayer implements
registerView();
initThumbnail(info == null ? item.getThumbnailUrl() : info.getThumbnailUrl());
}
-
- final int currentPlayQueueIndex = playQueue.indexOf(item);
onMetadataChanged(item, info, currentPlayQueueIndex, hasPlayQueueItemChanged);
- if (simpleExoPlayer == null) return;
- final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex();
// Check if on wrong window
if (currentPlayQueueIndex != playQueue.getIndex()) {
- Log.e(TAG, "Play Queue may be desynchronized: item " +
+ Log.e(TAG, "Playback - Play Queue may be desynchronized: item " +
"index=[" + currentPlayQueueIndex + "], " +
"queue index=[" + playQueue.getIndex() + "]");
- // on metadata changed
- } else if (currentPlaylistIndex != currentPlayQueueIndex || !isPlaying()) {
- final long startPos = info != null ? info.start_position : C.TIME_UNSET;
- if (DEBUG) Log.d(TAG, "Rewinding to correct" +
- " window=[" + currentPlayQueueIndex + "]," +
- " at=[" + getTimeString((int)startPos) + "]," +
- " from=[" + simpleExoPlayer.getCurrentWindowIndex() + "].");
- simpleExoPlayer.seekTo(currentPlayQueueIndex, startPos);
- }
+ // Check if bad seek position
+ } else if ((currentPlaylistSize > 0 && currentPlayQueueIndex >= currentPlaylistSize) ||
+ currentPlayQueueIndex < 0) {
+ Log.e(TAG, "Playback - Trying to seek to invalid " +
+ "index=[" + currentPlayQueueIndex + "] with " +
+ "playlist length=[" + currentPlaylistSize + "]");
- // when starting playback on the last item when not repeating, maybe auto queue
- if (info != null && currentPlayQueueIndex == playQueue.size() - 1 &&
- getRepeatMode() == Player.REPEAT_MODE_OFF &&
- PlayerHelper.isAutoQueueEnabled(context)) {
- final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info, playQueue.getStreams());
- if (autoQueue != null) playQueue.append(autoQueue.getStreams());
+ // If not playing correct stream, change window position and sets flag
+ // for synchronizing once window position is corrected
+ // @see maybeCorrectSeekPosition()
+ } else if (currentPlaylistIndex != currentPlayQueueIndex || onPlaybackInitial ||
+ !isPlaying()) {
+ if (DEBUG) Log.d(TAG, "Playback - Rewinding to correct" +
+ " index=[" + currentPlayQueueIndex + "]," +
+ " from=[" + currentPlaylistIndex + "], size=[" + currentPlaylistSize + "].");
+ isSynchronizing = true;
+ simpleExoPlayer.seekToDefaultPosition(currentPlayQueueIndex);
}
}
@@ -858,6 +890,11 @@ public abstract class BasePlayer implements
@Nullable
@Override
public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) {
+ final StreamType streamType = info.getStreamType();
+ if (!(streamType == StreamType.AUDIO_LIVE_STREAM || streamType == StreamType.LIVE_STREAM)) {
+ return null;
+ }
+
if (!info.getHlsUrl().isEmpty()) {
return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS);
} else if (!info.getDashMpdUrl().isEmpty()) {
@@ -911,14 +948,11 @@ public abstract class BasePlayer implements
changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED);
}
- public void onVideoPlayPause() {
- if (DEBUG) Log.d(TAG, "onVideoPlayPause() called");
+ public void onPlay() {
+ if (DEBUG) Log.d(TAG, "onPlay() called");
+ if (audioReactor == null || playQueue == null || simpleExoPlayer == null) return;
- if (!isPlaying()) {
- audioReactor.requestAudioFocus();
- } else {
- audioReactor.abandonAudioFocus();
- }
+ audioReactor.requestAudioFocus();
if (getCurrentState() == STATE_COMPLETED) {
if (playQueue.getIndex() == 0) {
@@ -928,7 +962,25 @@ public abstract class BasePlayer implements
}
}
- simpleExoPlayer.setPlayWhenReady(!isPlaying());
+ simpleExoPlayer.setPlayWhenReady(true);
+ }
+
+ public void onPause() {
+ if (DEBUG) Log.d(TAG, "onPause() called");
+ if (audioReactor == null || simpleExoPlayer == null) return;
+
+ audioReactor.abandonAudioFocus();
+ simpleExoPlayer.setPlayWhenReady(false);
+ }
+
+ public void onPlayPause() {
+ if (DEBUG) Log.d(TAG, "onPlayPause() called");
+
+ if (!isPlaying()) {
+ onPlay();
+ } else {
+ onPause();
+ }
}
public void onFastRewind() {
@@ -945,14 +997,15 @@ public abstract class BasePlayer implements
if (simpleExoPlayer == null || playQueue == null) return;
if (DEBUG) Log.d(TAG, "onPlayPrevious() called");
- savePlaybackState();
-
- /* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds, restart current track.
- * Also restart the track if the current track is the first in a queue.*/
- if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT || playQueue.getIndex() == 0) {
- final long startPos = currentInfo == null ? 0 : currentInfo.start_position;
- simpleExoPlayer.seekTo(startPos);
+ /* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds,
+ * restart current track. Also restart the track if the current track
+ * is the first in a queue.*/
+ if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT ||
+ playQueue.getIndex() == 0) {
+ seekToDefault();
+ playQueue.offsetIndex(0);
} else {
+ savePlaybackState();
playQueue.offsetIndex(-1);
}
}
@@ -962,7 +1015,6 @@ public abstract class BasePlayer implements
if (DEBUG) Log.d(TAG, "onPlayNext() called");
savePlaybackState();
-
playQueue.offsetIndex(+1);
}
@@ -975,20 +1027,21 @@ public abstract class BasePlayer implements
if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) {
seekToDefault();
} else {
- playQueue.setIndex(index);
+ savePlaybackState();
}
+ playQueue.setIndex(index);
}
- public void seekBy(int milliSeconds) {
- if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]");
- if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) ||
- ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0))) {
- return;
- }
+ public void seekTo(long positionMillis) {
+ if (DEBUG) Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]");
+ if (simpleExoPlayer == null || positionMillis < 0 ||
+ positionMillis > simpleExoPlayer.getDuration()) return;
+ simpleExoPlayer.seekTo(positionMillis);
+ }
- int progress = (int) (simpleExoPlayer.getCurrentPosition() + milliSeconds);
- if (progress < 0) progress = 0;
- simpleExoPlayer.seekTo(progress);
+ public void seekBy(long offsetMillis) {
+ if (DEBUG) Log.d(TAG, "seekBy() called with: offsetMillis = [" + offsetMillis + "]");
+ seekTo(simpleExoPlayer.getCurrentPosition() + offsetMillis);
}
public boolean isCurrentWindowValid() {
@@ -1015,8 +1068,11 @@ public abstract class BasePlayer implements
protected void reload() {
if (playbackManager != null) {
- playbackManager.reset();
- playbackManager.load();
+ playbackManager.dispose();
+ }
+
+ if (playQueue != null) {
+ playbackManager = new MediaSourceManager(this, playQueue);
}
}
@@ -1069,8 +1125,22 @@ public abstract class BasePlayer implements
return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getUploader();
}
- public boolean isCompleted() {
- return simpleExoPlayer != null && simpleExoPlayer.getPlaybackState() == Player.STATE_ENDED;
+ /** Checks if the current playback is a livestream AND is playing at or beyond the live edge */
+ public boolean isLiveEdge() {
+ if (simpleExoPlayer == null) return false;
+ final boolean isLive = simpleExoPlayer.isCurrentWindowDynamic();
+ if (!isLive) return false;
+
+ final Timeline currentTimeline = simpleExoPlayer.getCurrentTimeline();
+ final int currentWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
+ if (currentTimeline.isEmpty() || currentWindowIndex < 0 ||
+ currentWindowIndex >= currentTimeline.getWindowCount()) {
+ return false;
+ }
+
+ Timeline.Window timelineWindow = new Timeline.Window();
+ currentTimeline.getWindow(currentWindowIndex, timelineWindow);
+ return timelineWindow.getDefaultPositionMs() <= simpleExoPlayer.getCurrentPosition();
}
public boolean isPlaying() {
@@ -1123,8 +1193,8 @@ public abstract class BasePlayer implements
return playQueueAdapter;
}
- public boolean isPlayerReady() {
- return currentState == STATE_PLAYING || currentState == STATE_COMPLETED || currentState == STATE_PAUSED;
+ public boolean isPrepared() {
+ return isPrepared;
}
public boolean isProgressLoopRunning() {
diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java
index 4f27d1fee..dbc34b11a 100644
--- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java
@@ -19,7 +19,6 @@
package org.schabi.newpipe.player;
-import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
@@ -33,6 +32,7 @@ import android.preference.PreferenceManager;
import android.provider.Settings;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.util.DisplayMetrics;
@@ -57,11 +57,13 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
+import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
+import org.schabi.newpipe.playlist.PlayQueueItemTouchCallback;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
@@ -76,6 +78,8 @@ import java.util.UUID;
import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING;
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION;
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME;
+import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA;
+import static org.schabi.newpipe.util.AnimationUtils.animateRotation;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE;
@@ -84,7 +88,8 @@ import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE;
*
* @author mauriciocolli
*/
-public final class MainVideoPlayer extends Activity implements StateSaver.WriteRead {
+public final class MainVideoPlayer extends AppCompatActivity
+ implements StateSaver.WriteRead, PlaybackParameterDialog.Callback {
private static final String TAG = ".MainVideoPlayer";
private static final boolean DEBUG = BasePlayer.DEBUG;
@@ -110,7 +115,7 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) getWindow().setStatusBarColor(Color.BLACK);
setVolumeControlStream(AudioManager.STREAM_MUSIC);
- changeSystemUi();
+ hideSystemUi();
setContentView(R.layout.activity_main_player);
playerImpl = new VideoPlayerImpl(this);
playerImpl.setup(findViewById(android.R.id.content));
@@ -147,7 +152,7 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
if (DEBUG) Log.d(TAG, "onResume() called");
if (playerImpl.getPlayer() != null && activityPaused && playerImpl.wasPlaying()
&& !playerImpl.isPlaying()) {
- playerImpl.onVideoPlayPause();
+ playerImpl.onPlay();
}
activityPaused = false;
@@ -182,7 +187,7 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
if (playerImpl != null && playerImpl.getPlayer() != null && !activityPaused) {
playerImpl.wasPlaying = playerImpl.isPlaying();
- if (playerImpl.isPlaying()) playerImpl.onVideoPlayPause();
+ playerImpl.onPause();
}
activityPaused = true;
}
@@ -337,6 +342,15 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
}
}
+ ////////////////////////////////////////////////////////////////////////////
+ // Playback Parameters Listener
+ ////////////////////////////////////////////////////////////////////////////
+
+ @Override
+ public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) {
+ if (playerImpl != null) playerImpl.setPlaybackParameters(playbackTempo, playbackPitch);
+ }
+
///////////////////////////////////////////////////////////////////////////
@SuppressWarnings({"unused", "WeakerAccess"})
@@ -548,7 +562,7 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
public void onClick(View v) {
super.onClick(v);
if (v.getId() == playPauseButton.getId()) {
- onVideoPlayPause();
+ onPlayPause();
} else if (v.getId() == playPreviousButton.getId()) {
onPlayPrevious();
@@ -597,28 +611,27 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
updatePlaybackButtons();
getControlsRoot().setVisibility(View.INVISIBLE);
- queueLayout.setVisibility(View.VISIBLE);
+ animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/true,
+ DEFAULT_CONTROLS_DURATION);
itemsList.scrollToPosition(playQueue.getIndex());
}
private void onQueueClosed() {
- queueLayout.setVisibility(View.GONE);
+ animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/false,
+ DEFAULT_CONTROLS_DURATION);
queueVisible = false;
}
private void onMoreOptionsClicked() {
if (DEBUG) Log.d(TAG, "onMoreOptionsClicked() called");
- if (secondaryControls.getVisibility() == View.VISIBLE) {
- moreOptionsButton.setImageDrawable(getResources().getDrawable(
- R.drawable.ic_expand_more_white_24dp));
- animateView(secondaryControls, false, 200);
- } else {
- moreOptionsButton.setImageDrawable(getResources().getDrawable(
- R.drawable.ic_expand_less_white_24dp));
- animateView(secondaryControls, true, 200);
- }
+ final boolean isMoreControlsVisible = secondaryControls.getVisibility() == View.VISIBLE;
+
+ animateRotation(moreOptionsButton, DEFAULT_CONTROLS_DURATION,
+ isMoreControlsVisible ? 0 : 180);
+ animateView(secondaryControls, SLIDE_AND_ALPHA, !isMoreControlsVisible,
+ DEFAULT_CONTROLS_DURATION);
showControls(DEFAULT_CONTROLS_DURATION);
}
@@ -628,6 +641,12 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
showControlsThenHide();
}
+ @Override
+ public void onPlaybackSpeedClicked() {
+ PlaybackParameterDialog.newInstance(getPlaybackSpeed(), getPlaybackPitch())
+ .show(getSupportFragmentManager(), TAG);
+ }
+
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
super.onStopTrackingTouch(seekBar);
@@ -638,6 +657,7 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
public void onDismiss(PopupMenu menu) {
super.onDismiss(menu);
if (isPlaying()) hideControls(DEFAULT_CONTROLS_DURATION, 0);
+ hideSystemUi();
}
@Override
@@ -696,7 +716,6 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
animatePlayButtons(true, 200);
});
- changeSystemUi();
getRootView().setKeepScreenOn(true);
}
@@ -798,31 +817,11 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
}
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
- return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) {
+ return new PlayQueueItemTouchCallback() {
@Override
- public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
- if (source.getItemViewType() != target.getItemViewType()) {
- return false;
- }
-
- final int sourceIndex = source.getLayoutPosition();
- final int targetIndex = target.getLayoutPosition();
- playQueue.move(sourceIndex, targetIndex);
- return true;
+ public void onMove(int sourceIndex, int targetIndex) {
+ if (playQueue != null) playQueue.move(sourceIndex, targetIndex);
}
-
- @Override
- public boolean isLongPressDragEnabled() {
- return false;
- }
-
- @Override
- public boolean isItemViewSwipeEnabled() {
- return false;
- }
-
- @Override
- public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {}
};
}
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 123fbfee3..20860d9c5 100644
--- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java
@@ -618,7 +618,7 @@ public final class PopupVideoPlayer extends Service {
onClose();
break;
case ACTION_PLAY_PAUSE:
- onVideoPlayPause();
+ onPlayPause();
break;
case ACTION_REPEAT:
onRepeatClicked();
@@ -716,7 +716,7 @@ public final class PopupVideoPlayer extends Service {
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 == null || !playerImpl.isPlaying() || !playerImpl.isPlayerReady()) return false;
+ if (playerImpl == null || !playerImpl.isPlaying()) return false;
if (e.getX() > popupWidth / 2) {
playerImpl.onFastForward();
@@ -731,7 +731,7 @@ public final class PopupVideoPlayer extends Service {
public boolean onSingleTapConfirmed(MotionEvent e) {
if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
if (playerImpl == null || playerImpl.getPlayer() == null) return false;
- playerImpl.onVideoPlayPause();
+ playerImpl.onPlayPause();
return true;
}
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 d9c04b796..239c9c8d3 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java
@@ -31,9 +31,11 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.player.event.PlayerEventListener;
+import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
+import org.schabi.newpipe.playlist.PlayQueueItemTouchCallback;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;
@@ -42,7 +44,8 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.formatPitch;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
public abstract class ServicePlayerActivity extends AppCompatActivity
- implements PlayerEventListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener {
+ implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
+ View.OnClickListener, PlaybackParameterDialog.Callback {
private boolean serviceBound;
private ServiceConnection serviceConnection;
@@ -56,14 +59,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
////////////////////////////////////////////////////////////////////////////
private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47;
- private static final int PLAYBACK_SPEED_POPUP_MENU_GROUP_ID = 61;
- private static final int PLAYBACK_PITCH_POPUP_MENU_GROUP_ID = 97;
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
- private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 10;
- private static final int MAXIMUM_INITIAL_DRAG_VELOCITY = 25;
-
private View rootView;
private RecyclerView itemsList;
@@ -87,9 +85,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private ProgressBar progressBar;
private TextView playbackSpeedButton;
- private PopupMenu playbackSpeedPopupMenu;
private TextView playbackPitchButton;
- private PopupMenu playbackPitchPopupMenu;
////////////////////////////////////////////////////////////////////////////
// Abstracts
@@ -319,45 +315,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
shuffleButton.setOnClickListener(this);
playbackSpeedButton.setOnClickListener(this);
playbackPitchButton.setOnClickListener(this);
-
- playbackSpeedPopupMenu = new PopupMenu(this, playbackSpeedButton);
- playbackPitchPopupMenu = new PopupMenu(this, playbackPitchButton);
- buildPlaybackSpeedMenu();
- buildPlaybackPitchMenu();
- }
-
- private void buildPlaybackSpeedMenu() {
- if (playbackSpeedPopupMenu == null) return;
-
- playbackSpeedPopupMenu.getMenu().removeGroup(PLAYBACK_SPEED_POPUP_MENU_GROUP_ID);
- for (int i = 0; i < BasePlayer.PLAYBACK_SPEEDS.length; i++) {
- final float playbackSpeed = BasePlayer.PLAYBACK_SPEEDS[i];
- final String formattedSpeed = formatSpeed(playbackSpeed);
- final MenuItem item = playbackSpeedPopupMenu.getMenu().add(PLAYBACK_SPEED_POPUP_MENU_GROUP_ID, i, Menu.NONE, formattedSpeed);
- item.setOnMenuItemClickListener(menuItem -> {
- if (player == null) return false;
-
- player.setPlaybackSpeed(playbackSpeed);
- return true;
- });
- }
- }
-
- private void buildPlaybackPitchMenu() {
- if (playbackPitchPopupMenu == null) return;
-
- playbackPitchPopupMenu.getMenu().removeGroup(PLAYBACK_PITCH_POPUP_MENU_GROUP_ID);
- for (int i = 0; i < BasePlayer.PLAYBACK_PITCHES.length; i++) {
- final float playbackPitch = BasePlayer.PLAYBACK_PITCHES[i];
- final String formattedPitch = formatPitch(playbackPitch);
- final MenuItem item = playbackPitchPopupMenu.getMenu().add(PLAYBACK_PITCH_POPUP_MENU_GROUP_ID, i, Menu.NONE, formattedPitch);
- item.setOnMenuItemClickListener(menuItem -> {
- if (player == null) return false;
-
- player.setPlaybackPitch(playbackPitch);
- return true;
- });
- }
}
private void buildItemPopupMenu(final PlayQueueItem item, final View view) {
@@ -398,43 +355,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
}
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
- return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) {
+ return new PlayQueueItemTouchCallback() {
@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 clampedAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
- Math.min(Math.abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY));
- return clampedAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
- }
-
- @Override
- public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source,
- RecyclerView.ViewHolder target) {
- if (source.getItemViewType() != target.getItemViewType()) {
- return false;
- }
-
- final int sourceIndex = source.getLayoutPosition();
- final int targetIndex = target.getLayoutPosition();
+ public void onMove(int sourceIndex, int targetIndex) {
if (player != null) player.getPlayQueue().move(sourceIndex, targetIndex);
- return true;
}
-
- @Override
- public boolean isLongPressDragEnabled() {
- return false;
- }
-
- @Override
- public boolean isItemViewSwipeEnabled() {
- return false;
- }
-
- @Override
- public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {}
};
}
@@ -499,7 +424,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
player.onPlayPrevious();
} else if (view.getId() == playPauseButton.getId()) {
- player.onVideoPlayPause();
+ player.onPlayPause();
} else if (view.getId() == forwardButton.getId()) {
player.onPlayNext();
@@ -508,10 +433,10 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
player.onShuffleClicked();
} else if (view.getId() == playbackSpeedButton.getId()) {
- playbackSpeedPopupMenu.show();
+ openPlaybackParameterDialog();
} else if (view.getId() == playbackPitchButton.getId()) {
- playbackPitchPopupMenu.show();
+ openPlaybackParameterDialog();
} else if (view.getId() == metadata.getId()) {
scrollToSelected();
@@ -522,6 +447,21 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
}
}
+ ////////////////////////////////////////////////////////////////////////////
+ // Playback Parameters
+ ////////////////////////////////////////////////////////////////////////////
+
+ private void openPlaybackParameterDialog() {
+ if (player == null) return;
+ PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(),
+ player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag());
+ }
+
+ @Override
+ public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) {
+ if (player != null) player.setPlaybackParameters(playbackTempo, playbackPitch);
+ }
+
////////////////////////////////////////////////////////////////////////////
// Seekbar Listener
////////////////////////////////////////////////////////////////////////////
@@ -543,7 +483,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
- if (player != null) player.simpleExoPlayer.seekTo(seekBar.getProgress());
+ if (player != null) player.seekTo(seekBar.getProgress());
seekDisplay.setVisibility(View.GONE);
seeking = false;
}
@@ -573,13 +513,17 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
progressSeekBar.setProgress(currentProgress);
progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000));
}
+
+ if (player != null) {
+ progressLiveSync.setClickable(!player.isLiveEdge());
+ }
}
@Override
public void onMetadataUpdate(StreamInfo info) {
if (info != null) {
metadataTitle.setText(info.getName());
- metadataArtist.setText(info.uploader_name);
+ metadataArtist.setText(info.getUploaderName());
progressEndTime.setVisibility(View.GONE);
progressLiveSync.setVisibility(View.GONE);
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 aa90b7b88..b019ea91e 100644
--- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java
@@ -49,6 +49,7 @@ 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;
@@ -354,10 +355,10 @@ public abstract class VideoPlayer extends BasePlayer
break;
case VIDEO_STREAM:
- if (info.video_streams.size() + info.video_only_streams.size() == 0) break;
+ if (info.getVideoStreams().size() + info.getVideoOnlyStreams().size() == 0) break;
final List videos = ListHelper.getSortedStreamVideosList(context,
- info.video_streams, info.video_only_streams, false);
+ info.getVideoStreams(), info.getVideoOnlyStreams(), false);
availableStreams = new ArrayList<>(videos);
if (playbackQuality == null) {
selectedStreamIndex = getDefaultResolutionIndex(videos);
@@ -388,7 +389,7 @@ public abstract class VideoPlayer extends BasePlayer
// Create video stream source
final List videos = ListHelper.getSortedStreamVideosList(context,
- info.video_streams, info.video_only_streams, false);
+ info.getVideoStreams(), info.getVideoOnlyStreams(), false);
final int index;
if (videos.isEmpty()) {
index = -1;
@@ -425,7 +426,7 @@ public abstract class VideoPlayer extends BasePlayer
// Create subtitle sources
for (final Subtitles subtitle : info.getSubtitles()) {
final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType());
- if (mimeType == null || context == null) continue;
+ if (mimeType == null) continue;
final Format textFormat = Format.createTextSampleFormat(null, mimeType,
SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle));
@@ -523,6 +524,12 @@ public abstract class VideoPlayer extends BasePlayer
onTextTrackUpdate();
}
+ @Override
+ public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
+ super.onPlaybackParametersChanged(playbackParameters);
+ playbackSpeedTextView.setText(formatSpeed(playbackParameters.speed));
+ }
+
@Override
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
if (DEBUG) {
@@ -599,7 +606,7 @@ public abstract class VideoPlayer extends BasePlayer
@Override
public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) {
- if (!isPrepared) return;
+ if (!isPrepared()) return;
if (duration != playbackSeekBar.getMax()) {
playbackEndTime.setText(getTimeString(duration));
@@ -615,6 +622,7 @@ public abstract class VideoPlayer extends BasePlayer
if (DEBUG && bufferPercent % 20 == 0) { //Limit log
Log.d(TAG, "updateProgress() called with: isVisible = " + isControlsVisible() + ", currentProgress = [" + currentProgress + "], duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]");
}
+ playbackLiveSync.setClickable(!isLiveEdge());
}
@Override
@@ -624,8 +632,6 @@ public abstract class VideoPlayer extends BasePlayer
}
protected void onFullScreenButtonClicked() {
- if (!isPlayerReady()) return;
-
changeState(STATE_BLOCKED);
}
@@ -720,7 +726,7 @@ public abstract class VideoPlayer extends BasePlayer
wasPlaying = simpleExoPlayer.getPlayWhenReady();
}
- private void onPlaybackSpeedClicked() {
+ public void onPlaybackSpeedClicked() {
if (DEBUG) Log.d(TAG, "onPlaybackSpeedClicked() called");
playbackSpeedPopupMenu.show();
isSomePopupMenuVisible = true;
@@ -735,7 +741,7 @@ public abstract class VideoPlayer extends BasePlayer
}
private void onResizeClicked() {
- if (getAspectRatioFrameLayout() != null && context != null) {
+ if (getAspectRatioFrameLayout() != null) {
final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode();
final int newResizeMode = nextResizeMode(currentResizeMode);
getAspectRatioFrameLayout().setResizeMode(newResizeMode);
@@ -772,7 +778,7 @@ public abstract class VideoPlayer extends BasePlayer
public void onStopTrackingTouch(SeekBar seekBar) {
if (DEBUG) Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]");
- simpleExoPlayer.seekTo(seekBar.getProgress());
+ seekTo(seekBar.getProgress());
if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) simpleExoPlayer.setPlayWhenReady(true);
playbackCurrentTime.setText(getTimeString(seekBar.getProgress()));
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java
index 2c85cfc34..5b9cce947 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java
@@ -17,10 +17,14 @@ import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters;
-public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AudioRendererEventListener {
+public class AudioReactor implements AudioManager.OnAudioFocusChangeListener,
+ AudioRendererEventListener {
private static final String TAG = "AudioFocusReactor";
+ private static final boolean SHOULD_BUILD_FOCUS_REQUEST =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
+
private static final int DUCK_DURATION = 1500;
private static final float DUCK_AUDIO_TO = .2f;
@@ -33,13 +37,14 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au
private final AudioFocusRequest request;
- public AudioReactor(@NonNull final Context context, @NonNull final SimpleExoPlayer player) {
+ public AudioReactor(@NonNull final Context context,
+ @NonNull final SimpleExoPlayer player) {
this.player = player;
this.context = context;
this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
- player.setAudioDebugListener(this);
+ player.addAudioDebugListener(this);
- if (shouldBuildFocusRequest()) {
+ if (SHOULD_BUILD_FOCUS_REQUEST) {
request = new AudioFocusRequest.Builder(FOCUS_GAIN_TYPE)
.setAcceptsDelayedFocusGain(true)
.setWillPauseWhenDucked(true)
@@ -50,12 +55,17 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au
}
}
+ public void dispose() {
+ abandonAudioFocus();
+ player.removeAudioDebugListener(this);
+ }
+
/*//////////////////////////////////////////////////////////////////////////
// Audio Manager
//////////////////////////////////////////////////////////////////////////*/
public void requestAudioFocus() {
- if (shouldBuildFocusRequest()) {
+ if (SHOULD_BUILD_FOCUS_REQUEST) {
audioManager.requestAudioFocus(request);
} else {
audioManager.requestAudioFocus(this, STREAM_TYPE, FOCUS_GAIN_TYPE);
@@ -63,7 +73,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au
}
public void abandonAudioFocus() {
- if (shouldBuildFocusRequest()) {
+ if (SHOULD_BUILD_FOCUS_REQUEST) {
audioManager.abandonAudioFocusRequest(request);
} else {
audioManager.abandonAudioFocus(this);
@@ -82,10 +92,6 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au
audioManager.setStreamVolume(STREAM_TYPE, volume, 0);
}
- private boolean shouldBuildFocusRequest() {
- return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
- }
-
/*//////////////////////////////////////////////////////////////////////////
// AudioFocus
//////////////////////////////////////////////////////////////////////////*/
@@ -148,12 +154,8 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au
player.setVolume(to);
}
});
- valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
- player.setVolume(((float) animation.getAnimatedValue()));
- }
- });
+ valueAnimator.addUpdateListener(animation ->
+ player.setVolume(((float) animation.getAnimatedValue())));
valueAnimator.start();
}
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
new file mode 100644
index 000000000..8405e45fd
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java
@@ -0,0 +1,38 @@
+package org.schabi.newpipe.player.helper;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.v4.media.session.MediaSessionCompat;
+
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
+
+import org.schabi.newpipe.player.mediasession.DummyPlaybackPreparer;
+import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
+import org.schabi.newpipe.player.mediasession.PlayQueueNavigator;
+import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController;
+
+public class MediaSessionManager {
+ private static final String TAG = "MediaSessionManager";
+
+ private final MediaSessionCompat mediaSession;
+ private final MediaSessionConnector sessionConnector;
+
+ public MediaSessionManager(@NonNull final Context context,
+ @NonNull final Player player,
+ @NonNull final MediaSessionCallback callback) {
+ this.mediaSession = new MediaSessionCompat(context, TAG);
+ this.sessionConnector = new MediaSessionConnector(mediaSession,
+ new PlayQueuePlaybackController(callback));
+ this.sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback));
+ this.sessionConnector.setPlayer(player, new DummyPlaybackPreparer());
+ }
+
+ public MediaSessionCompat getMediaSession() {
+ return mediaSession;
+ }
+
+ public MediaSessionConnector getSessionConnector() {
+ return sessionConnector;
+ }
+}
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
new file mode 100644
index 000000000..7c7d87791
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
@@ -0,0 +1,379 @@
+package org.schabi.newpipe.player.helper;
+
+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.v4.app.DialogFragment;
+import android.support.v7.app.AlertDialog;
+import android.util.Log;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.util.SliderStrategy;
+
+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;
+ 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 DEFAULT_TEMPO = 1.00f;
+ public static final double DEFAULT_PITCH = 1.00f;
+
+ @NonNull private static final String INITIAL_TEMPO_KEY = "initial_tempo_key";
+ @NonNull private static final String INITIAL_PITCH_KEY = "initial_pitch_key";
+
+ public interface Callback {
+ void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch);
+ }
+
+ @Nullable private Callback callback;
+
+ @NonNull private final SliderStrategy strategy = new SliderStrategy.Quadratic(
+ MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE,
+ /*centerAt=*/1.00f, /*sliderGranularity=*/10000);
+
+ private double initialTempo = DEFAULT_TEMPO;
+ private double initialPitch = DEFAULT_PITCH;
+
+ @Nullable private SeekBar tempoSlider;
+ @Nullable private TextView tempoMinimumText;
+ @Nullable private TextView tempoMaximumText;
+ @Nullable private TextView tempoCurrentText;
+ @Nullable private TextView tempoStepDownText;
+ @Nullable private TextView tempoStepUpText;
+
+ @Nullable private SeekBar pitchSlider;
+ @Nullable private TextView pitchMinimumText;
+ @Nullable private TextView pitchMaximumText;
+ @Nullable private TextView pitchCurrentText;
+ @Nullable private TextView pitchStepDownText;
+ @Nullable private TextView pitchStepUpText;
+
+ @Nullable private CheckBox unhookingCheckbox;
+
+ @Nullable private TextView nightCorePresetText;
+ @Nullable private TextView resetPresetText;
+
+ public static PlaybackParameterDialog newInstance(final double playbackTempo,
+ final double playbackPitch) {
+ PlaybackParameterDialog dialog = new PlaybackParameterDialog();
+ dialog.initialTempo = playbackTempo;
+ dialog.initialPitch = playbackPitch;
+ return dialog;
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Lifecycle
+ //////////////////////////////////////////////////////////////////////////*/
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ if (context != null && context instanceof Callback) {
+ callback = (Callback) context;
+ } else {
+ dismiss();
+ }
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO);
+ initialPitch = savedInstanceState.getDouble(INITIAL_PITCH_KEY, DEFAULT_PITCH);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putDouble(INITIAL_TEMPO_KEY, initialTempo);
+ outState.putDouble(INITIAL_PITCH_KEY, initialPitch);
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Dialog
+ //////////////////////////////////////////////////////////////////////////*/
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null);
+ setupControlViews(view);
+
+ final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity())
+ .setTitle(R.string.playback_speed_control)
+ .setView(view)
+ .setCancelable(true)
+ .setNegativeButton(R.string.cancel, (dialogInterface, i) ->
+ setPlaybackParameters(initialTempo, initialPitch))
+ .setPositiveButton(R.string.finish, (dialogInterface, i) ->
+ setCurrentPlaybackParameters());
+
+ return dialogBuilder.create();
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Control Views
+ //////////////////////////////////////////////////////////////////////////*/
+
+ private void setupControlViews(@NonNull View rootView) {
+ setupHookingControl(rootView);
+ setupTempoControl(rootView);
+ setupPitchControl(rootView);
+ setupPresetControl(rootView);
+ }
+
+ private void setupTempoControl(@NonNull View rootView) {
+ tempoSlider = rootView.findViewById(R.id.tempoSeekbar);
+ tempoMinimumText = rootView.findViewById(R.id.tempoMinimumText);
+ tempoMaximumText = rootView.findViewById(R.id.tempoMaximumText);
+ tempoCurrentText = rootView.findViewById(R.id.tempoCurrentText);
+ tempoStepUpText = rootView.findViewById(R.id.tempoStepUp);
+ tempoStepDownText = rootView.findViewById(R.id.tempoStepDown);
+
+ if (tempoCurrentText != null)
+ tempoCurrentText.setText(PlayerHelper.formatSpeed(initialTempo));
+ 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.setOnSeekBarChangeListener(getOnTempoChangedListener());
+ }
+ }
+
+ private void setupPitchControl(@NonNull View rootView) {
+ pitchSlider = rootView.findViewById(R.id.pitchSeekbar);
+ pitchMinimumText = rootView.findViewById(R.id.pitchMinimumText);
+ pitchMaximumText = rootView.findViewById(R.id.pitchMaximumText);
+ pitchCurrentText = rootView.findViewById(R.id.pitchCurrentText);
+ pitchStepDownText = rootView.findViewById(R.id.pitchStepDown);
+ pitchStepUpText = rootView.findViewById(R.id.pitchStepUp);
+
+ if (pitchCurrentText != null)
+ pitchCurrentText.setText(PlayerHelper.formatPitch(initialPitch));
+ 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.setOnSeekBarChangeListener(getOnPitchChangedListener());
+ }
+ }
+
+ private void setupHookingControl(@NonNull View rootView) {
+ unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox);
+ if (unhookingCheckbox != null) {
+ unhookingCheckbox.setChecked(initialPitch != initialTempo);
+ unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
+ if (isChecked) return;
+ // When unchecked, slide back to the minimum of current tempo or pitch
+ final double minimum = Math.min(getCurrentPitch(), getCurrentTempo());
+ setSliders(minimum);
+ setCurrentPlaybackParameters();
+ });
+ }
+ }
+
+ 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);
+
+ setTempoSlider(NIGHTCORE_TEMPO);
+ setPitchSlider(randomPitch);
+ setCurrentPlaybackParameters();
+ });
+ }
+
+ resetPresetText = rootView.findViewById(R.id.presetReset);
+ if (resetPresetText != null) {
+ resetPresetText.setOnClickListener(view -> {
+ setTempoSlider(DEFAULT_TEMPO);
+ setPitchSlider(DEFAULT_PITCH);
+ setCurrentPlaybackParameters();
+ });
+ }
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Sliders
+ //////////////////////////////////////////////////////////////////////////*/
+
+ private SeekBar.OnSeekBarChangeListener getOnTempoChangedListener() {
+ return new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ final double currentTempo = strategy.valueOf(progress);
+ if (fromUser) {
+ onTempoSliderUpdated(currentTempo);
+ setCurrentPlaybackParameters();
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ // Do Nothing.
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ // Do Nothing.
+ }
+ };
+ }
+
+ private SeekBar.OnSeekBarChangeListener getOnPitchChangedListener() {
+ return new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ final double currentPitch = strategy.valueOf(progress);
+ if (fromUser) { // this change is first in chain
+ onPitchSliderUpdated(currentPitch);
+ setCurrentPlaybackParameters();
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ // Do Nothing.
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ // Do Nothing.
+ }
+ };
+ }
+
+ private void onTempoSliderUpdated(final double newTempo) {
+ if (unhookingCheckbox == null) return;
+ if (!unhookingCheckbox.isChecked()) {
+ setSliders(newTempo);
+ } else {
+ setTempoSlider(newTempo);
+ }
+ }
+
+ private void onPitchSliderUpdated(final double newPitch) {
+ if (unhookingCheckbox == null) return;
+ if (!unhookingCheckbox.isChecked()) {
+ setSliders(newPitch);
+ } else {
+ setPitchSlider(newPitch);
+ }
+ }
+
+ private void setSliders(final double newValue) {
+ setTempoSlider(newValue);
+ setPitchSlider(newValue);
+ }
+
+ private void setTempoSlider(final double newTempo) {
+ if (tempoSlider == null) return;
+ tempoSlider.setProgress(strategy.progressOf(newTempo));
+ }
+
+ private void setPitchSlider(final double newPitch) {
+ if (pitchSlider == null) return;
+ pitchSlider.setProgress(strategy.progressOf(newPitch));
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Helper
+ //////////////////////////////////////////////////////////////////////////*/
+
+ private void setCurrentPlaybackParameters() {
+ setPlaybackParameters(getCurrentTempo(), getCurrentPitch());
+ }
+
+ private void setPlaybackParameters(final double tempo, final double pitch) {
+ if (callback != null && tempoCurrentText != null && pitchCurrentText != null) {
+ if (DEBUG) Log.d(TAG, "Setting playback parameters to " +
+ "tempo=[" + tempo + "], " +
+ "pitch=[" + pitch + "]");
+
+ tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo));
+ pitchCurrentText.setText(PlayerHelper.formatPitch(pitch));
+ callback.onPlaybackParameterChanged((float) tempo, (float) pitch);
+ }
+ }
+
+ private double getCurrentTempo() {
+ return tempoSlider == null ? initialTempo : strategy.valueOf(
+ tempoSlider.getProgress());
+ }
+
+ private double getCurrentPitch() {
+ return pitchSlider == null ? initialPitch : strategy.valueOf(
+ pitchSlider.getProgress());
+ }
+
+ @NonNull
+ private static String getStepUpPercentString(final double percent) {
+ return STEP_UP_SIGN + PlayerHelper.formatPitch(percent);
+ }
+
+ @NonNull
+ private static String getStepDownPercentString(final double percent) {
+ return STEP_DOWN_SIGN + 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 b34cec724..63ac7e8a1 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
@@ -60,11 +60,11 @@ public class PlayerHelper {
: stringFormatter.format("%02d:%02d", minutes, seconds).toString();
}
- public static String formatSpeed(float speed) {
+ public static String formatSpeed(double speed) {
return speedFormatter.format(speed);
}
- public static String formatPitch(float pitch) {
+ public static String formatPitch(double pitch) {
return pitchFormatter.format(pitch);
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/DummyPlaybackPreparer.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/DummyPlaybackPreparer.java
new file mode 100644
index 000000000..431a90d8a
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/DummyPlaybackPreparer.java
@@ -0,0 +1,45 @@
+package org.schabi.newpipe.player.mediasession;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
+
+public class DummyPlaybackPreparer implements MediaSessionConnector.PlaybackPreparer {
+ @Override
+ public long getSupportedPrepareActions() {
+ return 0;
+ }
+
+ @Override
+ public void onPrepare() {
+
+ }
+
+ @Override
+ public void onPrepareFromMediaId(String mediaId, Bundle extras) {
+
+ }
+
+ @Override
+ public void onPrepareFromSearch(String query, Bundle extras) {
+
+ }
+
+ @Override
+ public void onPrepareFromUri(Uri uri, Bundle extras) {
+
+ }
+
+ @Override
+ public String[] getCommands() {
+ return new String[0];
+ }
+
+ @Override
+ public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
+
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java
new file mode 100644
index 000000000..a1a57a87d
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java
@@ -0,0 +1,17 @@
+package org.schabi.newpipe.player.mediasession;
+
+import android.support.v4.media.MediaDescriptionCompat;
+
+public interface MediaSessionCallback {
+ void onSkipToPrevious();
+ void onSkipToNext();
+ void onSkipToIndex(final int index);
+
+ int getCurrentPlayingIndex();
+ int getQueueSize();
+ MediaDescriptionCompat getQueueMetadata(final int index);
+
+ void onPlay();
+ void onPause();
+ void onSetShuffle(final boolean isShuffled);
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java
new file mode 100644
index 000000000..429c26fd9
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java
@@ -0,0 +1,111 @@
+package org.schabi.newpipe.player.mediasession;
+
+import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.media.session.MediaSessionCompat;
+
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
+import com.google.android.exoplayer2.util.Util;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
+import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
+import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM;
+
+
+public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator {
+ public static final int DEFAULT_MAX_QUEUE_SIZE = 10;
+
+ private final MediaSessionCompat mediaSession;
+ private final MediaSessionCallback callback;
+ private final int maxQueueSize;
+
+ private long activeQueueItemId;
+
+ public PlayQueueNavigator(@NonNull final MediaSessionCompat mediaSession,
+ @NonNull final MediaSessionCallback callback) {
+ this.mediaSession = mediaSession;
+ this.callback = callback;
+ this.maxQueueSize = DEFAULT_MAX_QUEUE_SIZE;
+
+ this.activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
+ }
+
+ @Override
+ public long getSupportedQueueNavigatorActions(@Nullable Player player) {
+ return ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_PREVIOUS | ACTION_SKIP_TO_QUEUE_ITEM;
+ }
+
+ @Override
+ public void onTimelineChanged(Player player) {
+ publishFloatingQueueWindow();
+ }
+
+ @Override
+ public void onCurrentWindowIndexChanged(Player player) {
+ if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID
+ || player.getCurrentTimeline().getWindowCount() > maxQueueSize) {
+ publishFloatingQueueWindow();
+ } else if (!player.getCurrentTimeline().isEmpty()) {
+ activeQueueItemId = player.getCurrentWindowIndex();
+ }
+ }
+
+ @Override
+ public long getActiveQueueItemId(@Nullable Player player) {
+ return callback.getCurrentPlayingIndex();
+ }
+
+ @Override
+ public void onSkipToPrevious(Player player) {
+ callback.onSkipToPrevious();
+ }
+
+ @Override
+ public void onSkipToQueueItem(Player player, long id) {
+ callback.onSkipToIndex((int) id);
+ }
+
+ @Override
+ public void onSkipToNext(Player player) {
+ callback.onSkipToNext();
+ }
+
+ private void publishFloatingQueueWindow() {
+ if (callback.getQueueSize() == 0) {
+ mediaSession.setQueue(Collections.emptyList());
+ activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
+ return;
+ }
+
+ // Yes this is almost a copypasta, got a problem with that? =\
+ int windowCount = callback.getQueueSize();
+ int currentWindowIndex = callback.getCurrentPlayingIndex();
+ int queueSize = Math.min(maxQueueSize, windowCount);
+ int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0,
+ windowCount - queueSize);
+
+ List queue = new ArrayList<>();
+ for (int i = startIndex; i < startIndex + queueSize; i++) {
+ queue.add(new MediaSessionCompat.QueueItem(callback.getQueueMetadata(i), i));
+ }
+ mediaSession.setQueue(queue);
+ activeQueueItemId = currentWindowIndex;
+ }
+
+ @Override
+ public String[] getCommands() {
+ return new String[0];
+ }
+
+ @Override
+ public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
+
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java
new file mode 100644
index 000000000..2aa41bd63
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java
@@ -0,0 +1,31 @@
+package org.schabi.newpipe.player.mediasession;
+
+import android.support.v4.media.session.PlaybackStateCompat;
+
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.ext.mediasession.DefaultPlaybackController;
+
+public class PlayQueuePlaybackController extends DefaultPlaybackController {
+ private final MediaSessionCallback callback;
+
+ public PlayQueuePlaybackController(final MediaSessionCallback callback) {
+ super();
+ this.callback = callback;
+ }
+
+ @Override
+ public void onPlay(Player player) {
+ callback.onPlay();
+ }
+
+ @Override
+ public void onPause(Player player) {
+ callback.onPause();
+ }
+
+ @Override
+ public void onSetShuffleMode(Player player, int shuffleMode) {
+ callback.onSetShuffle(shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL
+ || shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP);
+ }
+}
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 d07baf2a7..5f029cc50 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
@@ -72,7 +72,13 @@ public class FailedMediaSource implements ManagedMediaSource {
public void releaseSource() {}
@Override
- public boolean canReplace(@NonNull final PlayQueueItem newIdentity) {
+ public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity,
+ final boolean isInterruptable) {
return newIdentity != playQueueItem || canRetry();
}
+
+ @Override
+ public boolean isStreamEqual(@NonNull PlayQueueItem stream) {
+ return playQueueItem == stream;
+ }
}
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 f523667f9..fe7508ecc 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
@@ -59,7 +59,13 @@ public class LoadedMediaSource implements ManagedMediaSource {
}
@Override
- public boolean canReplace(@NonNull final PlayQueueItem newIdentity) {
- return newIdentity != stream || isExpired();
+ public boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity,
+ final boolean isInterruptable) {
+ return newIdentity != stream || (isInterruptable && isExpired());
+ }
+
+ @Override
+ public boolean isStreamEqual(@NonNull PlayQueueItem stream) {
+ return this.stream == stream;
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java
index 3bb7ca429..46fd149bb 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java
@@ -7,5 +7,21 @@ import com.google.android.exoplayer2.source.MediaSource;
import org.schabi.newpipe.playlist.PlayQueueItem;
public interface ManagedMediaSource extends MediaSource {
- boolean canReplace(@NonNull final PlayQueueItem newIdentity);
+ /**
+ * Determines whether or not this {@link ManagedMediaSource} can be replaced.
+ *
+ * @param newIdentity a stream the {@link ManagedMediaSource} should encapsulate over, if
+ * it is different from the existing stream in the
+ * {@link ManagedMediaSource}, then it should be replaced.
+ * @param isInterruptable specifies if this {@link ManagedMediaSource} potentially
+ * being played.
+ * */
+ boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity,
+ final boolean isInterruptable);
+
+ /**
+ * Determines if the {@link PlayQueueItem} is the one the
+ * {@link ManagedMediaSource} encapsulates over.
+ * */
+ boolean isStreamEqual(@NonNull final PlayQueueItem stream);
}
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 0d3436a01..2c57f2f9c 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
@@ -19,7 +19,13 @@ public class PlaceholderMediaSource implements ManagedMediaSource {
@Override public void releaseSource() {}
@Override
- public boolean canReplace(@NonNull final PlayQueueItem newIdentity) {
+ public boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity,
+ final boolean isInterruptable) {
return true;
}
+
+ @Override
+ public boolean isStreamEqual(@NonNull PlayQueueItem stream) {
+ return false;
+ }
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java b/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java
new file mode 100644
index 000000000..616879917
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java
@@ -0,0 +1,77 @@
+package org.schabi.newpipe.player.playback;
+
+import android.net.Uri;
+import android.support.v4.media.MediaDescriptionCompat;
+
+import org.schabi.newpipe.player.BasePlayer;
+import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
+import org.schabi.newpipe.playlist.PlayQueueItem;
+
+public class BasePlayerMediaSession implements MediaSessionCallback {
+ private BasePlayer player;
+
+ public BasePlayerMediaSession(final BasePlayer player) {
+ this.player = player;
+ }
+
+ @Override
+ public void onSkipToPrevious() {
+ player.onPlayPrevious();
+ }
+
+ @Override
+ public void onSkipToNext() {
+ player.onPlayNext();
+ }
+
+ @Override
+ public void onSkipToIndex(int index) {
+ if (player.getPlayQueue() == null) return;
+ player.onSelected(player.getPlayQueue().getItem(index));
+ }
+
+ @Override
+ public int getCurrentPlayingIndex() {
+ if (player.getPlayQueue() == null) return -1;
+ return player.getPlayQueue().getIndex();
+ }
+
+ @Override
+ public int getQueueSize() {
+ if (player.getPlayQueue() == null) return -1;
+ return player.getPlayQueue().size();
+ }
+
+ @Override
+ public MediaDescriptionCompat getQueueMetadata(int index) {
+ if (player.getPlayQueue() == null || player.getPlayQueue().getItem(index) == null) {
+ return null;
+ }
+
+ final PlayQueueItem item = player.getPlayQueue().getItem(index);
+ MediaDescriptionCompat.Builder descriptionBuilder = new MediaDescriptionCompat.Builder()
+ .setMediaId(String.valueOf(index))
+ .setTitle(item.getTitle())
+ .setSubtitle(item.getUploader());
+
+ final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl());
+ if (thumbnailUri != null) descriptionBuilder.setIconUri(thumbnailUri);
+
+ return descriptionBuilder.build();
+ }
+
+ @Override
+ public void onPlay() {
+ player.onPlay();
+ }
+
+ @Override
+ public void onPause() {
+ player.onPause();
+ }
+
+ @Override
+ public void onSetShuffle(boolean isShuffled) {
+ player.onShuffleModeEnabledChanged(isShuffled);
+ }
+}
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 cb803dcd1..477358113 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
@@ -21,15 +21,15 @@ import org.schabi.newpipe.playlist.events.MoveEvent;
import org.schabi.newpipe.playlist.events.PlayQueueEvent;
import org.schabi.newpipe.playlist.events.RemoveEvent;
import org.schabi.newpipe.playlist.events.ReorderEvent;
+import org.schabi.newpipe.util.ServiceHelper;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
-import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
+import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
@@ -42,7 +42,7 @@ import io.reactivex.subjects.PublishSubject;
import static org.schabi.newpipe.playlist.PlayQueue.DEBUG;
public class MediaSourceManager {
- @NonNull private final static String TAG = "MediaSourceManager";
+ @NonNull private final String TAG = "MediaSourceManager@" + hashCode();
/**
* Determines how many streams before and after the current stream should be loaded.
@@ -60,17 +60,18 @@ public class MediaSourceManager {
@NonNull private final PlayQueue playQueue;
/**
- * Determines how long NEIGHBOURING {@link LoadedMediaSource} window of a currently playing
- * {@link MediaSource} is allowed to stay in the playlist timeline. This is to ensure
- * the {@link StreamInfo} used in subsequent playback is up-to-date.
- *
- * Once a {@link LoadedMediaSource} has expired, a new source will be reloaded to
- * replace the expired one on whereupon {@link #loadImmediate()} is called.
+ * Determines the gap time between the playback position and the playback duration which
+ * the {@link #getEdgeIntervalSignal()} begins to request loading.
*
- * @see #loadImmediate()
- * @see #isCorrectionNeeded(PlayQueueItem)
+ * @see #progressUpdateIntervalMillis
* */
- private final long windowRefreshTimeMillis;
+ private final long playbackNearEndGapMillis;
+ /**
+ * Determines the interval which the {@link #getEdgeIntervalSignal()} waits for between
+ * each request for loading, once {@link #playbackNearEndGapMillis} has reached.
+ * */
+ private final long progressUpdateIntervalMillis;
+ @NonNull private final Observable nearEndIntervalSignal;
/**
* Process only the last load order when receiving a stream of load orders (lessens I/O).
@@ -106,23 +107,31 @@ public class MediaSourceManager {
public MediaSourceManager(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue) {
- this(listener, playQueue,
- /*loadDebounceMillis=*/400L,
- /*windowRefreshTimeMillis=*/TimeUnit.MILLISECONDS.convert(10, TimeUnit.MINUTES));
+ this(listener, playQueue, /*loadDebounceMillis=*/400L,
+ /*playbackNearEndGapMillis=*/TimeUnit.MILLISECONDS.convert(30, TimeUnit.SECONDS),
+ /*progressUpdateIntervalMillis*/TimeUnit.MILLISECONDS.convert(2, TimeUnit.SECONDS));
}
private MediaSourceManager(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue,
final long loadDebounceMillis,
- final long windowRefreshTimeMillis) {
+ final long playbackNearEndGapMillis,
+ final long progressUpdateIntervalMillis) {
if (playQueue.getBroadcastReceiver() == null) {
throw new IllegalArgumentException("Play Queue has not been initialized.");
}
+ if (playbackNearEndGapMillis < progressUpdateIntervalMillis) {
+ throw new IllegalArgumentException("Playback end gap=[" + playbackNearEndGapMillis +
+ " ms] must be longer than update interval=[ " + progressUpdateIntervalMillis +
+ " ms] for them to be useful.");
+ }
this.playbackListener = listener;
this.playQueue = playQueue;
- this.windowRefreshTimeMillis = windowRefreshTimeMillis;
+ this.playbackNearEndGapMillis = playbackNearEndGapMillis;
+ this.progressUpdateIntervalMillis = progressUpdateIntervalMillis;
+ this.nearEndIntervalSignal = getEdgeIntervalSignal();
this.loadDebounceMillis = loadDebounceMillis;
this.debouncedSignal = PublishSubject.create();
@@ -161,28 +170,6 @@ public class MediaSourceManager {
sources.releaseSource();
}
- /**
- * Loads the current playing stream and the streams within its windowSize bound.
- *
- * Unblocks the player once the item at the current index is loaded.
- * */
- public void load() {
- if (DEBUG) Log.d(TAG, "load() called.");
- loadDebounced();
- }
-
- /**
- * Blocks the player and repopulate the sources.
- *
- * Does not ensure the player is unblocked and should be done explicitly
- * through {@link #load() load}.
- * */
- public void reset() {
- if (DEBUG) Log.d(TAG, "reset() called.");
-
- maybeBlock();
- populateSources();
- }
/*//////////////////////////////////////////////////////////////////////////
// Event Reactor
//////////////////////////////////////////////////////////////////////////*/
@@ -219,11 +206,13 @@ public class MediaSourceManager {
switch (event.type()) {
case INIT:
case ERROR:
- reset();
- break;
+ maybeBlock();
case APPEND:
populateSources();
break;
+ case SELECT:
+ maybeRenewCurrentIndex();
+ break;
case REMOVE:
final RemoveEvent removeEvent = (RemoveEvent) event;
remove(removeEvent.getRemoveIndex());
@@ -238,7 +227,6 @@ public class MediaSourceManager {
final ReorderEvent reorderEvent = (ReorderEvent) event;
move(reorderEvent.getFromSelectedIndex(), reorderEvent.getToSelectedIndex());
break;
- case SELECT:
case RECOVERY:
default:
break;
@@ -280,15 +268,10 @@ public class MediaSourceManager {
private boolean isPlaybackReady() {
if (sources.getSize() != playQueue.size()) return false;
- final MediaSource mediaSource = sources.getMediaSource(playQueue.getIndex());
+ final ManagedMediaSource mediaSource =
+ (ManagedMediaSource) sources.getMediaSource(playQueue.getIndex());
final PlayQueueItem playQueueItem = playQueue.getItem();
-
- if (mediaSource instanceof LoadedMediaSource) {
- return playQueueItem == ((LoadedMediaSource) mediaSource).getStream();
- } else if (mediaSource instanceof FailedMediaSource) {
- return playQueueItem == ((FailedMediaSource) mediaSource).getStream();
- }
- return false;
+ return mediaSource.isStreamEqual(playQueueItem);
}
private void maybeBlock() {
@@ -319,7 +302,7 @@ public class MediaSourceManager {
if (DEBUG) Log.d(TAG, "onPlaybackSynchronize() called.");
final PlayQueueItem currentItem = playQueue.getItem();
- if (isBlocked.get() || currentItem == null) return;
+ if (isBlocked.get() || !isPlaybackReady() || currentItem == null) return;
final Consumer onSuccess = info -> syncInternal(currentItem, info);
final Consumer onError = throwable -> syncInternal(currentItem, null);
@@ -347,8 +330,13 @@ public class MediaSourceManager {
// MediaSource Loading
//////////////////////////////////////////////////////////////////////////*/
+ private Observable getEdgeIntervalSignal() {
+ return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS)
+ .filter(ignored -> playbackListener.isNearPlaybackEdge(playbackNearEndGapMillis));
+ }
+
private Disposable getDebouncedLoader() {
- return debouncedSignal
+ return debouncedSignal.mergeWith(nearEndIntervalSignal)
.debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(timestamp -> loadImmediate());
@@ -359,13 +347,14 @@ public class MediaSourceManager {
}
private void loadImmediate() {
+ if (DEBUG) Log.d(TAG, "MediaSource - loadImmediate() called");
// The current item has higher priority
final int currentIndex = playQueue.getIndex();
final PlayQueueItem currentItem = playQueue.getItem(currentIndex);
if (currentItem == null) return;
// Evict the items being loaded to free up memory
- if (!loadingItems.contains(currentItem) && loaderReactor.size() > MAXIMUM_LOADER_SIZE) {
+ if (loaderReactor.size() > MAXIMUM_LOADER_SIZE) {
loaderReactor.clear();
loadingItems.clear();
}
@@ -377,7 +366,7 @@ public class MediaSourceManager {
final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE);
final int rightLimit = currentIndex + WINDOW_SIZE + 1;
final int rightBound = Math.min(playQueue.size(), rightLimit);
- final List items = new ArrayList<>(
+ final Set items = new HashSet<>(
playQueue.getStreams().subList(leftBound,rightBound));
// Do a round robin
@@ -385,6 +374,7 @@ public class MediaSourceManager {
if (excess >= 0) {
items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
}
+ items.remove(currentItem);
for (final PlayQueueItem item : items) {
maybeLoadItem(item);
@@ -406,8 +396,6 @@ public class MediaSourceManager {
.subscribe(mediaSource -> onMediaSourceReceived(item, mediaSource));
loaderReactor.add(loader);
}
-
- maybeSynchronizePlayer();
}
private Single getLoadedMediaSource(@NonNull final PlayQueueItem stream) {
@@ -417,13 +405,14 @@ public class MediaSourceManager {
final Exception exception = new IllegalStateException(
"Unable to resolve source from stream info." +
" URL: " + stream.getUrl() +
- ", audio count: " + streamInfo.audio_streams.size() +
- ", video count: " + streamInfo.video_only_streams.size() +
- streamInfo.video_streams.size());
+ ", audio count: " + streamInfo.getAudioStreams().size() +
+ ", video count: " + streamInfo.getVideoOnlyStreams().size() +
+ streamInfo.getVideoStreams().size());
return new FailedMediaSource(stream, exception);
}
- final long expiration = System.currentTimeMillis() + windowRefreshTimeMillis;
+ final long expiration = System.currentTimeMillis() +
+ ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId());
return new LoadedMediaSource(source, stream, expiration);
}).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable));
}
@@ -459,14 +448,37 @@ public class MediaSourceManager {
if (index == -1 || index >= sources.getSize()) return false;
final ManagedMediaSource mediaSource = (ManagedMediaSource) sources.getMediaSource(index);
-
- if (index == playQueue.getIndex() && mediaSource instanceof LoadedMediaSource) {
- return item != ((LoadedMediaSource) mediaSource).getStream();
- } else {
- return mediaSource.canReplace(item);
- }
+ return mediaSource.shouldBeReplacedWith(item,
+ /*mightBeInProgress=*/index != playQueue.getIndex());
}
+ /**
+ * Checks if the current playing index contains an expired {@link ManagedMediaSource}.
+ * If so, the expired source is replaced by a {@link PlaceholderMediaSource} and
+ * {@link #loadImmediate()} is called to reload the current item.
+ *
+ * If not, then the media source at the current index is ready for playback, and
+ * {@link #maybeSynchronizePlayer()} is called.
+ *
+ * Under both cases, {@link #maybeSync()} will be called to ensure the listener
+ * is up-to-date.
+ * */
+ private void maybeRenewCurrentIndex() {
+ final int currentIndex = playQueue.getIndex();
+ if (sources.getSize() <= currentIndex) return;
+
+ final ManagedMediaSource currentSource =
+ (ManagedMediaSource) sources.getMediaSource(currentIndex);
+ final PlayQueueItem currentItem = playQueue.getItem();
+ if (!currentSource.shouldBeReplacedWith(currentItem, /*canInterruptOnRenew=*/true)) {
+ maybeSynchronizePlayer();
+ return;
+ }
+
+ if (DEBUG) Log.d(TAG, "MediaSource - Reloading currently playing, " +
+ "index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]");
+ update(currentIndex, new PlaceholderMediaSource(), this::loadImmediate);
+ }
/*//////////////////////////////////////////////////////////////////////////
// MediaSource Playlist Helpers
//////////////////////////////////////////////////////////////////////////*/
@@ -476,6 +488,7 @@ public class MediaSourceManager {
this.sources.releaseSource();
this.sources = new DynamicConcatenatingMediaSource(false,
+ // Shuffling is done on PlayQueue, thus no need to use ExoPlayer's shuffle order
new ShuffleOrder.UnshuffledShuffleOrder(0));
}
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 b37a269e2..34c7702bc 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
@@ -11,6 +11,16 @@ import org.schabi.newpipe.playlist.PlayQueueItem;
import java.util.List;
public interface PlaybackListener {
+
+ /**
+ * Called to check if the currently playing stream is close to the end of its playback.
+ * Implementation should return true when the current playback position is within
+ * timeToEndMillis or less until its playback completes or transitions.
+ *
+ * May be called at any time.
+ * */
+ boolean isNearPlaybackEdge(final long timeToEndMillis);
+
/**
* Called when the stream at the current queue index is not ready yet.
* Signals to the listener to block the player from playing anything and notify the source
diff --git a/app/src/main/java/org/schabi/newpipe/playlist/AbstractInfoPlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/AbstractInfoPlayQueue.java
index 6e63a3aaa..2b31cd340 100644
--- a/app/src/main/java/org/schabi/newpipe/playlist/AbstractInfoPlayQueue.java
+++ b/app/src/main/java/org/schabi/newpipe/playlist/AbstractInfoPlayQueue.java
@@ -26,13 +26,13 @@ abstract class AbstractInfoPlayQueue ext
transient Disposable fetchReactor;
AbstractInfoPlayQueue(final U item) {
- this(item.getServiceId(), item.getUrl(), null, Collections.emptyList(), 0);
+ this(item.getServiceId(), item.getUrl(), null, Collections.emptyList(), 0);
}
AbstractInfoPlayQueue(final int serviceId,
final String url,
final String nextPageUrl,
- final List streams,
+ final List streams,
final int index) {
super(index, extractListItems(streams));
@@ -65,10 +65,10 @@ abstract class AbstractInfoPlayQueue ext
@Override
public void onSuccess(@NonNull T result) {
isInitial = false;
- if (!result.has_more_streams) isComplete = true;
- nextUrl = result.next_streams_url;
+ if (!result.hasNextPage()) isComplete = true;
+ nextUrl = result.getNextPageUrl();
- append(extractListItems(result.related_streams));
+ append(extractListItems(result.getRelatedItems()));
fetchReactor.dispose();
fetchReactor = null;
@@ -83,8 +83,8 @@ abstract class AbstractInfoPlayQueue ext
};
}
- SingleObserver getNextPageObserver() {
- return new SingleObserver() {
+ SingleObserver getNextPageObserver() {
+ return new SingleObserver() {
@Override
public void onSubscribe(@NonNull Disposable d) {
if (isComplete || isInitial || (fetchReactor != null && !fetchReactor.isDisposed())) {
@@ -95,11 +95,11 @@ abstract class AbstractInfoPlayQueue ext
}
@Override
- public void onSuccess(@NonNull ListExtractor.InfoItemPage result) {
+ public void onSuccess(@NonNull ListExtractor.InfoItemsPage result) {
if (!result.hasNextPage()) isComplete = true;
- nextUrl = result.nextPageUrl;
+ nextUrl = result.getNextPageUrl();
- append(extractListItems(result.infoItemList));
+ append(extractListItems(result.getItems()));
fetchReactor.dispose();
fetchReactor = null;
@@ -121,7 +121,7 @@ abstract class AbstractInfoPlayQueue ext
fetchReactor = null;
}
- private static List extractListItems(final List infos) {
+ private static List extractListItems(final List infos) {
List result = new ArrayList<>();
for (final InfoItem stream : infos) {
if (stream instanceof StreamInfoItem) {
diff --git a/app/src/main/java/org/schabi/newpipe/playlist/ChannelPlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/ChannelPlayQueue.java
index a5ecad027..d37b84072 100644
--- a/app/src/main/java/org/schabi/newpipe/playlist/ChannelPlayQueue.java
+++ b/app/src/main/java/org/schabi/newpipe/playlist/ChannelPlayQueue.java
@@ -3,6 +3,7 @@ package org.schabi.newpipe.playlist;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
+import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.util.ExtractorHelper;
import java.util.List;
@@ -16,13 +17,13 @@ public final class ChannelPlayQueue extends AbstractInfoPlayQueue streams,
+ final List streams,
final int index) {
super(serviceId, url, nextPageUrl, streams, index);
}
diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java
index 752dc223d..df4d19720 100644
--- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java
+++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java
@@ -11,20 +11,19 @@ import org.schabi.newpipe.util.ExtractorHelper;
import java.io.Serializable;
import io.reactivex.Single;
-import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
public class PlayQueueItem implements Serializable {
- final public static long RECOVERY_UNSET = Long.MIN_VALUE;
+ public final static long RECOVERY_UNSET = Long.MIN_VALUE;
+ private final static String EMPTY_STRING = "";
- final private String title;
- final private String url;
+ @NonNull final private String title;
+ @NonNull final private String url;
final private int serviceId;
final private long duration;
- final private String thumbnailUrl;
- final private String uploader;
- final private StreamType streamType;
+ @NonNull final private String thumbnailUrl;
+ @NonNull final private String uploader;
+ @NonNull final private StreamType streamType;
private long recoveryPosition;
private Throwable error;
@@ -42,15 +41,16 @@ public class PlayQueueItem implements Serializable {
item.getThumbnailUrl(), item.getUploaderName(), item.getStreamType());
}
- private PlayQueueItem(final String name, final String url, final int serviceId,
- final long duration, final String thumbnailUrl, final String uploader,
- final StreamType streamType) {
- this.title = name;
- this.url = url;
+ private PlayQueueItem(@Nullable final String name, @Nullable final String url,
+ final int serviceId, final long duration,
+ @Nullable final String thumbnailUrl, @Nullable final String uploader,
+ @NonNull final StreamType streamType) {
+ this.title = name != null ? name : EMPTY_STRING;
+ this.url = url != null ? url : EMPTY_STRING;
this.serviceId = serviceId;
this.duration = duration;
- this.thumbnailUrl = thumbnailUrl;
- this.uploader = uploader;
+ this.thumbnailUrl = thumbnailUrl != null ? thumbnailUrl : EMPTY_STRING;
+ this.uploader = uploader != null ? uploader : EMPTY_STRING;
this.streamType = streamType;
this.recoveryPosition = RECOVERY_UNSET;
@@ -84,6 +84,7 @@ public class PlayQueueItem implements Serializable {
return uploader;
}
+ @NonNull
public StreamType getStreamType() {
return streamType;
}
diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java
index 73cdf1113..7042bea89 100644
--- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java
+++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java
@@ -1,28 +1,22 @@
package org.schabi.newpipe.playlist;
import android.content.Context;
-import android.graphics.Bitmap;
import android.text.TextUtils;
import android.view.MotionEvent;
import android.view.View;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
-import com.nostra13.universalimageloader.core.assist.ImageScaleType;
-import com.nostra13.universalimageloader.core.process.BitmapProcessor;
import org.schabi.newpipe.R;
+import org.schabi.newpipe.extractor.NewPipe;
+import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
-
public class PlayQueueItemBuilder {
private static final String TAG = PlayQueueItemBuilder.class.toString();
- private final int thumbnailWidthPx;
- private final int thumbnailHeightPx;
- private final DisplayImageOptions imageOptions;
-
public interface OnSelectedListener {
void selected(PlayQueueItem item, View view);
void held(PlayQueueItem item, View view);
@@ -31,11 +25,7 @@ public class PlayQueueItemBuilder {
private OnSelectedListener onItemClickListener;
- public PlayQueueItemBuilder(final Context context) {
- thumbnailWidthPx = context.getResources().getDimensionPixelSize(R.dimen.play_queue_thumbnail_width);
- thumbnailHeightPx = context.getResources().getDimensionPixelSize(R.dimen.play_queue_thumbnail_height);
- imageOptions = buildImageOptions(thumbnailWidthPx, thumbnailHeightPx);
- }
+ public PlayQueueItemBuilder(final Context context) {}
public void setOnSelectedListener(OnSelectedListener listener) {
this.onItemClickListener = listener;
@@ -43,7 +33,8 @@ public class PlayQueueItemBuilder {
public void buildStreamInfoItem(final PlayQueueItemHolder holder, final PlayQueueItem item) {
if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle());
- if (!TextUtils.isEmpty(item.getUploader())) holder.itemAdditionalDetailsView.setText(item.getUploader());
+ holder.itemAdditionalDetailsView.setText(Localization.concatenateStrings(item.getUploader(),
+ NewPipe.getNameOfService(item.getServiceId())));
if (item.getDuration() > 0) {
holder.itemDurationView.setText(Localization.getDurationString(item.getDuration()));
@@ -51,7 +42,8 @@ public class PlayQueueItemBuilder {
holder.itemDurationView.setVisibility(View.GONE);
}
- ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView, imageOptions);
+ ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView,
+ ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
holder.itemRoot.setOnClickListener(view -> {
if (onItemClickListener != null) {
@@ -81,23 +73,4 @@ public class PlayQueueItemBuilder {
return false;
};
}
-
- private DisplayImageOptions buildImageOptions(final int widthPx, final int heightPx) {
- final BitmapProcessor bitmapProcessor = bitmap -> {
- final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, widthPx, heightPx, false);
- bitmap.recycle();
- return resizedBitmap;
- };
-
- return new DisplayImageOptions.Builder()
- .showImageOnFail(R.drawable.dummy_thumbnail)
- .showImageForEmptyUri(R.drawable.dummy_thumbnail)
- .showImageOnLoading(R.drawable.dummy_thumbnail)
- .bitmapConfig(Bitmap.Config.RGB_565) // Users won't be able to see much anyways
- .preProcessor(bitmapProcessor)
- .imageScaleType(ImageScaleType.EXACTLY)
- .cacheInMemory(true)
- .cacheOnDisk(true)
- .build();
- }
}
diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemTouchCallback.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemTouchCallback.java
new file mode 100644
index 000000000..405dba11e
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemTouchCallback.java
@@ -0,0 +1,52 @@
+package org.schabi.newpipe.playlist;
+
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.helper.ItemTouchHelper;
+
+public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleCallback {
+ private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 10;
+ private static final int MAXIMUM_INITIAL_DRAG_VELOCITY = 25;
+
+ public PlayQueueItemTouchCallback() {
+ super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0);
+ }
+
+ public abstract void onMove(final int sourceIndex, final int targetIndex);
+
+ @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 clampedAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
+ Math.min(Math.abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY));
+ return clampedAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
+ }
+
+ @Override
+ public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source,
+ RecyclerView.ViewHolder target) {
+ if (source.getItemViewType() != target.getItemViewType()) {
+ return false;
+ }
+
+ final int sourceIndex = source.getLayoutPosition();
+ final int targetIndex = target.getLayoutPosition();
+ onMove(sourceIndex, targetIndex);
+ return true;
+ }
+
+ @Override
+ public boolean isLongPressDragEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isItemViewSwipeEnabled() {
+ return false;
+ }
+
+ @Override
+ public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {}
+}
diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlaylistPlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/PlaylistPlayQueue.java
index c9340afad..d9e1d2d2b 100644
--- a/app/src/main/java/org/schabi/newpipe/playlist/PlaylistPlayQueue.java
+++ b/app/src/main/java/org/schabi/newpipe/playlist/PlaylistPlayQueue.java
@@ -3,6 +3,7 @@ package org.schabi.newpipe.playlist;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
+import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.util.ExtractorHelper;
import java.util.List;
@@ -16,13 +17,13 @@ public final class PlaylistPlayQueue extends AbstractInfoPlayQueue streams,
+ final List streams,
final int index) {
super(serviceId, url, nextPageUrl, streams, index);
}
diff --git a/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java b/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java
index 8e3509032..017c094aa 100644
--- a/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java
@@ -77,12 +77,10 @@ public class ErrorActivity extends AppCompatActivity {
public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org";
public static final String ERROR_EMAIL_SUBJECT = "Exception in NewPipe " + BuildConfig.VERSION_NAME;
- Thread globIpRangeThread;
private String[] errorList;
private ErrorInfo errorInfo;
private Class returnActivity;
private String currentTimeStamp;
- private String globIpRange;
// views
private TextView errorView;
private EditText userCommentBox;
@@ -224,9 +222,6 @@ public class ErrorActivity extends AppCompatActivity {
});
reportButton.setEnabled(false);
- globIpRangeThread = new Thread(new IpRangeRequester());
- globIpRangeThread.start();
-
// normal bugreport
buildInfo(errorInfo);
if (errorInfo.message != 0) {
@@ -342,8 +337,7 @@ public class ErrorActivity extends AppCompatActivity {
.put("package", getPackageName())
.put("version", BuildConfig.VERSION_NAME)
.put("os", getOsString())
- .put("time", currentTimeStamp)
- .put("ip_range", globIpRange);
+ .put("time", currentTimeStamp);
JSONArray exceptionArray = new JSONArray();
if (errorList != null) {
@@ -454,41 +448,4 @@ public class ErrorActivity extends AppCompatActivity {
dest.writeInt(this.message);
}
}
-
- private class IpRangeRequester implements Runnable {
- Handler h = new Handler();
-
- public void run() {
- String ipRange = "none";
- try {
- Downloader dl = Downloader.getInstance();
- String ip = dl.download("https://ipv4.icanhazip.com");
-
- ipRange = Parser.matchGroup1("([0-9]*\\.[0-9]*\\.)[0-9]*\\.[0-9]*", ip)
- + "0.0";
- } catch (Throwable e) {
- Log.w(TAG, "Error while error: could not get iprange", e);
- } finally {
- h.post(new IpRangeReturnRunnable(ipRange));
- }
- }
- }
-
- private class IpRangeReturnRunnable implements Runnable {
- String ipRange;
-
- public IpRangeReturnRunnable(String ipRange) {
- this.ipRange = ipRange;
- }
-
- public void run() {
- globIpRange = ipRange;
- if (infoView != null) {
- String text = infoView.getText().toString();
- text += "\n" + globIpRange;
- infoView.setText(text);
- reportButton.setEnabled(true);
- }
- }
- }
}
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 26278ac75..f0ab3bc03 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java
@@ -6,12 +6,14 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.v7.preference.ListPreference;
import android.support.v7.preference.Preference;
import android.util.Log;
import android.widget.Toast;
import com.nononsenseapps.filepicker.Utils;
+import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
@@ -47,6 +49,29 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
private File newpipe_db;
private File newpipe_db_journal;
+ private String thumbnailLoadToggleKey;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ thumbnailLoadToggleKey = getString(R.string.download_thumbnail_key);
+ }
+
+ @Override
+ public boolean onPreferenceTreeClick(Preference preference) {
+ if (preference.getKey().equals(thumbnailLoadToggleKey)) {
+ final ImageLoader imageLoader = ImageLoader.getInstance();
+ imageLoader.stop();
+ imageLoader.clearDiskCache();
+ imageLoader.clearMemoryCache();
+ imageLoader.resume();
+ Toast.makeText(preference.getContext(), R.string.thumbnail_cache_wipe_complete_notice,
+ Toast.LENGTH_SHORT).show();
+ }
+
+ return super.onPreferenceTreeClick(preference);
+ }
+
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
diff --git a/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java
index e0836e06c..53e8d6fc4 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java
@@ -1,12 +1,35 @@
package org.schabi.newpipe.settings;
import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.preference.Preference;
+import android.widget.Toast;
import org.schabi.newpipe.R;
+import org.schabi.newpipe.util.InfoCache;
public class HistorySettingsFragment extends BasePreferenceFragment {
+ private String cacheWipeKey;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ cacheWipeKey = getString(R.string.metadata_cache_wipe_key);
+ }
+
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.history_settings);
}
+
+ @Override
+ public boolean onPreferenceTreeClick(Preference preference) {
+ if (preference.getKey().equals(cacheWipeKey)) {
+ InfoCache.getInstance().clearCache();
+ Toast.makeText(preference.getContext(), R.string.metadata_cache_wipe_complete_notice,
+ Toast.LENGTH_SHORT).show();
+ }
+
+ return super.onPreferenceTreeClick(preference);
+ }
}
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 12c3dc44c..1897589c6 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java
@@ -29,7 +29,7 @@ import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.extractor.Info;
-import org.schabi.newpipe.extractor.ListExtractor.InfoItemPage;
+import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
@@ -78,7 +78,7 @@ public final class ExtractorHelper {
);
}
- public static Single getMoreSearchItems(final int serviceId,
+ public static Single getMoreSearchItems(final int serviceId,
final String query,
final int nextPageNumber,
final String searchLanguage,
@@ -86,7 +86,7 @@ public final class ExtractorHelper {
checkServiceId(serviceId);
return searchFor(serviceId, query, nextPageNumber, searchLanguage, filter)
.map((@NonNull SearchResult searchResult) ->
- new InfoItemPage(searchResult.resultList,
+ new InfoItemsPage(searchResult.resultList,
nextPageNumber + "",
searchResult.errors));
}
@@ -117,7 +117,7 @@ public final class ExtractorHelper {
ChannelInfo.getInfo(NewPipe.getService(serviceId), url)));
}
- public static Single getMoreChannelItems(final int serviceId,
+ public static Single getMoreChannelItems(final int serviceId,
final String url,
final String nextStreamsUrl) {
checkServiceId(serviceId);
@@ -133,7 +133,7 @@ public final class ExtractorHelper {
PlaylistInfo.getInfo(NewPipe.getService(serviceId), url)));
}
- public static Single getMorePlaylistItems(final int serviceId,
+ public static Single getMorePlaylistItems(final int serviceId,
final String url,
final String nextStreamsUrl) {
checkServiceId(serviceId);
@@ -149,7 +149,7 @@ public final class ExtractorHelper {
KioskInfo.getInfo(NewPipe.getService(serviceId), url, contentCountry)));
}
- public static Single getMoreKioskItems(final int serviceId,
+ public static Single getMoreKioskItems(final int serviceId,
final String url,
final String nextStreamsUrl,
final String contentCountry) {
diff --git a/app/src/main/java/org/schabi/newpipe/util/ImageDisplayConstants.java b/app/src/main/java/org/schabi/newpipe/util/ImageDisplayConstants.java
new file mode 100644
index 000000000..9ee8a1095
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/ImageDisplayConstants.java
@@ -0,0 +1,58 @@
+package org.schabi.newpipe.util;
+
+import android.graphics.Bitmap;
+
+import com.nostra13.universalimageloader.core.DisplayImageOptions;
+import com.nostra13.universalimageloader.core.assist.ImageScaleType;
+import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
+
+import org.schabi.newpipe.R;
+
+public class ImageDisplayConstants {
+ private static final int BITMAP_FADE_IN_DURATION_MILLIS = 250;
+
+ /**
+ * Base display options
+ */
+ private static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS =
+ new DisplayImageOptions.Builder()
+ .cacheInMemory(true)
+ .cacheOnDisk(true)
+ .resetViewBeforeLoading(true)
+ .bitmapConfig(Bitmap.Config.RGB_565)
+ .imageScaleType(ImageScaleType.EXACTLY)
+ .displayer(new FadeInBitmapDisplayer(BITMAP_FADE_IN_DURATION_MILLIS))
+ .build();
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // DisplayImageOptions default configurations
+ //////////////////////////////////////////////////////////////////////////*/
+
+ public static final DisplayImageOptions DISPLAY_AVATAR_OPTIONS =
+ new DisplayImageOptions.Builder()
+ .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
+ .showImageForEmptyUri(R.drawable.buddy)
+ .showImageOnFail(R.drawable.buddy)
+ .build();
+
+ public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
+ new DisplayImageOptions.Builder()
+ .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
+ .showImageForEmptyUri(R.drawable.dummy_thumbnail)
+ .showImageOnFail(R.drawable.dummy_thumbnail)
+ .build();
+
+ public static final DisplayImageOptions DISPLAY_BANNER_OPTIONS =
+ new DisplayImageOptions.Builder()
+ .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
+ .showImageForEmptyUri(R.drawable.channel_banner)
+ .showImageOnFail(R.drawable.channel_banner)
+ .build();
+
+ public static final DisplayImageOptions DISPLAY_PLAYLIST_OPTIONS =
+ new DisplayImageOptions.Builder()
+ .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
+ .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist)
+ .showImageOnFail(R.drawable.dummy_thumbnail_playlist)
+ .build();
+}
diff --git a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java
index 47c45e82a..ecc66bb40 100644
--- a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java
+++ b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java
@@ -43,7 +43,6 @@ public final class InfoCache {
* Trim the cache to this size
*/
private static final int TRIM_CACHE_TO = 30;
- private static final int DEFAULT_TIMEOUT_HOURS = 4;
private static final LruCache lruCache = new LruCache<>(MAX_ITEMS_ON_CACHE);
@@ -66,13 +65,7 @@ public final class InfoCache {
public void putInfo(int serviceId, @NonNull String url, @NonNull Info info) {
if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]");
- final long expirationMillis;
- if (info.getServiceId() == SoundCloud.getServiceId()) {
- expirationMillis = TimeUnit.MILLISECONDS.convert(15, TimeUnit.MINUTES);
- } else {
- expirationMillis = TimeUnit.MILLISECONDS.convert(DEFAULT_TIMEOUT_HOURS, TimeUnit.HOURS);
- }
-
+ final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId());
synchronized (lruCache) {
final CacheData data = new CacheData(info, expirationMillis);
lruCache.put(keyOf(serviceId, url), data);
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 7d71750eb..9d71ae83a 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java
@@ -12,6 +12,10 @@ import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
+import java.util.concurrent.TimeUnit;
+
+import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
+
public class ServiceHelper {
private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube;
@@ -98,4 +102,12 @@ public class ServiceHelper {
PreferenceManager.getDefaultSharedPreferences(context).edit().
putString(context.getString(R.string.current_service_key), serviceName).apply();
}
+
+ public static long getCacheExpirationMillis(final int serviceId) {
+ if (serviceId == SoundCloud.getServiceId()) {
+ return TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES);
+ } else {
+ return TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS);
+ }
+ }
}
diff --git a/app/src/main/java/org/schabi/newpipe/util/SliderStrategy.java b/app/src/main/java/org/schabi/newpipe/util/SliderStrategy.java
new file mode 100644
index 000000000..efec1abb0
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/SliderStrategy.java
@@ -0,0 +1,73 @@
+package org.schabi.newpipe.util;
+
+public interface SliderStrategy {
+ /**
+ * Converts from zeroed double with a minimum offset to the nearest rounded slider
+ * equivalent integer
+ * */
+ int progressOf(final double value);
+
+ /**
+ * Converts from slider integer value to an equivalent double value with a given
+ * minimum offset
+ * */
+ double valueOf(final int progress);
+
+ // TODO: also implement linear strategy when needed
+
+ final class Quadratic implements SliderStrategy {
+ private final double leftGap;
+ private final double rightGap;
+ private final double center;
+
+ private final int centerProgress;
+
+ /**
+ * Quadratic slider strategy that scales the value of a slider given how far the slider
+ * progress is from the center of the slider. The further away from the center,
+ * the faster the interpreted value changes, and vice versa.
+ *
+ * @param minimum the minimum value of the interpreted value of the slider.
+ * @param maximum the maximum value of the interpreted value of the slider.
+ * @param center center of the interpreted value between the minimum and maximum, which
+ * will be used as the center value on the slider progress. Doesn't need
+ * to be the average of the minimum and maximum values, but must be in
+ * between the two.
+ * @param maxProgress the maximum possible progress of the slider, this is the
+ * value that is shown for the UI and controls the granularity of
+ * the slider. Should be as large as possible to avoid floating
+ * point round-off error. Using odd number is recommended.
+ * */
+ public Quadratic(double minimum, double maximum, double center, int maxProgress) {
+ if (center < minimum || center > maximum) {
+ throw new IllegalArgumentException("Center must be in between minimum and maximum");
+ }
+
+ this.leftGap = minimum - center;
+ this.rightGap = maximum - center;
+ this.center = center;
+
+ this.centerProgress = maxProgress / 2;
+ }
+
+ @Override
+ public int progressOf(double value) {
+ final double difference = value - center;
+ final double root = difference >= 0 ?
+ Math.sqrt(difference / rightGap) :
+ -Math.sqrt(Math.abs(difference / leftGap));
+ final double offset = Math.round(root * centerProgress);
+
+ return (int) (centerProgress + offset);
+ }
+
+ @Override
+ public double valueOf(int progress) {
+ final int offset = progress - centerProgress;
+ final double square = Math.pow(((double) offset) / ((double) centerProgress), 2);
+ final double difference = square * (offset >= 0 ? rightGap : leftGap);
+
+ return difference + center;
+ }
+ }
+}
diff --git a/app/src/main/res/layout-land/activity_player_queue_control.xml b/app/src/main/res/layout-land/activity_player_queue_control.xml
index c3480c547..11765f901 100644
--- a/app/src/main/res/layout-land/activity_player_queue_control.xml
+++ b/app/src/main/res/layout-land/activity_player_queue_control.xml
@@ -301,9 +301,13 @@
android:id="@+id/live_sync"
android:layout_width="wrap_content"
android:layout_height="match_parent"
+ android:paddingLeft="4dp"
+ android:paddingRight="4dp"
android:gravity="center"
- android:text="@string/live_sync"
+ android:text="@string/duration_live"
+ android:textAllCaps="true"
android:textColor="?attr/colorAccent"
+ android:maxLength="4"
android:background="?attr/selectableItemBackground"
android:visibility="gone"/>
diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml
index e7d337c17..c581c3203 100644
--- a/app/src/main/res/layout/activity_main_player.xml
+++ b/app/src/main/res/layout/activity_main_player.xml
@@ -52,7 +52,7 @@
android:id="@+id/playQueuePanel"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:visibility="gone"
+ android:visibility="invisible"
android:background="?attr/queue_background_color"
tools:visibility="visible">
@@ -254,7 +254,7 @@
android:focusable="true"
android:scaleType="fitXY"
android:src="@drawable/ic_expand_more_white_24dp"
- android:background="?attr/selectableItemBackground"
+ android:background="?attr/selectableItemBackgroundBorderless"
tools:ignore="ContentDescription,RtlHardcoded"/>
@@ -266,7 +266,7 @@
android:gravity="top"
android:paddingLeft="5dp"
android:paddingRight="5dp"
- android:visibility="gone"
+ android:visibility="invisible"
tools:ignore="RtlHardcoded"
tools:visibility="visible">
@@ -308,7 +308,7 @@
android:id="@+id/toggleOrientation"
android:layout_width="30dp"
android:layout_height="30dp"
- android:layout_marginLeft="2dp"
+ android:layout_marginLeft="4dp"
android:layout_marginRight="2dp"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
@@ -325,8 +325,8 @@
android:id="@+id/switchPopup"
android:layout_width="30dp"
android:layout_height="30dp"
- android:layout_marginLeft="2dp"
- android:layout_marginRight="2dp"
+ android:layout_marginLeft="4dp"
+ android:layout_marginRight="4dp"
android:layout_toLeftOf="@id/toggleOrientation"
android:layout_centerVertical="true"
android:clickable="true"
@@ -341,8 +341,8 @@
android:id="@+id/switchBackground"
android:layout_width="30dp"
android:layout_height="30dp"
- android:layout_marginLeft="2dp"
- android:layout_marginRight="2dp"
+ android:layout_marginLeft="4dp"
+ android:layout_marginRight="4dp"
android:layout_toLeftOf="@id/switchPopup"
android:layout_centerVertical="true"
android:clickable="true"
@@ -403,9 +403,13 @@
android:id="@+id/playbackLiveSync"
android:layout_width="wrap_content"
android:layout_height="match_parent"
+ android:paddingLeft="4dp"
+ android:paddingRight="4dp"
android:gravity="center"
- android:text="@string/live_sync"
+ android:text="@string/duration_live"
+ android:textAllCaps="true"
android:textColor="@android:color/white"
+ android:maxLength="4"
android:visibility="gone"
android:background="?attr/selectableItemBackground"
tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" />
diff --git a/app/src/main/res/layout/activity_player_queue_control.xml b/app/src/main/res/layout/activity_player_queue_control.xml
index 639a8037c..7f649e382 100644
--- a/app/src/main/res/layout/activity_player_queue_control.xml
+++ b/app/src/main/res/layout/activity_player_queue_control.xml
@@ -151,9 +151,13 @@
android:id="@+id/live_sync"
android:layout_width="wrap_content"
android:layout_height="match_parent"
+ android:paddingLeft="4dp"
+ android:paddingRight="4dp"
android:gravity="center"
- android:text="@string/live_sync"
+ android:text="@string/duration_live"
+ android:textAllCaps="true"
android:textColor="?attr/colorAccent"
+ android:maxLength="4"
android:background="?attr/selectableItemBackground"
android:visibility="gone"/>
diff --git a/app/src/main/res/layout/dialog_playback_parameter.xml b/app/src/main/res/layout/dialog_playback_parameter.xml
new file mode 100644
index 000000000..a8c6a5dcd
--- /dev/null
+++ b/app/src/main/res/layout/dialog_playback_parameter.xml
@@ -0,0 +1,313 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_playlist_item.xml b/app/src/main/res/layout/list_playlist_item.xml
index 23f5224c5..57a3cbef9 100644
--- a/app/src/main/res/layout/list_playlist_item.xml
+++ b/app/src/main/res/layout/list_playlist_item.xml
@@ -19,7 +19,7 @@
android:layout_alignParentTop="true"
android:layout_marginRight="@dimen/video_item_search_image_right_margin"
android:contentDescription="@string/list_thumbnail_view_description"
- android:scaleType="fitEnd"
+ android:scaleType="centerCrop"
android:src="@drawable/dummy_thumbnail_playlist"
tools:ignore="RtlHardcoded"/>
diff --git a/app/src/main/res/layout/player_popup.xml b/app/src/main/res/layout/player_popup.xml
index 9bbd72fec..0c3ea77df 100644
--- a/app/src/main/res/layout/player_popup.xml
+++ b/app/src/main/res/layout/player_popup.xml
@@ -195,9 +195,13 @@
android:id="@+id/playbackLiveSync"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:paddingLeft="4dp"
+ android:paddingRight="4dp"
android:gravity="center_vertical"
- android:text="@string/live_sync"
+ android:text="@string/duration_live"
+ android:textAllCaps="true"
android:textColor="@android:color/white"
+ android:maxLength="4"
android:visibility="gone"
android:background="?attr/selectableItemBackground"
tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" />
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index f437d7cbb..d716f71cc 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -362,4 +362,9 @@
مُزامَنة
+ تنزيل الملف المتدفق.
+ المؤشرات
+
+ استعمال التقديم السريع الغير دقيق
+ "التقديم الغير دقيق يسمح للمشغل بالإطلاع الى الأماكن بشكل اسرع مع دقة اقل "
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index df414b781..28dff2038 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -227,7 +227,7 @@
MainVerhalten
- Verlauf
+ Verlauf & CachePlaylistRückgängig machen
@@ -309,7 +309,7 @@
Ausrichtung umschaltenIn den Hintergrund wechselnZu Popup wechseln
- Zur Hauptseite wechseln
+ Zum normalen Player wechselnExterne Player unterstützen diese Art von Links nichtUngültige URL
@@ -359,7 +359,7 @@
Neue Playlist ErstellenPlaylist LöschenPlaylist umbenennen
- Zu Playlist Hinzufügen
+ Zu Playlist hinzufügenAls Thumbnail der Playlist festlegenLesezeichen entfernen
@@ -376,4 +376,51 @@
AbbrechenNormale SchriftgrößeStream-Datei herunterladen
-
+ Benutze schnelle ungenaue Suche
+ Ungenaues Suchen erlaubt dem Player die Positionen schneller mit geringerer Genauigkeit zu suchen
+ Datei
+
+ Ungültiges Verzeichnis
+ Datei existiert nicht oder nicht ausreichende Rechte um sie zu lesen oder zu beschreiben
+ Dateiname darf nicht leer sein
+ Ein Fehler ist aufgetreten: %1$s
+
+ Automatisch erzeugt
+ Kleinere Schriftgröße
+ Größere Schriftgröße
+
+ LeakCanary aktivieren
+ Import von
+ Export nach
+
+ Importiere…
+ Exportiere…
+
+ Datei importieren
+ Vorheriger Export
+
+ Beachte, dass diese Aktion sehr Netzwerk intensiv sein kann.
+\n
+\nMöchtest du fortfahren?
+Thumbnails laden
+ Bildercache gelöscht
+ Leere die gecachten Metadaten
+ Entfene alle gecachten Website-Daten
+ Metadatencache gelöscht
+ Fehlersuche
+ Ungültige Datei-/Inhaltsquelle
+ Export abgeschlossen
+ Import abgeschlossen
+ Name
+ Import/Export
+ Import
+ Import der Abonnements fehlgeschlagen
+ Export der Abonnements fehlgeschlagen
+
+ Wiedergabegeschwindigkeit
+ Tempo
+ Tonhöhe
+ Aushaken (kann zu Verzerrungen führen)
+ Nightcore
+ Standard
+
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 5499188a3..5b578d6c9 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -17,7 +17,7 @@
Ruta de descarga de vídeoRuta para almacenar los vídeos descargadosIntroducir directorio de descargas para vídeos
- Resolución de vídeo por defecto
+ Resolución por defecto de vídeoReproducir con KodiAplicación Kore no encontrada. ¿Instalarla?Mostrar opción \"Reproducir con Kodi\"
@@ -156,7 +156,7 @@ abrir en modo popupMostrar resoluciones más altasSolo algunos dispositivos soportan reproducción de vídeos en 2K/4K
- Resolución del popup por defecto
+ Resolución por defecto del popupSegundo planoPopup
@@ -233,7 +233,7 @@ abrir en modo popup
ReproductorFuncionamiento
- Historial
+ Historial y CachéLista de reproducciónDeshacer
@@ -410,4 +410,56 @@ abrir en modo popup
Automáticamente añadir un vídeo relacionado cuando el reproductor llegue al último vídeo en una lista de reproducción no repetible.DIRECTOSINCRONIZAR
+Archivo
+
+ Directorio invalido
+ Fuente del archivo/contenido inválida
+ El archivo no existe o el permiso es insuficiente para leerlo o escribir en él
+ El nombre del archivo no puede estar vacío
+ Ocurrió un error: %1$s
+
+ Importar/Exportar
+ Importar
+ Importar desde
+ Exportar a
+
+ Importando…
+ Exportando…
+
+ Importar archivo
+ Exportación anterior
+
+ Importación de suscripciones fallida
+ Exportación de suscripciones fallida
+
+ Para importar sus suscripciones de YouTube, necesitará el archivo de exportación, el cual puede ser descargado siguiendo estas instrucciones:
+\n
+\n1. Vaya a esta URL: %1$s
+\n2. Ingrese a su cuenta cuando se le pida
+\n3. Una descarga debería comenzar (ese es el archivo de exportación)
+ Para importar sus seguimientos de SoundCloud, debe conocer la URL o el ID de su perfil. Si es así, simplemente escriba cualquiera de ellos en la entrada de abajo y ya está listo para comenzar.
+\n
+\nSi no es así, puede seguir estos pasos:
+\n
+\n1. Active el \"modo escritorio\" en algún navegador (el sitio no está disponible para dispositivos móviles)
+\n2. Vaya a esta URL: %1$s
+\n3. Ingrese a su cuenta cuando se le pida
+\n4. Copie la URL a la que fue redireccionado (esa es la URL de su perfil)
+ suID, soundcloud.com/suID
+
+ Tenga en cuenta que esta operación puede ser costosa para la red.
+\n
+\n¿Desea continuar?
+Cargar Miniaturas
+ Descativar todas las miniaturas para evitar que se carguen, guarden datos y usen memoria. Al cambiar esto se borrarán tanto la caché de imágenes en la memoria como en el disco.
+ Caché de imagen limpiado
+ Metadatos eliminados del caché
+ Eliminar todos los datos de la página web en caché
+ Metadatos del caché limpiados
+ Control de velocidad de la reproducción
+ Tiempo
+ Tono
+ Desenganchar (puede casusar distorsión)
+ Nightcore (tipo de música)
+ Reproducción por defecto
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 330f1e26c..ca49189c3 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -28,7 +28,7 @@
M4A — meilleure qualitéTéléchargerVidéo suivante
- Afficher les vidéos suivantes et similaires
+ Afficher vidéos suivantes/liéesLien non pris en chargeVidéo et audioAutre
@@ -66,11 +66,11 @@
Désolé, des erreurs se sont produites.Contenu
- Afficher le contenu avec restriction d\'âge
+ Afficher le contenu pour adultesDirectImpossible de charger toutes les miniatures
- Impossible de déchiffrer la signature du lien
+ Impossible de déchiffrer le lien de la vidéoImpossible d\'analyser complètement le site webIl s\'agit d\'un direct, non supporté pour le moment.Désolé, une erreur inattendue s\'est produite.
@@ -121,7 +121,7 @@
Sélectionner un dossier de téléchargement disponibleImpossible de charger l\'image
- L’appli/l’interface a crashé
+ L’application a crashéreCAPTCHANoir
@@ -154,7 +154,7 @@
FenêtreRésolution de la fenêtre par défaut
- Afficher des résolutions plus élevées
+ Afficher résolutions plus élevéesCertains appareils uniquement supportent la lecture 2K/4KFormat vidéo par défautMémoriser la taille et la position de la fenêtre
@@ -279,8 +279,8 @@
RetirerDétailsParamètres audio
- Afficher l\'aide \"Appui long pour mettre en file d\'attente\"
- Afficher l\'aide en appuyant sur les boutons \"Arrière-plan\" et \"Fenêtre\" sur la page de détails d\'une vidéo
+ Afficher les fenêtres d\'aide
+ Afficher l\'aide\\\"Appui long pour mettre en file d\'attente\\\" en appuyant sur les boutons \\\"Arrière-plan\\\" et \\\"Fenêtre\\\" sur la page de détails d\'une vidéo[Inconnu]Récupération de l\'erreur du lecteur
@@ -354,7 +354,7 @@
Tout supprimerVoulez vous supprimer cet élément de votre historique ?Êtes vous sûr de supprimer tout votre historique ?
- Titres les plus joués
+ Vidéos les plus regardéesToujours demander
@@ -371,7 +371,7 @@
Voulez-vous supprimer cette playlist ?Playlist créée avec succèsAjoutée à la playlist
- La playlist à été modifiée avec succès
+ Modification de la playlist réussieÉchec de la suppression de la playlistAucun sous-titre
@@ -379,9 +379,78 @@
RedimensionnerZoom
- Taille de police des sous-titres
- Police plus petite
- Police normale
- Police plus grande
+ Taille des sous-titres
+ Petite
+ Normale
+ Grande
-
+ Recherche rapide approximative
+ Permettre au lecteur d\'accéder plus rapidement à une position au détriment de la précision
+ Charger imagettes
+ Si désactivé, le chargement des imagettes sera stoppé et elles seont supprimées de votre mémoire cache et de votre stockage. Permet de réduire l\'utilisation de mémoire et de données.
+ Le cache des images a été nettoyé
+ Supprimer les données en cache
+ Supprimer toutes les pages web mises en cache
+ Données en cache supprimées
+ Fichier
+
+ Dossier non valide
+ Fichier/source du contenu non valide
+ Le fichier n\'existe pas ou il n\'est pas permis de le lire
+ Le nom du fichier ne peut être vide
+ Une erreur s\'est produite: %1$s
+
+ Supprimer un seul média
+ En cours de développement ;D
+
+
+ Télécharger le fichier de flux
+ Vidéo suivante en file d\'attente
+ Mettre automatiquement en file d\'attente la vidéo suivante liée à la vidéo en cours de lecture (si vous n\'êtes pas en mode répétition)
+ Débogage
+ Remplir
+ Affichage automatique
+ Activer LeakCanary
+ Surveiller la baisse de mémoire. L\'application pourrait ne plus répondre correctement
+
+ Signaler erreurs Out-of-lifecycle
+ Forcer le signalement des exceptions Rx qui surviennent hors activité
+
+ Importer/Exporter
+ Importer
+ Importer de
+ Exporter vers
+
+ Importation en cours…
+ Exporation en cours…
+
+ Importer fichier
+ Export précédent
+
+ Import des abonnements échoué
+ Export des abonnements échoué
+
+ \"Pour importer vos abonnements YouTube vous devez d\'abord télécharger un fichier export de YouTube, selon les modalités suivantes:
+\n
+\n1. Allez à ce lien: %1$s
+\n2. Connectez-vous à votre compte
+\n3. Le téléchargement devrait démarrer (votre fichier export YouTube)\"
+ Pour importer vos abonnements SoundCloud vous devez connaitre l\'URL de votre profil ou votre identifiant (id). Si vous le savez, tapez-le ci-dessous.
+\n
+\nSi vous ne le connaissez pas, veuillez suivre les étapes suivantes:
+\n
+\n1. Activer le \\\"mode bureau\\\" dans votre navigateur (le site n\'est pas accesible en mode mobile)
+\n2. Aller à ce lien: %1$s
+\n3. Connectez-vous à votre compte
+\n4. Copier l\'URL vers lequel vous venez d\'être redirigé (qui est l\'URL de votre profil)
+ votreid, soundcloud.com/votreid
+
+ N\'oubliez pas que cette opération peut consommer beaucoup de données mobiles.
+\n
+\nSouhaitez-vous continuer ?
+
+ Vitesse de lecture
+
+ Unhook (déformations possibles)
+ Défaut
+
diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml
index cb69d6639..8cb0e4571 100644
--- a/app/src/main/res/values-he/strings.xml
+++ b/app/src/main/res/values-he/strings.xml
@@ -308,4 +308,8 @@
יצוא מסד נתוניםנגנים חיצוניים לא תומכים בסוגי קישורים אלהכתובת שגויה
+ קובץ
+
+ העברה לרקע
+ העברה לחלון צץ
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 9b294a9a7..df6df710e 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -235,7 +235,7 @@
PrincipaleRiproduttoreComportamento
- Cronologia
+ Cronologia e cacheScalettaAnnulla
@@ -411,4 +411,56 @@
Aggiungi automaticamente un flusso correlato mentre il playback parte dall\'ultimo flusso in una cosa non ripetitiva.SINCRONIZZAZIONE
-
+ File
+
+ Cartella invalida
+ Fonte del contenuto o file invalido
+ Il file non esiste o non si hanno i permessi sufficenti per leggerlo o scriverci
+ Il nome del file non può essere vuoto
+ Si è verificato un errore: %1$s
+
+ Importa/Esporta
+ Importa
+ Importa da
+ Esporta a
+
+ Importando…
+ Esportando…
+
+ Importa file
+ Esportazione precedente
+
+ L\'importazione delle iscrizioni è fallita
+ L\'esportazione delle iscrizioni è fallita
+
+ Per importare le tue iscrizioni YouTube devi procurarti il file d\'esportazione, il quale può essere scaricato seguendo le seguenti istruzioni:
+\n
+\n1. Vai a questo URL: %2$s
+\n2. Accedi al tuo account quando è richiedto
+\n3. Un download dovrebbe essere partito (è il file d\'esportazione)
+ Per importare i tuoi seguiti di SoundCloud devi conoscere l\'URL del tuo profilo od il tuo ID. Se lo sai, ti basta scrivere uno dei due nell\'immisione in basso ed hai fatto.
+\n
+\nSe non lo sai, puoi seguire le seguenti istruzioni:
+\n
+\n1. Abilita la \"modalità desktop\" nel browser che usi (il sito non funziona nella modalità mobile)
+\n2. Vai a questo URL: %2$s
+\n3. Accedi al tuo account quando richiesto
+\n4. Copia l\'URL a cui vieni indirizzato (è l\'URL del tuo profilo)
+ iltuoid, soundcloud.com/iltuoid
+
+ Tieni in mente che questa operazione può richiedere un costo di connessione dati.
+\n
+\nVuoi continuare?
+Carica miniature
+ Disabilita per fermare il caricamento delle miniature, la loro archiviazione nella memoria e l\'uso della memoria aggiuntiva. Cambiare questa opzione comporta alla cancellazione della cache sia in memoria che sul disco.
+ Pulizia della cache delle immagini completata
+ Pulisci la cache dei metadati
+ Rimuovi tutti i dati delle pagine web salvate
+ Pulizia della cache dei metadati completata
+ Controllo della velocità del playback
+ Tempo
+ Tono
+ Slega (può causare distorsione)
+ Nightcore
+ Valore predefinito
+
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 4a0caee97..eaa97a2a5 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -78,7 +78,7 @@
다운로드 메뉴를 설정할 수 없습니다실시간 스트리밍 비디오는 아직 지원되지 않습니다.어떠한 스트림도 가져올 수 없습니다
- 죄송합니다
+ 죄송합니다. 오류가 발생했습니다.이메일을 통해 오류 보고죄송합니다. 오류가 발생했습니다.보고
@@ -215,7 +215,7 @@
이 권한은 팝업 모드에서
\n열기 위해 필요합니다
- reCAPTCHA
+ 로봇인지 확인 (reCAPTCHA)reCAPTCHA Challenge 요청됨다운로드
@@ -241,7 +241,7 @@
번역, 디자인, 코딩 등 다양한 기여를 언제나 환영합니다. 향상에 참여해주세요!GitHub에서 보기기부
- 뉴파이프는 자원봉사자들이 자발적으로 여가 시간을 활용해 개발하고 있습니다. 이제 이러한 노력에 보답할 시간입니다.
+ 뉴파이프는 자원봉사자들이 자발적으로 여가 시간을 활용해 개발하고 있습니다. 이제 이러한 노력에 보답할 시간입니다!보답하기웹사이트뉴파이프에 관한 최신 및 상세 정보를 얻으려면 웹사이트를 방문하세요.
@@ -284,4 +284,148 @@
여기서부터 재생여기서부터 백그라운드에서 재생여기서부터 팝업에 재생
+스트리밍 플레이어를 찾을 수 없습니다. VLC를 설치하면 플레이하실 수 있습니다
+ 스트리밍 파일 다운로드하기.
+ 정보 보기
+
+ 북마크
+
+ 이곳에 추가
+
+ 정확하지는 않지만 빠른 탐색
+ 정확하지 않은 탐색은 빠르게 위치로 탐색할 수 있지만 정확도는 떨어집니다
+ 다음 스트림을 자동으로 재생열에 추가하기
+ 전 스트림이 무한 반복 재생 큐가 아닐 때 관련된 스트림 자동 재생하기.
+ 기본 콘텐츠 국가
+ 서비스
+ 디버그
+ 라이브 (LIVE)
+ 항상
+ 한번만
+
+ 디바이스 방향 토글
+ 백그라운드로 전환
+ 팝업으로 전환
+ 기본으로 전환
+
+ 데이터베이스 가져오기
+ 데이터베이스 내보내기
+ 현재 시청 기록 및 구독 목록을 덮어쓰기 됩니다
+ 시청 기록, 구독 목록, 재생목록 내보내기.
+ 외부 플레이어는 이러한 종류의 링크를 지원하지 않습니다
+ 잘못된 URL
+ 발견된 비디오 스트림 없음
+ 발견된 오디오 스트림 없음
+
+ 드래그하여 재배열
+
+ 만들기
+ 1개 삭제하기
+ 모두 삭제하기
+ 취소
+ 이름 바꾸기
+
+ 로봇인지 확인합니다
+ 이 항목을 시청 기록에서 삭제하시겠습니까?
+ 모든 항목을 시청 기록에서 삭제하시겠습니까?
+ 마지막으로 재생
+ 가장 많이 재생
+
+ 내보내기 완료
+ 가져오기 완료
+ 유효한 ZIP 파일 없음
+ 경고: 모든 파일 가져오기를 실패했습니다.
+ 이것은 현재 설정을 덮어쓸 것입니다.
+
+ 드로어 열기
+ 드로어 닫기
+ 여기에 무언가가 추가될 거에요~ :D
+
+
+ 선호하는 플레이어로 열기
+ 선호하는 플레이어
+
+ 비디오 플레이어
+ 백그라운드 플레이어
+ 팝업 플레이어
+ 항상 묻기
+
+ 정보 가져오는 중…
+ 요청한 콘텐츠를 로딩 중입니다
+
+ 새로운 재생목록 만들기
+ 재생목록 삭제
+ 재생목록 이름 바꾸기
+ 이름
+ 재생목록에 추가
+ 재생목록 썸네일로 설정
+
+ 재생목록 북마크하기
+ 북마크 제거하기
+
+ 이 재생목록을 삭제하시겠습니까?
+ 재생목록 생성 성공
+ 재생목록에 추가됨
+ 재생목록 썸내일이 바뀜
+ 재생목록 삭제 실패
+
+ 자막 없음
+
+ 꼭 맞게 하기
+ 채우기
+ 확대
+
+ 자동 생성됨
+ 자막 폰트 크기
+ 작은 폰트
+ 보통 폰트
+ 큰 폰트
+
+ 동기화
+
+ LeakCanary 할성화
+ 메모리 누수 모니터링은 힙 덤핑시 앱이 불안정할 수 있습니다
+
+ Out-of-Lifecycle 에러 보고
+ 프래그먼트 또는 버려진 액티비티 주기 밖에서 일어나는 전달할 수 없는 Rx 예외를 강제적으로 보고하기
+
+파일
+
+ 잘못된 디렉토리
+ 잘못된 파일/콘덴츠 소스
+ 파일이 존재하지 않거나 읽기/쓰기 권환이 없습니다
+ 파일 이름이 비어 있으면 안됩니다
+ 오류 발생: %1$s
+
+ 가져오기/내보내기
+ 가져오기
+ 이곳으로부터 가져오기
+ 이곳으로 내보내기
+
+ 가져오는 중.…
+ 내보내는 중…
+
+ 파일 가져오기
+ 이전 내보내기
+
+ 구독 목록 가져오기 실패
+ 구독 목록 내보내기 실패
+
+ YouTube 구독 목록을 가져오려면 내보내기 파일이 필요합니다. 다운로드 하려면
+\n1. 이곳으로 가세요: $1$s
+\n2. 로그인이 필요하면 하세요
+\n3. 다운로드가 곧 시작 됩니다 (이 파일이 내보내기 파일 입니다)
+ SoundCloud 팔로잉 목록을 가져오려면 당신의 프로필 URL 및 ID를 알아야 합니다. 알고 있다면 아래에 있는 빈칸에 입력해 주세요.
+\n
+\n만약 모르신다면, 다음을 참고하세요:
+\n
+\n1. 모바일 환경이시면 브라우저 설정에서 데스크탑 모드를 활성화해주세요. Chrome 모바일에서는 오른쪽 ... 클릭시 아래쪽에 있습니다.
+\n2. 이 주소로 가세요: %1$s
+\n3. 로그인이 필요하면 하세요.
+\n4. 리디렉트된 곳의 URL을 복사하세요. (이 URL이 당신의 프로필 URL 입니다)
+ 프로필ID, soundcloud.com/프로필ID
+
+ 경고: 데이터 소모량이 늘어날 수 있습니다.
+\n
+\n진행하시겠습니까?
diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml
index c214f5bd1..df5221bc4 100644
--- a/app/src/main/res/values-nb-rNO/strings.xml
+++ b/app/src/main/res/values-nb-rNO/strings.xml
@@ -382,4 +382,26 @@
Mindre skriftNormal skriftStørre skrift
-
+Bruk raskt unøyaktig søk
+ Feilretting
+ Fil
+
+ Ugyldig mappe
+ Ugyldig fil/innholdskilde
+ Filen finnes ikke eller så har du ikke tilgang til å lese eller skrive til den
+ Filnavn kan ikke være tomt
+ En feil inntraff: %1$s
+
+ Auto-generert
+ Skru på LeakCanary
+ Importer
+ Importer fra
+ Eksporter til
+
+ Importerer…
+ Eksporterer…
+
+ Importer fil
+ Forrige eksport
+
+
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 4f8e5b0e5..d6f710351 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -234,7 +234,7 @@ te openen in pop-upmodus
SpelerGedrag
- Geschiedenis
+ Geschiedenis & CacheAfspeellijstOngedaan maken
@@ -406,4 +406,56 @@ te openen in pop-upmodus
Automatisch een gerealteerde stream toekennen als het afspelen van de laatste stream strat in een niet-herhalende afspeelwachtlijst.SYNCHRONISEREN
-
+ Bestand
+
+ Ongeldige map
+ Ongeldig bestand/Ongeldige inhoudsbron
+ Het bestand bestaat niet of u beschikt niet over voldoende machtiging om het te lezen/er naar te schrijven
+ De bestandsnaam mag niet leeg zijn
+ Er is een fout opgetreden: %1$s
+
+ Importeren/Exporteren
+ Importeren
+ Importeren uit
+ Exporteren naar
+
+ Bezig met importeren…
+ Bezig met exporteren…
+
+ Bestand importeren
+ Vorige exportering
+
+ Importeren van abonnementen is mislukt
+ Exporteren van abonnementen is mislukt
+
+ Als u uw YouTube-abonnementen wilt importeren, dan heeft u het exportbestand nodig. Dit kan worden gedownload door het volgen van onderstaande stappen:
+\n
+\n1. Ga naar dit adres: %1$s
+\n2. Log, indien nodig, in op uw account
+\n3. De download met het exportbestand zou nu moeten starten
+ Als u uw SoundCloud-abonnementen wilt importeren, dan moet u uw profiel-URL of ID kennen. Als u hem kent, typ hem dan hieronder in.
+\n
+\nAls u hem niet kent, volg dan onderstaande stappen:
+\n
+\n1. Kies een webbrowser en schakel bureaubladmodus in (de website is niet beschikbaar voor mobiele apparaten)
+\n2. Volg deze link: %1$s
+\n3. Log, indien nodig, in op uw account
+\n4. Kopieer de link van de pagina waar u op terechtkomt (dat is uw profiel-URL)
+ uwid, soundcloud.com/uwid
+
+ Let op: deze actie kan veel MB\'s van uw netwerk gebruiken.
+\n
+\nWilt u doorgaan?
+Miniatuurvoorbeelden laden
+ Schakel dit uit om alle miniatuurvoorbeelden niet meer te laden; dit bespaart gegevens en geheugen. Het wijzigen van deze instelling wist het geheugen en de afbeeldingscache.
+ Afbeeldingscache gewist
+ Gecachete metagegevens wissen
+ Alle gecachete webpagina-gegevens wissen
+ Metagegevens-cache gewist
+ Afspeelsnelheid
+ Tempo
+ Toon
+ Ontkoppelen (kan ruis veroorzaken)
+ Nightcore
+ Standaard
+
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 18f9ab9cb..4aad46268 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -397,4 +397,5 @@
Raportuj błędy Out-of-LifecycleWymusza raportowanie niedostarczonych wyjątków Rx poza cyklem życia fragmentu lub aktywności
-
+Użyj szybkiego niedokładnego wyszukiwania
+
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 96e6ccb70..ea22efa95 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -385,4 +385,44 @@ abrir em modo popup
Anexar automaticamente uma stream relacionada quando a reprodução iniciar na última stream em uma fila não repetitivaSincronizar
-
+ Arquivo
+
+ Diretório inválido
+ Origem do arquivo/conteúdo inválido
+ Arquivo não existe ou não há permissão para ler ou escrever nele
+ Nome do arquivo não pode ser vazio
+ Um erro ocorreu: %1$s
+
+ Importar/Exportar
+ Importar
+ Importar de
+ Exportar para
+
+ Importando…
+ Exportando…
+
+ Importar arquivo
+ Exportação anteriore
+
+ Importação de inscrições falhou
+ Exportação de inscrições falhou
+
+ "Para importar inscrições do YouTube você vai precisar exportar o arquivo, o que pode ser baixado seguindo estas informações:
+\n
+\n1. Vá para este link: %1$s
+\n2. Faça login na sua conta quando solicitado
+\n3. O download deverá começar (isto é exportar arquivo)"
+ Para importar as contas que você segue no SoundCloud, você terá que saber o link ou id do seu perfil. Se você souber, basta escrever um deles no campo abaixo e estará tudo pronto.
+\n
+\nSe você não souber, você pode seguir estas etapas:
+\n
+\n1. Habilite \"modo desktop\" em algum navegador da internet ( o site não está disponível para dispositivos móveis)
+\n2. Vá para esta url: %1$s
+\n3. Faça login na sua conta quando solicitado
+\n4. Copie o link no qual que você foi redirecionado (este é o link do seu perfil)
+ seuid, soundcloud.com/seuid
+
+ Tenha em mente que esta operação poderá usar bastante a conexão com a internet.
+\n
+\nVocê deseja continuar?
+
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 58fb004c7..fc4cc4fd0 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -58,7 +58,7 @@
Нажмите поиск, чтобы начатьПодождите…Файл уже существует
- Потоки
+ ТемыOKНачатьПауза
@@ -69,7 +69,7 @@
ОшибкаСервер не поддерживаетсяNewPipe скачивает
- Неправильный URL или Интернет не доступен
+ Неправильный URL или нет доступа к интернетуНажмите для деталейСкопировано в буфер обменаВыберите доступную папку для загрузки
@@ -137,19 +137,19 @@
ОбновитьОчиститьИспользовать старый плеер
- Жесты
+ Контроль жестовВсёФильтр
- Новая миссия
+ Новая цельЧто:\\nЗапрос:\\nЯзык контента:\\nСервис:\\nВремя по Гринвичу:\\nПакет:\\nВерсия:\\nВерсия ОС:\\nГлобальный диапазон IP:Это разрешение нужно для
-\nвоспроизведения видео в отдельном окне
+\nвоспроизведения в окне
reCAPTCHAОткрыть в отдельном окне
- Показывать подсказки во время поиска
+ Показывать подсказки в поискеПозжеОтключено
@@ -162,14 +162,14 @@
тыс.Разрешение в режиме всплывающего окнаЗапоминать последний размер и положение всплывающего окна
- Живой поиск
+ Поисковые подсказкиЛучшее разрешениеСтарый встроенный плеер на MediaframeworkЗапрос reCAPTCHAЗапрошен ввод reCAPTCHA
- Показывать более высокие разрешения
+ Показывать более высокое разрешениеNewPipe в окнеО NewPipeНастройки
@@ -265,7 +265,7 @@
Выберите киоскКиоск
- В тренде
+ ТрендыТоп 50Новое и горячееДобавлено в очередь в фоне
@@ -275,22 +275,22 @@
Не удалось воспроизвести этот потокПодробностиНастройки аудио
- Пока нет подписок
+ Пока нет подписок на каналыУдалитьОтписатьсяПодписка отменена
- Подсказка о длинном нажатии
- Отображать подсказку о длинном нажатии на кнопки \"В фоне\" и \"В окне\" для добавления в очередь
+ Показывать напоминание о длинном нажатии
+ Показывать подсказку при нажатии на иконку «В окне» или «В фоне» на странице сведений о видео[Неизвестно]Восстановление после ошибки проигрывателя
- Воспроизведение в фоне
- Воспроизведение в окне
- Зажмите чтобы добавить в очередь
- Добавить в очередь в фоне
- Добавить в очередь в окне
- Воспроизвести
+ В фоне
+ В окне
+ Зажмите, чтобы добавить в очередь
+ Добавить в очередь «В фоне»
+ Добавить в очередь «В окне»
+ Воспроизвести тутВоспроизвести в фонеВоспроизвести в окнеНи одного потокового проигрывателя не было найдено (вы можете установить VLC)
@@ -326,5 +326,124 @@
Всегда спрашиватьПолучение информации…
- Загрузка запрашиваемого контента
-
+ Загрузка запрошенного контента
+Загрузка файла прямой трансляции.
+ Показать информацию
+
+ Закладки
+
+ Добавить к
+
+ Использовать быстрый, но неточный поиск
+ Неточный поиск позволяет плееру искать позицию быстрее, но с пониженной точностью
+ Автоматическая очередь следующего стрима
+ Автоматически добавлять связанные потоки, когда воспроизведение начинается с последнего потока в неповторяющейся очереди воспроизведения.
+ Отладка
+ Файл
+
+ Импорт данных
+ Экспорт данных
+ Ваша текущая история и подписки будут перезаписаны
+ Экспорт истории, подписок и плейлистов.
+ Неправильная директория
+ Неправильный файл/контент источника
+ Файл не существует или нет разрешения на его прочтение или запись
+ Имя файла не может быть пустым
+ Произошла ошибка: %1$s
+
+ Перетащите, чтобы изменить порядок
+
+ Создать
+ Удалить одно
+ Удалить всё
+ Отклонить
+ Переименовать
+
+ Вы хотите удалить этот элемент из истории поиска?
+ Вы уверены, что хотите удалить все элементы из истории?
+ Последнее проигрывание
+ Наиболее проигрываемые
+
+ Экспорт завершён
+ Импорт завершён
+ Нет верного Zip файла
+ Предупреждение: нет возможности импорта всех файлов.
+ Это перезапишет вашу текущую установку.
+
+ Что-то будет тут, скоро ;D
+
+
+ Всегда спрашивать
+
+ Создать новый плейлист
+ Удалить плейлист
+ Переименовать плейлист
+ Имя
+ Добавить в плейлист
+ Установить как иконку плейлиста
+
+ Пометить плейлист
+ Удалить пометку
+
+ Вы хотите удалить этот плейлист?
+ Плейлист успешно создан
+ Добавлено в плейлист
+ Иконка плейлиста изменена
+ Ошибка при удалении плейлиста
+
+ Без подписи
+
+ Уместить
+ Заполнить
+ Приближение
+
+ Автоматически созданный
+ Размер шрифта подписи
+ Маленький шрифт
+ Обычный шрифт
+ Большой шрифт
+
+ Синхронизировать
+
+ Включить LeakCanary
+ Мониторинг утечки памяти может привести к зависанию приложения
+
+ Ошибки отчёта вне очереди
+ Форсировать отчетность о недопустимых исключениях Rx, возникающих за пределами фрагмента или цикла деятельности, после размещения
+
+ Импорт/Экспорт
+ Импорт
+ Импорт из
+ Экспорт в
+
+ Импорт…
+ Экспорт…
+
+ Импорт файла
+ Предыдущий экспорт
+
+ Импорт подписок провален
+ Экспорт подписок провален
+
+ Для импорта подписок из YouTube вам необходимо файл экспорта, которые можно загрузить в соответствии с этими инструкциями:
+\n
+\n1. Перейдите на: %1$s
+\n2. Войдите в ваш аккаунт, если необходимо
+\n3. Загрузка должна начаться (это файл экспорта)
+ "Для импорта ваших подписок из SoundCloud вы должны знать ссылку на ваш профиль или id. Если вы знаете, просто напишите это в поле ниже и будьте готовы начинать.
+\n
+\nЕсли вы не знаете, то проследуйте следующей инструкции:
+\n
+\n1. Включите \"режим рабочего стола\" в браузере (сайт недоступен на телефоне)
+\n2. Пройдите на: %1$s
+\n3. Войдите в аккаунт, если надо
+\n4. Скопируйте адрес из адресной строки (это адрес вашего профиля)
+\n
+\n"
+ вашid, soundcloud.com/вашid
+
+ Помните, что за выход в интернет может взиматься плата.
+\n
+\nВы хотите продолжить?
+Загрузить превью
+
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 9fbb42e0b..736bb12ae 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -401,4 +401,44 @@
Yinelemeyen oynatma kuyruğundaki son akış başladığında ilişkili akışı kuyruğun sonuna kendiliğinden ekle.EŞZAMANLA
-
+ Dosya
+
+ Geçersiz dizin
+ Geçersiz dosya/içerik kaynağı
+ Dosya yok ya da okuma veya yazma izni yetersiz
+ Dosya adı boş olamaz
+ Hata oluştu: %1$s
+
+ İçe/Dışa Aktar
+ İçe Aktar
+ Şuradan içe aktar
+ Şuna dışa aktar
+
+ İçe aktarılıyor…
+ Dışa aktarılıyor…
+
+ Dosyayı içe aktar
+ Önceki dışa aktarım
+
+ Aboneliklerin içe aktarımı başarısız
+ Aboneliklerin dışa aktarımı başarısız
+
+ YouTube aboneliklerinizi içe aktarmak için dışa aktarılmış dosya gerekiyor, dosya şu yönergeler izlenerek indirilebilir:
+\n
+\n1. Şu adrese gidin: %1$s
+\n2. Sorulduğunda hesabınıza giriş yapın
+\n3. İndirme başlamalı (bu dışa aktarılmış dosyadır)
+ SoundCloud takiplerinizi içe aktarmak için profil adresinizi veya kimliğinizi bilmelisiniz. Eğer biliyorsanız, ikisinden birini aşağıdaki giriye yazın ve işte hazırsınız.
+\n
+\nEğer bilmiyorsanız şu adımları izleyebilirsiniz:
+\n
+\n1. Herhangi bir tarayıcıda \"masaüstü kipi\"ni açın (site, mobil aygıtlar için uygun değildir)
+\n2. Şu adrese gidin: %1$s
+\n3. Sorulduğunda hesabınıza giriş yapın
+\n4. Yönlendirildiğiniz adresi kopyalayın (bu sizin profil adresinizdir)
+ kimliginiz, soundcloud.com/kimliginiz
+
+ Bu sürecin ağ masrafına neden olabileceğini unutmayın.
+\n
+\nDevam etmek istiyor musunuz?
+
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index 0608d8150..352f274ea 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -126,9 +126,9 @@
Будь ласка, оберіть теку для завантаженняПотоковий програвач не знайдено (ви можете встановити VLC для відтворення)
- Відкрити у виринальному режимі
+ Відкрити у віконному режиміПевні роздільності НЕ МАТИМУТЬ звуку якщо цей параметр увімкнено
- NewPipe у виринальному вікні
+ NewPipe у віконному режиміПідписатисяВи підписалисяВи відписалися від каналу
@@ -141,14 +141,14 @@
НовинкиТло
- Виринальне вікно
+ У вікні
- Типова роздільна здатність виринального вікна
+ Типова роздільна здатність вікнаНе всі пристрої підтримують програвання 2K/4K відеоПоказувати більші роздільні здатностіТиповий відео формат
- Пам\'ятати розмір виринального вікна та положення
- Пам\'ятати останній розмір та позицію виринального вікна
+ Пам\'ятати розмір та положення вікна
+ Пам\'ятати останній розмір та позицію вікнаКерування жестамиВикористовувати жести для контролю яскравості та гучності програвачаШукати схожі
@@ -159,16 +159,16 @@
Вести облік перегляду відеоВідновити фокусПродовжувати відтворення опісля переривання (наприклад телефонний дзвінок)
- Відображати вказівку Утримуйте для додачі
+ Показувати стримання для підказкиУсталена країна контентуСервісПрогравачПоведінка
- Історія
- Виринальне вікно
- Відворення у виринальному вікні
+ Історія та кеш
+ Вікно
+ Відворення у вікніДодано до фонового програвання
- Додано до чергу у виринальному вікні
+ Додано до чергу у вікноПлейлистФільтруватиОновити
@@ -181,12 +181,12 @@
Тільки теперNewPipe сповіщення
- Сповіщення для фонового та виринального програвача NewPipe
+ Сповіщення для фонового та віконного програвачів NewPipe[Невідомо]Перемкнутися до Тла
- Перемкнутися до Виринального вікна
+ Перемкнутися до вікнаПеремкнутися до ГоловноїІмпортувати базу
@@ -204,7 +204,7 @@
Без відеоПомилкова ланка URL або інтернет не є доступнимЦей дозвіл має бути відкритим
-\nу виринальному вікні
+\nу вікні
«reCAPTCHA»Завантажити
@@ -248,7 +248,7 @@
Додати до
- Показувати підказку коли фонова чи виринальна кнопка натиснута на сторінці відео деталей
+ Показувати підказку коли натиснута кнопка фону або вікна, на сторінці інформації відеоПеремкнути орієнтаціюФатальна помилка програвачаЗовнішні програвачі не підтримують такі види ланок
@@ -335,14 +335,14 @@
ЯткаНабуває популярностіФоновий програвач
- Виринальний програвач
+ Віконний програвачУсунутиЗатиснути, аби зняти з чергиЗняти з черги у фоновому програвачеві
- Зняти з черги у виринальному програвачеві
+ Зняти з черги у віконному програвачевіРозпочати програвання звідсиРозпочати програвання звідси у фоновому програвачеві
- Розпочати програвання звідси у виринальному програвачеві
+ Розпочати програвання у вікні звідсиВідчинити шухлядуЗачинити шухляду
@@ -383,8 +383,8 @@
Увімкнути LeakCanaryПід час роботи LeakCanary застосунок може стати несприйнятливим під час гіп-дампінґу
- Зазвітувати Out-of-Lifecycle хиби
- Примусове звітування про неможливість доставлення Rx винятків, яку відбуваються за межами фраґменту, або діяльності життєвого циклу після усунення
+ Зазвітувати Out-of-lifecycle хиби
+ Примусове звітування про неможливість доставлення Rx винятків, які відбуваються за межами фраґменту або діяльності життєвого циклу після усуненняВикористовувати неточне шуканняНеточне шукання дозволяє програвачеві рухатися позиціями швидше, проте з меншою точністю
@@ -392,4 +392,57 @@
Автоматично додавати пов\'язаний стрим, під час початку програвання останнього стриму.СИНХРОНІЗАЦІЯ
-
+ Файл
+
+ Неправильна тека
+ Неправильний файл/контент джерела
+ Файл не існує, або немає дозволу на його запис чи читання
+ Ім\'я файлу не повинно бути порожнім
+ Трапилась помилка: %1$s
+
+ Імпортування/Експортування
+ Імпортування
+ Імпортувати з
+ Експортувати до
+
+ Імпортування…
+ Експортування…
+
+ Імпортування файлу
+ Попереднє експортування
+
+ Не вдалось імпортувати підписки
+ Не вдалося експортувати підписки
+
+ Аби імпортувати ваші підписання Ютюб, вам буде потрібно експортувати файл, який можна буде завантажити наступним чином:
+\n
+\n1. Перейдіть за цією ланкою: %1$s
+\n2. За запитом увійдіть до вашої обліківки
+\n3. Завантаження має початися (експортований файл)
+ Для імпортування ваших підписок з SoundCloud, ви маєте знати url вашого профайлу або ID. Якщо ви знаєте їх, упишіть їх нижче та можна працювати.
+\n
+\nЯкщо ви не маєте їх, зробіть наступним чином:
+\n
+\n1. Увімкніть режимі \"desktop\" у будь-якому з переглядачів (сайт не має підтримки мобільних ґаджетів)
+\n
+\n2. Перейдіть за цією ланкою: %1$s
+\n3. За запитом увійдіть до вашої обліківки
+\n4. Скопіюйте url, до якого вас відішле (це й є url вашого профайлу)
+ yourid, soundcloud.com/yourid
+
+ Майте на увазі: ця операція може потребувати багато трафіку.
+\n
+\nПродовжуватимете?
+Завантажити ескізи
+ Відключити аби зупинити завантаження ескізів та заощадити використання ресурсів та пам\'яті. Увімкнення функції призведе до повного вичищення кешу зображень.
+ Кеш зображень стерто
+ Стерти кеш метаданих
+ "Усунути всі кешовані дані веб-сторінки "
+ Кеш метаданих стерто
+ Керування швидкістю програвання
+ Темп
+ Тон
+ Від\'єднати (може спричинити спотворення)
+ Nightcore
+ Усталено
+
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index a112b4830..17982f7cf 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -181,7 +181,7 @@
在干擾結束後繼續播放(例如有來電)播放器行為
- 歷史紀錄
+ 歷史記錄和快取播放清單復原
@@ -241,7 +241,7 @@
項目已刪除確定要刪除此項搜尋紀錄嗎?沒有找到串流播放器(你可以安裝 VLC播放器 來播放)
- 顯示鎖定到附加指引上
+ 顯示鎖定到附加提示預設內容國家服務在背景播放器上等候
@@ -387,4 +387,55 @@
在非重複播放佇列中的最後一個串流上開始播放時,自動附上相關串流。同步
-
+ 檔案
+
+ 無效的目錄
+ 無效的檔案/內容來源
+ 檔案名稱不能留空
+ 發生錯誤:%1$s
+
+ 匯入/匯出
+ 匯入
+ 匯入來自
+ 匯出到
+
+ 正在匯入…
+ 正在匯出…
+
+ 匯入檔案
+ 訂閱匯入失敗
+ 訂閱匯出失敗
+
+ 之前的匯出
+
+ 檔案不存在或沒有足夠的權限讀取或寫入
+ 要匯入您的 YouTube 訂閱,您必須匯出檔案,可以按照以下說明進行下載:
+\n
+\n1. 轉到此網址:%1$s
+\n2. 當被詢問時登入您的帳戶
+\n3. 下載應該開始 ( 這就是匯出的檔案 )
+ yourid, soundcloud.com/yourid
+
+ 請記住,此操作可能會造成網路昂貴花費。
+\n
+\n您想繼續嗎?
+要匯入您的 SoundCloud,您必須知道您的個人資料網址或 ID。 如果您這樣做,只需在下方的輸入中鍵入其中的任意一個,然後就可以開始了。
+\n
+\n如果您不這樣做,您可以按照以下步驟操作:
+\n1. 在一些瀏覽器中啟用「桌面模式」(該網站不適用於行動裝置)
+\n2. 移至此網址:%1$s
+\n3. 詢問時登入到您的帳號
+\n4. 複製網址您會被重新導向(這是您的個人資料網址)
+ 載入縮圖
+ 停用可以停止載入所有的縮圖和儲存資料與使用的記憶體。更改此動作將清除在記憶體和磁碟上的影像快取。
+ 圖片快取被抹除
+ 抹除快取中介資料
+ 移除所有快取網頁的資料
+ 中介資料快取已抹除
+ 重播速度控制
+ 節拍
+ 間距
+ 解除 (可能導致失真)
+ Nightcore
+ 預設
+
diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml
index a897aa185..68d75737a 100644
--- a/app/src/main/res/values/settings_keys.xml
+++ b/app/src/main/res/values/settings_keys.xml
@@ -160,6 +160,10 @@
import_dataexport_data
+ download_thumbnail_key
+
+ cache_wipe_key
+
file_renamefile_replacement_character
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c97f12809..effdeaaba 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -74,6 +74,12 @@
Remember last size and position of popupUse fast inexact seekInexact seek allows the player to seek to positions faster with reduced precision
+ Load thumbnails
+ Disable to stop all thumbnails from loading and save on data and memory usage. Changing this will clear both in-memory and on-disk image cache.
+ Image cache wiped
+ Wipe cached metadata
+ Remove all cached webpage data
+ Metadata cache wipedAuto-queue next streamAutomatically append a related stream when playback starts on the last stream in a non-repeating play queue.Player gesture controls
@@ -89,7 +95,7 @@
DownloadNext videoShow next and similar videos
- Show Hold to Append Tip
+ Show hold to append tipShow tip when background or popup button is pressed on video details pageURL not supportedDefault content country
@@ -98,7 +104,7 @@
PlayerBehaviorVideo & Audio
- History
+ History & CachePopupAppearanceOther
@@ -418,18 +424,16 @@
ZOOMAuto-generated
- Caption Font Size
- Smaller Font
- Normal Font
- Larger Font
-
- SYNC
+ Caption font size
+ Smaller font
+ Normal font
+ Larger fontEnable LeakCanaryMemory leak monitoring may cause app to become unresponsive when heap dumping
- Report Out-of-Lifecycle Errors
+ Report Out-of-lifecycle errorsForce reporting of undeliverable Rx exceptions occurring outside of fragment or activity lifecycle after dispose
@@ -452,4 +456,12 @@
yourid, soundcloud.com/youridKeep in mind that this operation can be network expensive.\n\nDo you want to continue?
+
+
+ Playback Speed Control
+ Tempo
+ Pitch
+ Unhook (may cause distortion)
+ Nightcore
+ Default
diff --git a/app/src/main/res/xml/content_settings.xml b/app/src/main/res/xml/content_settings.xml
index c8c1efb12..2ce8bf9e6 100644
--- a/app/src/main/res/xml/content_settings.xml
+++ b/app/src/main/res/xml/content_settings.xml
@@ -37,6 +37,12 @@
android:summary="@string/auto_queue_summary"
android:title="@string/auto_queue_title"/>
+
+
+
+
diff --git a/app/src/test/java/org/schabi/newpipe/util/QuadraticSliderStrategyTest.java b/app/src/test/java/org/schabi/newpipe/util/QuadraticSliderStrategyTest.java
new file mode 100644
index 000000000..8c8d52043
--- /dev/null
+++ b/app/src/test/java/org/schabi/newpipe/util/QuadraticSliderStrategyTest.java
@@ -0,0 +1,86 @@
+package org.schabi.newpipe.util;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class QuadraticSliderStrategyTest {
+ private final static int STEP = 100;
+ private final static float DELTA = 1f / (float) STEP;
+
+ private final SliderStrategy.Quadratic standard =
+ new SliderStrategy.Quadratic(0f, 100f, 50f, STEP);
+ @Test
+ public void testLeftBound() throws Exception {
+ assertEquals(standard.progressOf(0), 0);
+ assertEquals(standard.valueOf(0), 0f, DELTA);
+ }
+
+ @Test
+ public void testCenter() throws Exception {
+ assertEquals(standard.progressOf(50), 50);
+ assertEquals(standard.valueOf(50), 50f, DELTA);
+ }
+
+ @Test
+ public void testRightBound() throws Exception {
+ assertEquals(standard.progressOf(100), 100);
+ assertEquals(standard.valueOf(100), 100f, DELTA);
+ }
+
+ @Test
+ public void testLeftRegion() throws Exception {
+ final int leftProgress = standard.progressOf(25);
+ final double leftValue = standard.valueOf(25);
+ assertTrue(leftProgress > 0 && leftProgress < 50);
+ assertTrue(leftValue > 0f && leftValue < 50);
+ }
+
+ @Test
+ public void testRightRegion() throws Exception {
+ final int leftProgress = standard.progressOf(75);
+ final double leftValue = standard.valueOf(75);
+ assertTrue(leftProgress > 50 && leftProgress < 100);
+ assertTrue(leftValue > 50f && leftValue < 100);
+ }
+
+ @Test
+ public void testConversion() throws Exception {
+ assertEquals(standard.progressOf(standard.valueOf(0)), 0);
+ assertEquals(standard.progressOf(standard.valueOf(25)), 25);
+ assertEquals(standard.progressOf(standard.valueOf(50)), 50);
+ assertEquals(standard.progressOf(standard.valueOf(75)), 75);
+ assertEquals(standard.progressOf(standard.valueOf(100)), 100);
+ }
+
+ @Test
+ public void testReverseConversion() throws Exception {
+ // Need a larger delta since step size / granularity is too small and causes
+ // floating point round-off errors during conversion
+ final float largeDelta = 1f;
+
+ assertEquals(standard.valueOf(standard.progressOf(0)), 0f, largeDelta);
+ assertEquals(standard.valueOf(standard.progressOf(25)), 25f, largeDelta);
+ assertEquals(standard.valueOf(standard.progressOf(50)), 50f, largeDelta);
+ assertEquals(standard.valueOf(standard.progressOf(75)), 75f, largeDelta);
+ assertEquals(standard.valueOf(standard.progressOf(100)), 100f, largeDelta);
+ }
+
+ @Test
+ public void testQuadraticPropertyLeftRegion() throws Exception {
+ final double differenceCloserToCenter =
+ Math.abs(standard.valueOf(40) - standard.valueOf(45));
+ final double differenceFurtherFromCenter =
+ Math.abs(standard.valueOf(10) - standard.valueOf(15));
+ assertTrue(differenceCloserToCenter < differenceFurtherFromCenter);
+ }
+
+ @Test
+ public void testQuadraticPropertyRightRegion() throws Exception {
+ final double differenceCloserToCenter =
+ Math.abs(standard.valueOf(75) - standard.valueOf(70));
+ final double differenceFurtherFromCenter =
+ Math.abs(standard.valueOf(95) - standard.valueOf(90));
+ assertTrue(differenceCloserToCenter < differenceFurtherFromCenter);
+ }
+}
diff --git a/assets/bitcoin_qr_code.png b/assets/bitcoin_qr_code.png
index 17fa26518..13e3f76fe 100644
Binary files a/assets/bitcoin_qr_code.png and b/assets/bitcoin_qr_code.png differ
diff --git a/assets/bountysource_qr_code.png b/assets/bountysource_qr_code.png
index 4fe03236a..18ff10fd0 100644
Binary files a/assets/bountysource_qr_code.png and b/assets/bountysource_qr_code.png differ
diff --git a/assets/liberapay_donate_button.svg b/assets/liberapay_donate_button.svg
new file mode 100644
index 000000000..c5e245f58
--- /dev/null
+++ b/assets/liberapay_donate_button.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/liberapay_qr_code.png b/assets/liberapay_qr_code.png
new file mode 100644
index 000000000..bc5fe63f3
Binary files /dev/null and b/assets/liberapay_qr_code.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_1.png
index 0ad4394b7..c547cbb79 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_1.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_1.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_2.png
index 12a964104..9c914e81e 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_2.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_2.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_3.png
index 7696e5071..fd7064e0e 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_3.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_3.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_4.png
index 341906224..b5d413142 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_4.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_4.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_5.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_5.png
index c836c9dbe..08c1c211c 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_5.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_5.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_7.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_7.png
index 152e76d02..3bfe97765 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_7.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_7.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_8.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_8.png
index 276d99fbb..12cf3002e 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_8.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_8.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_9.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_9.png
index 805bb7bfb..fca7a0128 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_9.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot_9.png differ