Merge branch 'dev' of github.com:teamnewpipe/NewPipe into dev

This commit is contained in:
Schabi 2018-03-28 09:58:18 +02:00
commit 2f4097ca9d
107 changed files with 2918 additions and 865 deletions

View file

@ -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).
<table>
<tr>
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin" /></td>
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR Code" width="100px"/></td>
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
</tr>
<tr>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alz="Bountysource" width="190px" /></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"/></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn." /></a></td>
</tr>
<tr>
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin" /></td>
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR Code" width="100px"/></td>
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
</tr>
<tr>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" /></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"/></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px" /></a></td>
</tr>
<tr>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px" /></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"/></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn." /></a></td>
</tr>
</table>
## License

View file

@ -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"
}

View file

@ -28,6 +28,12 @@
</intent-filter>
</activity>
<receiver android:name="android.support.v4.media.session.MediaButtonReceiver" >
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<activity
android:name=".player.old.PlayVideoActivity"
android:configChanges="orientation|keyboardHidden|screenSize"

View file

@ -8,9 +8,7 @@ import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
import com.squareup.leakcanary.RefWatcher;
import icepick.Icepick;
@ -94,35 +92,4 @@ public abstract class BaseFragment extends Fragment {
activity.getSupportActionBar().setTitle(title);
}
}
/*//////////////////////////////////////////////////////////////////////////
// DisplayImageOptions default configurations
//////////////////////////////////////////////////////////////////////////*/
public static final DisplayImageOptions BASE_OPTIONS =
new DisplayImageOptions.Builder().cacheInMemory(true).build();
public static final DisplayImageOptions DISPLAY_AVATAR_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_OPTIONS)
.showImageOnLoading(R.drawable.buddy)
.showImageForEmptyUri(R.drawable.buddy)
.showImageOnFail(R.drawable.buddy)
.build();
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_OPTIONS)
.displayer(new FadeInBitmapDisplayer(250))
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
.showImageOnFail(R.drawable.dummy_thumbnail)
.build();
public static final DisplayImageOptions DISPLAY_BANNER_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_OPTIONS)
.showImageOnLoading(R.drawable.channel_banner)
.showImageForEmptyUri(R.drawable.channel_banner)
.showImageOnFail(R.drawable.channel_banner)
.build();
}

View file

@ -1,6 +1,10 @@
package org.schabi.newpipe;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.preference.PreferenceManager;
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
@ -10,16 +14,33 @@ import java.io.IOException;
import java.io.InputStream;
public class ImageDownloader extends BaseImageDownloader {
private final Resources resources;
private final SharedPreferences preferences;
private final String downloadThumbnailKey;
public ImageDownloader(Context context) {
super(context);
this.resources = context.getResources();
this.preferences = PreferenceManager.getDefaultSharedPreferences(context);
this.downloadThumbnailKey = context.getString(R.string.download_thumbnail_key);
}
public ImageDownloader(Context context, int connectTimeout, int readTimeout) {
super(context, connectTimeout, readTimeout);
private boolean isDownloadingThumbnail() {
return preferences.getBoolean(downloadThumbnailKey, true);
}
@SuppressLint("ResourceType")
@Override
public InputStream getStream(String imageUri, Object extra) throws IOException {
if (isDownloadingThumbnail()) {
return super.getStream(imageUri, extra);
} else {
return resources.openRawResource(R.drawable.dummy_thumbnail_dark);
}
}
protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
Downloader downloader = (Downloader) NewPipe.getDownloader();
final Downloader downloader = (Downloader) NewPipe.getDownloader();
return downloader.stream(imageUri);
}
}

View file

@ -176,9 +176,11 @@ public class MainActivity extends AppCompatActivity {
// when the user returns to MainActivity
drawer.closeDrawer(Gravity.START, false);
try {
String selectedServiceName = NewPipe.getService(
ServiceHelper.getSelectedServiceId(this)).getServiceInfo().getName();
headerServiceView.setText(selectedServiceName);
if(BuildConfig.BUILD_TYPE != "release" ) {
String selectedServiceName = NewPipe.getService(
ServiceHelper.getSelectedServiceId(this)).getServiceInfo().getName();
headerServiceView.setText(selectedServiceName);
}
} catch (Exception e) {
ErrorActivity.reportUiError(this, e);
}

View file

@ -71,14 +71,14 @@ public class StreamEntity implements Serializable {
@Ignore
public StreamEntity(final StreamInfoItem item) {
this(item.service_id, item.name, item.url, item.stream_type, item.thumbnail_url,
item.uploader_name, item.duration);
this(item.getServiceId(), item.getName(), item.getUrl(), item.getStreamType(), item.getThumbnailUrl(),
item.getUploaderName(), item.getDuration());
}
@Ignore
public StreamEntity(final StreamInfo info) {
this(info.service_id, info.name, info.url, info.stream_type, info.thumbnail_url,
info.uploader_name, info.duration);
this(info.getServiceId(), info.getName(), info.getUrl(), info.getStreamType(), info.getThumbnailUrl(),
info.getUploaderName(), info.getDuration());
}
@Ignore

View file

@ -205,7 +205,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
if (DEBUG) Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]");
switch (checkedId) {
case R.id.audio_button:
setupAudioSpinner(currentInfo.audio_streams, streamsSpinner);
setupAudioSpinner(currentInfo.getAudioStreams(), streamsSpinner);
break;
case R.id.video_button:
setupVideoSpinner(sortedStreamVideosList, streamsSpinner);

View file

@ -43,6 +43,7 @@ import android.widget.Toast;
import com.nirhart.parallaxscroll.views.ParallaxScrollView;
import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
import org.schabi.newpipe.R;
@ -73,6 +74,7 @@ import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.InfoCache;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.Localization;
@ -383,7 +385,8 @@ public class VideoDetailFragment
}
break;
case R.id.detail_thumbnail_root_layout:
if (currentInfo.video_streams.isEmpty() && currentInfo.video_only_streams.isEmpty()) {
if (currentInfo.getVideoStreams().isEmpty()
&& currentInfo.getVideoOnlyStreams().isEmpty()) {
openBackgroundPlayer(false);
} else {
openVideoPlayer();
@ -580,30 +583,25 @@ public class VideoDetailFragment
};
}
private void initThumbnailViews(StreamInfo info) {
private void initThumbnailViews(@NonNull StreamInfo info) {
thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
if (!TextUtils.isEmpty(info.getThumbnailUrl())) {
imageLoader.displayImage(
info.getThumbnailUrl(),
thumbnailImageView,
DISPLAY_THUMBNAIL_OPTIONS, new SimpleImageLoadingListener() {
final String infoServiceName = NewPipe.getNameOfService(info.getServiceId());
final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() {
@Override
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
ErrorActivity.reportError(
activity,
failReason.getCause(),
null,
activity.findViewById(android.R.id.content),
ErrorActivity.ErrorInfo.make(UserAction.LOAD_IMAGE,
NewPipe.getNameOfService(currentInfo.getServiceId()),
imageUri,
R.string.could_not_load_thumbnails));
showSnackBarError(failReason.getCause(), UserAction.LOAD_IMAGE,
infoServiceName, imageUri, R.string.could_not_load_thumbnails);
}
});
};
imageLoader.displayImage(info.getThumbnailUrl(), thumbnailImageView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener);
}
if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) {
imageLoader.displayImage(info.getUploaderAvatarUrl(), uploaderThumb, DISPLAY_AVATAR_OPTIONS);
imageLoader.displayImage(info.getUploaderAvatarUrl(), uploaderThumb,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
}
}
@ -618,7 +616,8 @@ public class VideoDetailFragment
relatedStreamRootLayout.setVisibility(View.VISIBLE);
} else nextStreamTitle.setVisibility(View.GONE);
if (info.related_streams != null && !info.related_streams.isEmpty() && showRelatedStreams) {
if (info.getRelatedStreams() != null
&& !info.getRelatedStreams().isEmpty() && showRelatedStreams) {
//long first = System.nanoTime(), each;
int to = info.getRelatedStreams().size() >= 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);

View file

@ -20,7 +20,7 @@ import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
public abstract class BaseListInfoFragment<I extends ListInfo>
extends BaseListFragment<I, ListExtractor.InfoItemPage> {
extends BaseListFragment<I, ListExtractor.InfoItemsPage> {
@State
protected int serviceId = Constants.NO_SERVICE_ID;
@ -117,7 +117,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
.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<I extends ListInfo>
* Implement the logic to load more items<br/>
* You can use the default implementations from {@link org.schabi.newpipe.util.ExtractorHelper}
*/
protected abstract Single<ListExtractor.InfoItemPage> loadMoreItemsLogic();
protected abstract Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic();
protected void loadMoreItems() {
isLoading.set(true);
@ -135,9 +135,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
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<I extends ListInfo>
}
@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<I extends ListInfo>
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();

View file

@ -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<ChannelInfo> {
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Single<ListExtractor.InfoItemPage> loadMoreItemsLogic() {
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPageUrl);
}
@ -415,8 +420,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
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<ChannelInfo> {
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<ChannelInfo> {
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<ChannelInfo> {
}
private PlayQueue getPlayQueue(final int index) {
final List<StreamInfoItem> 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()) {

View file

@ -297,12 +297,12 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, 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<List<SubscriptionEntity>, Voi
private boolean doesItemExist(final List<InfoItem> 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;

View file

@ -141,7 +141,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
}
@Override
public Single<ListExtractor.InfoItemPage> loadMoreItemsLogic() {
public Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
String contentCountry = PreferenceManager
.getDefaultSharedPreferences(activity)
.getString(getString(R.string.content_country_key),
@ -174,7 +174,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
}
@Override
public void handleNextItems(ListExtractor.InfoItemPage result) {
public void handleNextItems(ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {

View file

@ -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<PlaylistInfo> {
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Single<ListExtractor.InfoItemPage> loadMoreItemsLogic() {
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPageUrl);
}
@ -268,8 +272,10 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
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<PlaylistInfo> {
}
private PlayQueue getPlayQueue(final int index) {
final List<StreamInfoItem> 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()) {

View file

@ -71,7 +71,9 @@ import io.reactivex.subjects.PublishSubject;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor.InfoItemPage> implements BackPressable {
public class SearchFragment
extends BaseListFragment<SearchResult, ListExtractor.InfoItemsPage>
implements BackPressable {
/*//////////////////////////////////////////////////////////////////////////
// Search
@ -759,12 +761,7 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
public void handleSuggestions(@NonNull final List<SuggestionItem> 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<SearchResult, ListExtractor
}
@Override
public void handleNextItems(ListExtractor.InfoItemPage result) {
public void handleNextItems(ListExtractor.InfoItemsPage result) {
showListFooter(false);
currentPage = Integer.parseInt(result.getNextPageUrl());
infoListAdapter.addInfoItemList(result.getNextItemsList());
infoListAdapter.addInfoItemList(result.getItems());
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.SEARCHED, NewPipe.getNameOfService(serviceId)

View file

@ -1,12 +1,10 @@
package org.schabi.newpipe.fragments.local;
import android.content.Context;
import android.graphics.Bitmap;
import android.widget.ImageView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.process.BitmapProcessor;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.util.OnClickGesture;

View file

@ -151,7 +151,10 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
@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

View file

@ -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));
}
}
///////////////////////////////////////////////////////////////////////////

View file

@ -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();
}

View file

@ -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);
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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);
}

View file

@ -104,8 +104,8 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) {
setTitle(getString(R.string.tab_subscriptions));
if (activity != null && isVisibleToUser) {
setTitle(activity.getString(R.string.tab_subscriptions));
}
}
@ -401,12 +401,8 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
List<InfoItem> items = new ArrayList<>();
for (final SubscriptionEntity subscription : subscriptions) items.add(subscription.toChannelInfoItem());
Collections.sort(items, new Comparator<InfoItem>() {
@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;
}

View file

@ -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<StreamHistoryEntry> {
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);
}
}

View file

@ -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;
}

View file

@ -203,7 +203,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
return FOOTER_TYPE;
}
final InfoItem item = infoItemList.get(position);
switch (item.info_type) {
switch (item.getInfoType()) {
case STREAM:
return useMiniVariant ? MINI_STREAM_HOLDER_TYPE : STREAM_HOLDER_TYPE;
case CHANNEL:

View file

@ -44,15 +44,16 @@ public class ChannelInfoItemHolder extends ChannelMiniInfoItemHolder {
if (!(infoItem instanceof ChannelInfoItem)) return;
final ChannelInfoItem item = (ChannelInfoItem) infoItem;
itemChannelDescriptionView.setText(item.description);
itemChannelDescriptionView.setText(item.getDescription());
}
@Override
protected String getDetailLine(final ChannelInfoItem item) {
String details = super.getDetailLine(item);
if (item.stream_count >= 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;

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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;

View file

@ -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();
}

View file

@ -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();

View file

@ -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() {

View file

@ -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) {}
};
}

View file

@ -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;
}

View file

@ -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);

View file

@ -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<VideoStream> 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<VideoStream> 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()));

View file

@ -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();
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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) {
}
}

View file

@ -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);
}

View file

@ -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.<MediaSessionCompat.QueueItem>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<MediaSessionCompat.QueueItem> 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) {
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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.
* <br><br>
* 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<Long> 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<StreamInfo> onSuccess = info -> syncInternal(currentItem, info);
final Consumer<Throwable> onError = throwable -> syncInternal(currentItem, null);
@ -347,8 +330,13 @@ public class MediaSourceManager {
// MediaSource Loading
//////////////////////////////////////////////////////////////////////////*/
private Observable<Long> 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<PlayQueueItem> items = new ArrayList<>(
final Set<PlayQueueItem> 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<ManagedMediaSource> 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.
* <br><br>
* If not, then the media source at the current index is ready for playback, and
* {@link #maybeSynchronizePlayer()} is called.
* <br><br>
* 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));
}

View file

@ -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

View file

@ -26,13 +26,13 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> ext
transient Disposable fetchReactor;
AbstractInfoPlayQueue(final U item) {
this(item.getServiceId(), item.getUrl(), null, Collections.<InfoItem>emptyList(), 0);
this(item.getServiceId(), item.getUrl(), null, Collections.<StreamInfoItem>emptyList(), 0);
}
AbstractInfoPlayQueue(final int serviceId,
final String url,
final String nextPageUrl,
final List<InfoItem> streams,
final List<StreamInfoItem> streams,
final int index) {
super(index, extractListItems(streams));
@ -65,10 +65,10 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> 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<T extends ListInfo, U extends InfoItem> ext
};
}
SingleObserver<ListExtractor.InfoItemPage> getNextPageObserver() {
return new SingleObserver<ListExtractor.InfoItemPage>() {
SingleObserver<ListExtractor.InfoItemsPage> getNextPageObserver() {
return new SingleObserver<ListExtractor.InfoItemsPage>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
if (isComplete || isInitial || (fetchReactor != null && !fetchReactor.isDisposed())) {
@ -95,11 +95,11 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> 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<T extends ListInfo, U extends InfoItem> ext
fetchReactor = null;
}
private static List<PlayQueueItem> extractListItems(final List<InfoItem> infos) {
private static List<PlayQueueItem> extractListItems(final List<StreamInfoItem> infos) {
List<PlayQueueItem> result = new ArrayList<>();
for (final InfoItem stream : infos) {
if (stream instanceof StreamInfoItem) {

View file

@ -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<ChannelInfo, C
}
public ChannelPlayQueue(final ChannelInfo info) {
this(info.getServiceId(), info.getUrl(), info.getNextPageUrl(), info.getRelatedStreams(), 0);
this(info.getServiceId(), info.getUrl(), info.getNextPageUrl(), info.getRelatedItems(), 0);
}
public ChannelPlayQueue(final int serviceId,
final String url,
final String nextPageUrl,
final List<InfoItem> streams,
final List<StreamInfoItem> streams,
final int index) {
super(serviceId, url, nextPageUrl, streams, index);
}

View file

@ -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;
}

View file

@ -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();
}
}

View file

@ -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) {}
}

View file

@ -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<PlaylistInfo,
}
public PlaylistPlayQueue(final PlaylistInfo info) {
this(info.getServiceId(), info.getUrl(), info.getNextPageUrl(), info.getRelatedStreams(), 0);
this(info.getServiceId(), info.getUrl(), info.getNextPageUrl(), info.getRelatedItems(), 0);
}
public PlaylistPlayQueue(final int serviceId,
final String url,
final String nextPageUrl,
final List<InfoItem> streams,
final List<StreamInfoItem> streams,
final int index) {
super(serviceId, url, nextPageUrl, streams, index);
}

View file

@ -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);
}
}
}
}

View file

@ -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) {

View file

@ -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);
}
}

View file

@ -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<InfoItemPage> getMoreSearchItems(final int serviceId,
public static Single<InfoItemsPage> 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<InfoItemPage> getMoreChannelItems(final int serviceId,
public static Single<InfoItemsPage> 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<InfoItemPage> getMorePlaylistItems(final int serviceId,
public static Single<InfoItemsPage> 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<InfoItemPage> getMoreKioskItems(final int serviceId,
public static Single<InfoItemsPage> getMoreKioskItems(final int serviceId,
final String url,
final String nextStreamsUrl,
final String contentCountry) {

View file

@ -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();
}

View file

@ -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<String, CacheData> 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);

View file

@ -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);
}
}
}

View file

@ -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;
}
}
}

View file

@ -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"/>
</LinearLayout>

View file

@ -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"/>
</RelativeLayout>
@ -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" />

View file

@ -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"/>
</LinearLayout>

View file

@ -0,0 +1,313 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="false"
android:paddingLeft="@dimen/video_item_search_padding"
android:paddingRight="@dimen/video_item_search_padding"
android:paddingTop="@dimen/video_item_search_padding">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"
android:scrollbarAlwaysDrawVerticalTrack="true">
<!-- START HERE -->
<TextView
android:id="@+id/tempoControlText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_centerHorizontal="true"
android:text="@string/playback_tempo"
android:textStyle="bold"
android:textColor="?attr/colorAccent"
android:layout_alignParentTop="true"/>
<RelativeLayout
android:id="@+id/tempoControl"
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal"
android:layout_marginTop="4dp"
android:layout_below="@id/tempoControlText">
<TextView
android:id="@+id/tempoStepDown"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:layout_centerVertical="true"
android:clickable="true"
android:focusable="true"
android:text="--%"
android:textStyle="bold"
android:textColor="?attr/colorAccent"
android:background="?attr/selectableItemBackground"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
tools:ignore="HardcodedText"
tools:text="-5%"/>
<RelativeLayout
android:id="@+id/tempoDisplay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_toRightOf="@id/tempoStepDown"
android:layout_toEndOf="@id/tempoStepDown"
android:layout_toLeftOf="@id/tempoStepUp"
android:layout_toStartOf="@id/tempoStepUp">
<TextView
android:id="@+id/tempoMinimumText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="-.--x"
android:textColor="?attr/colorAccent"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginLeft="4dp"
android:layout_marginStart="4dp"
tools:ignore="HardcodedText"
tools:text="1.00x"/>
<TextView
android:id="@+id/tempoCurrentText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="---%"
android:textColor="?attr/colorAccent"
android:layout_centerHorizontal="true"
android:textStyle="bold"
tools:ignore="HardcodedText"
tools:text="100%"/>
<TextView
android:id="@+id/tempoMaximumText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="---%"
android:textColor="?attr/colorAccent"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="4dp"
android:layout_marginEnd="4dp"
tools:ignore="HardcodedText"
tools:text="300%"/>
<android.support.v7.widget.AppCompatSeekBar
android:id="@+id/tempoSeekbar"
style="@style/Widget.AppCompat.SeekBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/tempoCurrentText"
android:paddingBottom="4dp"
tools:progress="50"/>
</RelativeLayout>
<TextView
android:id="@+id/tempoStepUp"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:clickable="true"
android:focusable="true"
android:text="+-%"
android:textStyle="bold"
android:textColor="?attr/colorAccent"
android:background="?attr/selectableItemBackground"
android:layout_centerVertical="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="4dp"
android:layout_marginEnd="4dp"
tools:ignore="HardcodedText"
tools:text="+5%"/>
</RelativeLayout>
<View
android:id="@+id/separatorPitch"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@id/tempoControl"
android:layout_margin="@dimen/video_item_search_padding"
android:background="?attr/separator_color"/>
<TextView
android:id="@+id/pitchControlText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_centerHorizontal="true"
android:text="@string/playback_pitch"
android:textStyle="bold"
android:textColor="?attr/colorAccent"
android:layout_below="@id/separatorPitch"/>
<RelativeLayout
android:id="@+id/pitchControl"
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal"
android:layout_marginTop="4dp"
android:layout_below="@id/pitchControlText">
<TextView
android:id="@+id/pitchStepDown"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:layout_centerVertical="true"
android:clickable="true"
android:focusable="true"
android:text="--%"
android:textStyle="bold"
android:textColor="?attr/colorAccent"
android:background="?attr/selectableItemBackground"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
tools:ignore="HardcodedText"
tools:text="-5%"/>
<RelativeLayout
android:id="@+id/pitchDisplay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_toRightOf="@+id/pitchStepDown"
android:layout_toEndOf="@+id/pitchStepDown"
android:layout_toLeftOf="@+id/pitchStepUp"
android:layout_toStartOf="@+id/pitchStepUp">
<TextView
android:id="@+id/pitchMinimumText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="---%"
android:textColor="?attr/colorAccent"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginLeft="4dp"
android:layout_marginStart="4dp"
tools:ignore="HardcodedText"
tools:text="25%"/>
<TextView
android:id="@+id/pitchCurrentText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="---%"
android:textColor="?attr/colorAccent"
android:layout_centerHorizontal="true"
android:textStyle="bold"
tools:ignore="HardcodedText"
tools:text="100%"/>
<TextView
android:id="@+id/pitchMaximumText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="---%"
android:textColor="?attr/colorAccent"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="4dp"
android:layout_marginEnd="4dp"
tools:ignore="HardcodedText"
tools:text="300%"/>
<android.support.v7.widget.AppCompatSeekBar
android:id="@+id/pitchSeekbar"
style="@style/Widget.AppCompat.SeekBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/pitchCurrentText"
android:paddingBottom="4dp"
tools:progress="50"/>
</RelativeLayout>
<TextView
android:id="@+id/pitchStepUp"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:clickable="true"
android:focusable="true"
android:text="+-%"
android:textStyle="bold"
android:textColor="?attr/colorAccent"
android:background="?attr/selectableItemBackground"
android:layout_centerVertical="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="4dp"
android:layout_marginEnd="4dp"
tools:ignore="HardcodedText"
tools:text="+5%"/>
</RelativeLayout>
<View
android:id="@+id/separatorCheckbox"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/pitchControl"
android:layout_margin="@dimen/video_item_search_padding"
android:background="?attr/separator_color"/>
<CheckBox
android:id="@+id/unhookCheckbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="false"
android:clickable="true"
android:focusable="true"
android:text="@string/unhook_checkbox"
android:maxLines="1"
android:layout_centerHorizontal="true"
android:layout_below="@id/separatorCheckbox"/>
<LinearLayout
android:id="@+id/presetSelector"
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal"
android:layout_below="@id/unhookCheckbox">
<TextView
android:id="@+id/presetNightcore"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="@string/playback_nightcore"
android:background="?attr/selectableItemBackground"
android:textColor="?attr/colorAccent"/>
<TextView
android:id="@+id/presetReset"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="@string/playback_default"
android:background="?attr/selectableItemBackground"
android:textColor="?attr/colorAccent"/>
</LinearLayout>
<!-- END HERE -->
</RelativeLayout>
</ScrollView>

View file

@ -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"/>

View file

@ -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" />

View file

@ -362,4 +362,9 @@
<string name="live_sync">مُزامَنة</string>
<string name="controls_download_desc">تنزيل الملف المتدفق.</string>
<string name="tab_bookmarks">المؤشرات</string>
<string name="use_inexact_seek_title">استعمال التقديم السريع الغير دقيق</string>
<string name="use_inexact_seek_summary">"التقديم الغير دقيق يسمح للمشغل بالإطلاع الى الأماكن بشكل اسرع مع دقة اقل "</string>
</resources>

View file

@ -227,7 +227,7 @@
<string name="tab_main">Main</string>
<string name="settings_category_player_behavior_title">Verhalten</string>
<string name="settings_category_history_title">Verlauf</string>
<string name="settings_category_history_title">Verlauf &amp; Cache</string>
<string name="playlist">Playlist</string>
<string name="undo">Rückgängig machen</string>
@ -309,7 +309,7 @@
<string name="toggle_orientation">Ausrichtung umschalten</string>
<string name="switch_to_background">In den Hintergrund wechseln</string>
<string name="switch_to_popup">Zu Popup wechseln</string>
<string name="switch_to_main">Zur Hauptseite wechseln</string>
<string name="switch_to_main">Zum normalen Player wechseln</string>
<string name="external_player_unsupported_link_type">Externe Player unterstützen diese Art von Links nicht</string>
<string name="invalid_url_toast">Ungültige URL</string>
@ -359,7 +359,7 @@
<string name="create_playlist">Neue Playlist Erstellen</string>
<string name="delete_playlist">Playlist Löschen</string>
<string name="rename_playlist">Playlist umbenennen</string>
<string name="append_playlist">Zu Playlist Hinzufügen</string>
<string name="append_playlist">Zu Playlist hinzufügen</string>
<string name="set_as_playlist_thumbnail">Als Thumbnail der Playlist festlegen</string>
<string name="unbookmark_playlist">Lesezeichen entfernen</string>
@ -376,4 +376,51 @@
<string name="dismiss">Abbrechen</string>
<string name="normal_caption_font_size">Normale Schriftgröße</string>
<string name="controls_download_desc">Stream-Datei herunterladen</string>
</resources>
<string name="use_inexact_seek_title">Benutze schnelle ungenaue Suche</string>
<string name="use_inexact_seek_summary">Ungenaues Suchen erlaubt dem Player die Positionen schneller mit geringerer Genauigkeit zu suchen</string>
<string name="file">Datei</string>
<string name="invalid_directory">Ungültiges Verzeichnis</string>
<string name="invalid_file">Datei existiert nicht oder nicht ausreichende Rechte um sie zu lesen oder zu beschreiben</string>
<string name="file_name_empty_error">Dateiname darf nicht leer sein</string>
<string name="error_occurred_detail">Ein Fehler ist aufgetreten: %1$s</string>
<string name="caption_auto_generated">Automatisch erzeugt</string>
<string name="smaller_caption_font_size">Kleinere Schriftgröße</string>
<string name="larger_caption_font_size">Größere Schriftgröße</string>
<string name="enable_leak_canary_title">LeakCanary aktivieren</string>
<string name="import_from">Import von</string>
<string name="export_to">Export nach</string>
<string name="import_ongoing">Importiere…</string>
<string name="export_ongoing">Exportiere…</string>
<string name="import_file_title">Datei importieren</string>
<string name="previous_export">Vorheriger Export</string>
<string name="import_network_expensive_warning">Beachte, dass diese Aktion sehr Netzwerk intensiv sein kann.
\n
\nMöchtest du fortfahren?</string>
<string name="download_thumbnail_title">Thumbnails laden</string>
<string name="thumbnail_cache_wipe_complete_notice">Bildercache gelöscht</string>
<string name="metadata_cache_wipe_title">Leere die gecachten Metadaten</string>
<string name="metadata_cache_wipe_summary">Entfene alle gecachten Website-Daten</string>
<string name="metadata_cache_wipe_complete_notice">Metadatencache gelöscht</string>
<string name="settings_category_debug_title">Fehlersuche</string>
<string name="invalid_source">Ungültige Datei-/Inhaltsquelle</string>
<string name="export_complete_toast">Export abgeschlossen</string>
<string name="import_complete_toast">Import abgeschlossen</string>
<string name="playlist_name_input">Name</string>
<string name="import_export_title">Import/Export</string>
<string name="import_title">Import</string>
<string name="subscriptions_import_unsuccessful">Import der Abonnements fehlgeschlagen</string>
<string name="subscriptions_export_unsuccessful">Export der Abonnements fehlgeschlagen</string>
<string name="playback_speed_control">Wiedergabegeschwindigkeit</string>
<string name="playback_tempo">Tempo</string>
<string name="playback_pitch">Tonhöhe</string>
<string name="unhook_checkbox">Aushaken (kann zu Verzerrungen führen)</string>
<string name="playback_nightcore">Nightcore</string>
<string name="playback_default">Standard</string>
</resources>

View file

@ -17,7 +17,7 @@
<string name="download_path_title">Ruta de descarga de vídeo</string>
<string name="download_path_summary">Ruta para almacenar los vídeos descargados</string>
<string name="download_path_dialog_title">Introducir directorio de descargas para vídeos</string>
<string name="default_resolution_title">Resolución de vídeo por defecto</string>
<string name="default_resolution_title">Resolución por defecto de vídeo</string>
<string name="play_with_kodi_title">Reproducir con Kodi</string>
<string name="kore_not_found">Aplicación Kore no encontrada. ¿Instalarla?</string>
<string name="show_play_with_kodi_title">Mostrar opción \"Reproducir con Kodi\"</string>
@ -156,7 +156,7 @@ abrir en modo popup</string>
<string name="show_higher_resolutions_title">Mostrar resoluciones más altas</string>
<string name="show_higher_resolutions_summary">Solo algunos dispositivos soportan reproducción de vídeos en 2K/4K</string>
<string name="default_popup_resolution_title">Resolución del popup por defecto</string>
<string name="default_popup_resolution_title">Resolución por defecto del popup</string>
<string name="controls_background_title">Segundo plano</string>
<string name="controls_popup_title">Popup</string>
@ -233,7 +233,7 @@ abrir en modo popup</string>
<string name="settings_category_player_title">Reproductor</string>
<string name="settings_category_player_behavior_title">Funcionamiento</string>
<string name="settings_category_history_title">Historial</string>
<string name="settings_category_history_title">Historial y Caché</string>
<string name="playlist">Lista de reproducción</string>
<string name="undo">Deshacer</string>
@ -410,4 +410,56 @@ abrir en modo popup</string>
<string name="auto_queue_summary">Automáticamente añadir un vídeo relacionado cuando el reproductor llegue al último vídeo en una lista de reproducción no repetible.</string>
<string name="live">DIRECTO</string>
<string name="live_sync">SINCRONIZAR</string>
<string name="file">Archivo</string>
<string name="invalid_directory">Directorio invalido</string>
<string name="invalid_source">Fuente del archivo/contenido inválida</string>
<string name="invalid_file">El archivo no existe o el permiso es insuficiente para leerlo o escribir en él</string>
<string name="file_name_empty_error">El nombre del archivo no puede estar vacío</string>
<string name="error_occurred_detail">Ocurrió un error: %1$s</string>
<string name="import_export_title">Importar/Exportar</string>
<string name="import_title">Importar</string>
<string name="import_from">Importar desde</string>
<string name="export_to">Exportar a</string>
<string name="import_ongoing">Importando…</string>
<string name="export_ongoing">Exportando…</string>
<string name="import_file_title">Importar archivo</string>
<string name="previous_export">Exportación anterior</string>
<string name="subscriptions_import_unsuccessful">Importación de suscripciones fallida</string>
<string name="subscriptions_export_unsuccessful">Exportación de suscripciones fallida</string>
<string name="import_youtube_instructions">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)</string>
<string name="import_soundcloud_instructions">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)</string>
<string name="import_soundcloud_instructions_hint">suID, soundcloud.com/suID</string>
<string name="import_network_expensive_warning">Tenga en cuenta que esta operación puede ser costosa para la red.
\n
\n¿Desea continuar?</string>
<string name="download_thumbnail_title">Cargar Miniaturas</string>
<string name="download_thumbnail_summary">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.</string>
<string name="thumbnail_cache_wipe_complete_notice">Caché de imagen limpiado</string>
<string name="metadata_cache_wipe_title">Metadatos eliminados del caché</string>
<string name="metadata_cache_wipe_summary">Eliminar todos los datos de la página web en caché</string>
<string name="metadata_cache_wipe_complete_notice">Metadatos del caché limpiados</string>
<string name="playback_speed_control">Control de velocidad de la reproducción</string>
<string name="playback_tempo">Tiempo</string>
<string name="playback_pitch">Tono</string>
<string name="unhook_checkbox">Desenganchar (puede casusar distorsión)</string>
<string name="playback_nightcore">Nightcore (tipo de música)</string>
<string name="playback_default">Reproducción por defecto</string>
</resources>

View file

@ -28,7 +28,7 @@
<string name="m4a_description">M4A  meilleure qualité</string>
<string name="download_dialog_title">Télécharger</string>
<string name="next_video_title">Vidéo suivante</string>
<string name="show_next_and_similar_title">Afficher les vidéos suivantes et similaires</string>
<string name="show_next_and_similar_title">Afficher vidéos suivantes/liées</string>
<string name="url_not_supported_toast">Lien non pris en charge</string>
<string name="settings_category_video_audio_title">Vidéo et audio</string>
<string name="settings_category_other_title">Autre</string>
@ -66,11 +66,11 @@
<string name="error_snackbar_message">Désolé, des erreurs se sont produites.</string>
<string name="content">Contenu</string>
<string name="show_age_restricted_content_title">Afficher le contenu avec restriction d\'âge</string>
<string name="show_age_restricted_content_title">Afficher le contenu pour adultes</string>
<string name="duration_live">Direct</string>
<string name="could_not_load_thumbnails">Impossible de charger toutes les miniatures</string>
<string name="youtube_signature_decryption_error">Impossible de déchiffrer la signature du lien</string>
<string name="youtube_signature_decryption_error">Impossible de déchiffrer le lien de la vidéo</string>
<string name="light_parsing_error">Impossible d\'analyser complètement le site web</string>
<string name="live_streams_not_supported">Il s\'agit d\'un direct, non supporté pour le moment.</string>
<string name="sorry_string">Désolé, une erreur inattendue s\'est produite.</string>
@ -121,7 +121,7 @@
<string name="no_available_dir">Sélectionner un dossier de téléchargement disponible</string>
<string name="could_not_load_image">Impossible de charger l\'image</string>
<string name="app_ui_crash">Lappli/linterface a crashé</string>
<string name="app_ui_crash">Lapplication a crashé</string>
<string name="reCaptchaActivity">reCAPTCHA</string>
<string name="black_theme_title">Noir</string>
@ -154,7 +154,7 @@
<string name="controls_popup_title">Fenêtre</string>
<string name="default_popup_resolution_title">Résolution de la fenêtre par défaut</string>
<string name="show_higher_resolutions_title">Afficher des résolutions plus élevées</string>
<string name="show_higher_resolutions_title">Afficher résolutions plus élevées</string>
<string name="show_higher_resolutions_summary">Certains appareils uniquement supportent la lecture 2K/4K</string>
<string name="default_video_format_title">Format vidéo par défaut</string>
<string name="popup_remember_size_pos_title">Mémoriser la taille et la position de la fenêtre</string>
@ -279,8 +279,8 @@
<string name="play_queue_remove">Retirer</string>
<string name="play_queue_stream_detail">Détails</string>
<string name="play_queue_audio_settings">Paramètres audio</string>
<string name="show_hold_to_append_title">Afficher l\'aide \"Appui long pour mettre en file d\'attente\"</string>
<string name="show_hold_to_append_summary">Afficher l\'aide en appuyant sur les boutons \"Arrière-plan\" et \"Fenêtre\" sur la page de détails d\'une vidéo</string>
<string name="show_hold_to_append_title">Afficher les fenêtres d\'aide</string>
<string name="show_hold_to_append_summary">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</string>
<string name="unknown_content">[Inconnu]</string>
<string name="player_recoverable_failure">Récupération de l\'erreur du lecteur</string>
@ -354,7 +354,7 @@
<string name="delete_all">Tout supprimer</string>
<string name="delete_stream_history_prompt">Voulez vous supprimer cet élément de votre historique ?</string>
<string name="delete_all_history_prompt">Êtes vous sûr de supprimer tout votre historique ?</string>
<string name="title_most_played">Titres les plus joués</string>
<string name="title_most_played">Vidéos les plus regardées</string>
<string name="always_ask_open_action">Toujours demander</string>
@ -371,7 +371,7 @@
<string name="delete_playlist_prompt">Voulez-vous supprimer cette playlist ?</string>
<string name="playlist_creation_success">Playlist créée avec succès</string>
<string name="playlist_add_stream_success">Ajoutée à la playlist</string>
<string name="playlist_thumbnail_change_success">La playlist à été modifiée avec succès</string>
<string name="playlist_thumbnail_change_success">Modification de la playlist réussie</string>
<string name="playlist_delete_failure">Échec de la suppression de la playlist</string>
<string name="caption_none">Aucun sous-titre</string>
@ -379,9 +379,78 @@
<string name="resize_fit">Redimensionner</string>
<string name="resize_zoom">Zoom</string>
<string name="caption_font_size_settings_title">Taille de police des sous-titres</string>
<string name="smaller_caption_font_size">Police plus petite</string>
<string name="normal_caption_font_size">Police normale</string>
<string name="larger_caption_font_size">Police plus grande</string>
<string name="caption_font_size_settings_title">Taille des sous-titres</string>
<string name="smaller_caption_font_size">Petite</string>
<string name="normal_caption_font_size">Normale</string>
<string name="larger_caption_font_size">Grande</string>
</resources>
<string name="use_inexact_seek_title">Recherche rapide approximative</string>
<string name="use_inexact_seek_summary">Permettre au lecteur d\'accéder plus rapidement à une position au détriment de la précision</string>
<string name="download_thumbnail_title">Charger imagettes</string>
<string name="download_thumbnail_summary">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.</string>
<string name="thumbnail_cache_wipe_complete_notice">Le cache des images a été nettoyé</string>
<string name="metadata_cache_wipe_title">Supprimer les données en cache</string>
<string name="metadata_cache_wipe_summary">Supprimer toutes les pages web mises en cache</string>
<string name="metadata_cache_wipe_complete_notice">Données en cache supprimées</string>
<string name="file">Fichier</string>
<string name="invalid_directory">Dossier non valide</string>
<string name="invalid_source">Fichier/source du contenu non valide</string>
<string name="invalid_file">Le fichier n\'existe pas ou il n\'est pas permis de le lire</string>
<string name="file_name_empty_error">Le nom du fichier ne peut être vide</string>
<string name="error_occurred_detail">Une erreur s\'est produite: %1$s</string>
<string name="delete_one">Supprimer un seul média</string>
<string name="drawer_header_action_paceholder_text">En cours de développement ;D</string>
<string name="controls_download_desc">Télécharger le fichier de flux</string>
<string name="auto_queue_title">Vidéo suivante en file d\'attente</string>
<string name="auto_queue_summary">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)</string>
<string name="settings_category_debug_title">Débogage</string>
<string name="resize_fill">Remplir</string>
<string name="caption_auto_generated">Affichage automatique</string>
<string name="enable_leak_canary_title">Activer LeakCanary</string>
<string name="enable_leak_canary_summary">Surveiller la baisse de mémoire. L\'application pourrait ne plus répondre correctement</string>
<string name="enable_disposed_exceptions_title">Signaler erreurs Out-of-lifecycle</string>
<string name="enable_disposed_exceptions_summary">Forcer le signalement des exceptions Rx qui surviennent hors activité</string>
<string name="import_export_title">Importer/Exporter</string>
<string name="import_title">Importer</string>
<string name="import_from">Importer de</string>
<string name="export_to">Exporter vers</string>
<string name="import_ongoing">Importation en cours…</string>
<string name="export_ongoing">Exporation en cours…</string>
<string name="import_file_title">Importer fichier</string>
<string name="previous_export">Export précédent</string>
<string name="subscriptions_import_unsuccessful">Import des abonnements échoué</string>
<string name="subscriptions_export_unsuccessful">Export des abonnements échoué</string>
<string name="import_youtube_instructions">\"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)\"</string>
<string name="import_soundcloud_instructions">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)</string>
<string name="import_soundcloud_instructions_hint">votreid, soundcloud.com/votreid</string>
<string name="import_network_expensive_warning">N\'oubliez pas que cette opération peut consommer beaucoup de données mobiles.
\n
\nSouhaitez-vous continuer ?</string>
<string name="playback_speed_control">Vitesse de lecture</string>
<string name="playback_tempo"/>
<string name="unhook_checkbox">Unhook (déformations possibles)</string>
<string name="playback_default">Défaut</string>
</resources>

View file

@ -308,4 +308,8 @@
<string name="export_data_title">יצוא מסד נתונים</string>
<string name="external_player_unsupported_link_type">נגנים חיצוניים לא תומכים בסוגי קישורים אלה</string>
<string name="invalid_url_toast">כתובת שגויה</string>
<string name="file">קובץ</string>
<string name="switch_to_background">העברה לרקע</string>
<string name="switch_to_popup">העברה לחלון צץ</string>
</resources>

View file

@ -235,7 +235,7 @@
<string name="tab_main">Principale</string>
<string name="settings_category_player_title">Riproduttore</string>
<string name="settings_category_player_behavior_title">Comportamento</string>
<string name="settings_category_history_title">Cronologia</string>
<string name="settings_category_history_title">Cronologia e cache</string>
<string name="playlist">Scaletta</string>
<string name="undo">Annulla</string>
@ -411,4 +411,56 @@
<string name="auto_queue_summary">Aggiungi automaticamente un flusso correlato mentre il playback parte dall\'ultimo flusso in una cosa non ripetitiva.</string>
<string name="live_sync">SINCRONIZZAZIONE</string>
</resources>
<string name="file">File</string>
<string name="invalid_directory">Cartella invalida</string>
<string name="invalid_source">Fonte del contenuto o file invalido</string>
<string name="invalid_file">Il file non esiste o non si hanno i permessi sufficenti per leggerlo o scriverci</string>
<string name="file_name_empty_error">Il nome del file non può essere vuoto</string>
<string name="error_occurred_detail">Si è verificato un errore: %1$s</string>
<string name="import_export_title">Importa/Esporta</string>
<string name="import_title">Importa</string>
<string name="import_from">Importa da</string>
<string name="export_to">Esporta a</string>
<string name="import_ongoing">Importando…</string>
<string name="export_ongoing">Esportando…</string>
<string name="import_file_title">Importa file</string>
<string name="previous_export">Esportazione precedente</string>
<string name="subscriptions_import_unsuccessful">L\'importazione delle iscrizioni è fallita</string>
<string name="subscriptions_export_unsuccessful">L\'esportazione delle iscrizioni è fallita</string>
<string name="import_youtube_instructions">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)</string>
<string name="import_soundcloud_instructions">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)</string>
<string name="import_soundcloud_instructions_hint">iltuoid, soundcloud.com/iltuoid</string>
<string name="import_network_expensive_warning">Tieni in mente che questa operazione può richiedere un costo di connessione dati.
\n
\nVuoi continuare?</string>
<string name="download_thumbnail_title">Carica miniature</string>
<string name="download_thumbnail_summary">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.</string>
<string name="thumbnail_cache_wipe_complete_notice">Pulizia della cache delle immagini completata</string>
<string name="metadata_cache_wipe_title">Pulisci la cache dei metadati</string>
<string name="metadata_cache_wipe_summary">Rimuovi tutti i dati delle pagine web salvate</string>
<string name="metadata_cache_wipe_complete_notice">Pulizia della cache dei metadati completata</string>
<string name="playback_speed_control">Controllo della velocità del playback</string>
<string name="playback_tempo">Tempo</string>
<string name="playback_pitch">Tono</string>
<string name="unhook_checkbox">Slega (può causare distorsione)</string>
<string name="playback_nightcore">Nightcore</string>
<string name="playback_default">Valore predefinito</string>
</resources>

View file

@ -78,7 +78,7 @@
<string name="could_not_setup_download_menu">다운로드 메뉴를 설정할 수 없습니다</string>
<string name="live_streams_not_supported">실시간 스트리밍 비디오는 아직 지원되지 않습니다.</string>
<string name="could_not_get_stream">어떠한 스트림도 가져올 수 없습니다</string>
<string name="sorry_string">죄송합니다</string>
<string name="sorry_string">죄송합니다. 오류가 발생했습니다.</string>
<string name="error_report_button_text">이메일을 통해 오류 보고</string>
<string name="error_snackbar_message">죄송합니다. 오류가 발생했습니다.</string>
<string name="error_snackbar_action">보고</string>
@ -215,7 +215,7 @@
<string name="msg_popup_permission">이 권한은 팝업 모드에서
\n열기 위해 필요합니다</string>
<string name="reCaptchaActivity">reCAPTCHA</string>
<string name="reCaptchaActivity">로봇인지 확인 (reCAPTCHA)</string>
<string name="recaptcha_request_toast">reCAPTCHA Challenge 요청됨</string>
<string name="settings_category_downloads_title">다운로드</string>
@ -241,7 +241,7 @@
<string name="contribution_encouragement">번역, 디자인, 코딩 등 다양한 기여를 언제나 환영합니다. 향상에 참여해주세요!</string>
<string name="view_on_github">GitHub에서 보기</string>
<string name="donation_title">기부</string>
<string name="donation_encouragement">뉴파이프는 자원봉사자들이 자발적으로 여가 시간을 활용해 개발하고 있습니다. 이제 이러한 노력에 보답할 시간입니다.</string>
<string name="donation_encouragement">뉴파이프는 자원봉사자들이 자발적으로 여가 시간을 활용해 개발하고 있습니다. 이제 이러한 노력에 보답할 시간입니다!</string>
<string name="give_back">보답하기</string>
<string name="website_title">웹사이트</string>
<string name="website_encouragement">뉴파이프에 관한 최신 및 상세 정보를 얻으려면 웹사이트를 방문하세요.</string>
@ -284,4 +284,148 @@
<string name="start_here_on_main">여기서부터 재생</string>
<string name="start_here_on_background">여기서부터 백그라운드에서 재생</string>
<string name="start_here_on_popup">여기서부터 팝업에 재생</string>
<string name="no_player_found_toast">스트리밍 플레이어를 찾을 수 없습니다. VLC를 설치하면 플레이하실 수 있습니다</string>
<string name="controls_download_desc">스트리밍 파일 다운로드하기.</string>
<string name="show_info">정보 보기</string>
<string name="tab_bookmarks">북마크</string>
<string name="controls_add_to_playlist_title">이곳에 추가</string>
<string name="use_inexact_seek_title">정확하지는 않지만 빠른 탐색</string>
<string name="use_inexact_seek_summary">정확하지 않은 탐색은 빠르게 위치로 탐색할 수 있지만 정확도는 떨어집니다</string>
<string name="auto_queue_title">다음 스트림을 자동으로 재생열에 추가하기</string>
<string name="auto_queue_summary">전 스트림이 무한 반복 재생 큐가 아닐 때 관련된 스트림 자동 재생하기.</string>
<string name="default_content_country_title">기본 콘텐츠 국가</string>
<string name="service_title">서비스</string>
<string name="settings_category_debug_title">디버그</string>
<string name="live">라이브 (LIVE)</string>
<string name="always">항상</string>
<string name="just_once">한번만</string>
<string name="toggle_orientation">디바이스 방향 토글</string>
<string name="switch_to_background">백그라운드로 전환</string>
<string name="switch_to_popup">팝업으로 전환</string>
<string name="switch_to_main">기본으로 전환</string>
<string name="import_data_title">데이터베이스 가져오기</string>
<string name="export_data_title">데이터베이스 내보내기</string>
<string name="import_data_summary">현재 시청 기록 및 구독 목록을 덮어쓰기 됩니다</string>
<string name="export_data_summary">시청 기록, 구독 목록, 재생목록 내보내기.</string>
<string name="external_player_unsupported_link_type">외부 플레이어는 이러한 종류의 링크를 지원하지 않습니다</string>
<string name="invalid_url_toast">잘못된 URL</string>
<string name="video_streams_empty">발견된 비디오 스트림 없음</string>
<string name="audio_streams_empty">발견된 오디오 스트림 없음</string>
<string name="detail_drag_description">드래그하여 재배열</string>
<string name="create">만들기</string>
<string name="delete_one">1개 삭제하기</string>
<string name="delete_all">모두 삭제하기</string>
<string name="dismiss">취소</string>
<string name="rename">이름 바꾸기</string>
<string name="reCaptcha_title">로봇인지 확인합니다</string>
<string name="delete_stream_history_prompt">이 항목을 시청 기록에서 삭제하시겠습니까?</string>
<string name="delete_all_history_prompt">모든 항목을 시청 기록에서 삭제하시겠습니까?</string>
<string name="title_last_played">마지막으로 재생</string>
<string name="title_most_played">가장 많이 재생</string>
<string name="export_complete_toast">내보내기 완료</string>
<string name="import_complete_toast">가져오기 완료</string>
<string name="no_valid_zip_file">유효한 ZIP 파일 없음</string>
<string name="could_not_import_all_files">경고: 모든 파일 가져오기를 실패했습니다.</string>
<string name="override_current_data">이것은 현재 설정을 덮어쓸 것입니다.</string>
<string name="drawer_open">드로어 열기</string>
<string name="drawer_close">드로어 닫기</string>
<string name="drawer_header_action_paceholder_text">여기에 무언가가 추가될 거에요~ :D</string>
<string name="preferred_player_share_menu_dialog_title">선호하는 플레이어로 열기</string>
<string name="preferred_player_settings_title">선호하는 플레이어</string>
<string name="video_player">비디오 플레이어</string>
<string name="background_player">백그라운드 플레이어</string>
<string name="popup_player">팝업 플레이어</string>
<string name="always_ask_open_action">항상 묻기</string>
<string name="preferred_player_fetcher_notification_title">정보 가져오는 중…</string>
<string name="preferred_player_fetcher_notification_message">요청한 콘텐츠를 로딩 중입니다</string>
<string name="create_playlist">새로운 재생목록 만들기</string>
<string name="delete_playlist">재생목록 삭제</string>
<string name="rename_playlist">재생목록 이름 바꾸기</string>
<string name="playlist_name_input">이름</string>
<string name="append_playlist">재생목록에 추가</string>
<string name="set_as_playlist_thumbnail">재생목록 썸네일로 설정</string>
<string name="bookmark_playlist">재생목록 북마크하기</string>
<string name="unbookmark_playlist">북마크 제거하기</string>
<string name="delete_playlist_prompt">이 재생목록을 삭제하시겠습니까?</string>
<string name="playlist_creation_success">재생목록 생성 성공</string>
<string name="playlist_add_stream_success">재생목록에 추가됨</string>
<string name="playlist_thumbnail_change_success">재생목록 썸내일이 바뀜</string>
<string name="playlist_delete_failure">재생목록 삭제 실패</string>
<string name="caption_none">자막 없음</string>
<string name="resize_fit">꼭 맞게 하기</string>
<string name="resize_fill">채우기</string>
<string name="resize_zoom">확대</string>
<string name="caption_auto_generated">자동 생성됨</string>
<string name="caption_font_size_settings_title">자막 폰트 크기</string>
<string name="smaller_caption_font_size">작은 폰트</string>
<string name="normal_caption_font_size">보통 폰트</string>
<string name="larger_caption_font_size">큰 폰트</string>
<string name="live_sync">동기화</string>
<string name="enable_leak_canary_title">LeakCanary 할성화</string>
<string name="enable_leak_canary_summary">메모리 누수 모니터링은 힙 덤핑시 앱이 불안정할 수 있습니다</string>
<string name="enable_disposed_exceptions_title">Out-of-Lifecycle 에러 보고</string>
<string name="enable_disposed_exceptions_summary">프래그먼트 또는 버려진 액티비티 주기 밖에서 일어나는 전달할 수 없는 Rx 예외를 강제적으로 보고하기</string>
<string name="file">파일</string>
<string name="invalid_directory">잘못된 디렉토리</string>
<string name="invalid_source">잘못된 파일/콘덴츠 소스</string>
<string name="invalid_file">파일이 존재하지 않거나 읽기/쓰기 권환이 없습니다</string>
<string name="file_name_empty_error">파일 이름이 비어 있으면 안됩니다</string>
<string name="error_occurred_detail">오류 발생: %1$s</string>
<string name="import_export_title">가져오기/내보내기</string>
<string name="import_title">가져오기</string>
<string name="import_from">이곳으로부터 가져오기</string>
<string name="export_to">이곳으로 내보내기</string>
<string name="import_ongoing">가져오는 중.…</string>
<string name="export_ongoing">내보내는 중…</string>
<string name="import_file_title">파일 가져오기</string>
<string name="previous_export">이전 내보내기</string>
<string name="subscriptions_import_unsuccessful">구독 목록 가져오기 실패</string>
<string name="subscriptions_export_unsuccessful">구독 목록 내보내기 실패</string>
<string name="import_youtube_instructions">YouTube 구독 목록을 가져오려면 내보내기 파일이 필요합니다. 다운로드 하려면
\n1. 이곳으로 가세요: $1$s
\n2. 로그인이 필요하면 하세요
\n3. 다운로드가 곧 시작 됩니다 (이 파일이 내보내기 파일 입니다)</string>
<string name="import_soundcloud_instructions">SoundCloud 팔로잉 목록을 가져오려면 당신의 프로필 URL 및 ID를 알아야 합니다. 알고 있다면 아래에 있는 빈칸에 입력해 주세요.
\n
\n만약 모르신다면, 다음을 참고하세요:
\n
\n1. 모바일 환경이시면 브라우저 설정에서 데스크탑 모드를 활성화해주세요. Chrome 모바일에서는 오른쪽 ... 클릭시 아래쪽에 있습니다.
\n2. 이 주소로 가세요: %1$s
\n3. 로그인이 필요하면 하세요.
\n4. 리디렉트된 곳의 URL을 복사하세요. (이 URL이 당신의 프로필 URL 입니다)</string>
<string name="import_soundcloud_instructions_hint">프로필ID, soundcloud.com/프로필ID</string>
<string name="import_network_expensive_warning">경고: 데이터 소모량이 늘어날 수 있습니다.
\n
\n진행하시겠습니까?</string>
</resources>

View file

@ -382,4 +382,26 @@
<string name="smaller_caption_font_size">Mindre skrift</string>
<string name="normal_caption_font_size">Normal skrift</string>
<string name="larger_caption_font_size">Større skrift</string>
</resources>
<string name="use_inexact_seek_title">Bruk raskt unøyaktig søk</string>
<string name="settings_category_debug_title">Feilretting</string>
<string name="file">Fil</string>
<string name="invalid_directory">Ugyldig mappe</string>
<string name="invalid_source">Ugyldig fil/innholdskilde</string>
<string name="invalid_file">Filen finnes ikke eller så har du ikke tilgang til å lese eller skrive til den</string>
<string name="file_name_empty_error">Filnavn kan ikke være tomt</string>
<string name="error_occurred_detail">En feil inntraff: %1$s</string>
<string name="caption_auto_generated">Auto-generert</string>
<string name="enable_leak_canary_title">Skru på LeakCanary</string>
<string name="import_title">Importer</string>
<string name="import_from">Importer fra</string>
<string name="export_to">Eksporter til</string>
<string name="import_ongoing">Importerer…</string>
<string name="export_ongoing">Eksporterer…</string>
<string name="import_file_title">Importer fil</string>
<string name="previous_export">Forrige eksport</string>
</resources>

View file

@ -234,7 +234,7 @@ te openen in pop-upmodus</string>
<string name="settings_category_player_title">Speler</string>
<string name="settings_category_player_behavior_title">Gedrag</string>
<string name="settings_category_history_title">Geschiedenis</string>
<string name="settings_category_history_title">Geschiedenis &amp; Cache</string>
<string name="playlist">Afspeellijst</string>
<string name="undo">Ongedaan maken</string>
@ -406,4 +406,56 @@ te openen in pop-upmodus</string>
<string name="auto_queue_summary">Automatisch een gerealteerde stream toekennen als het afspelen van de laatste stream strat in een niet-herhalende afspeelwachtlijst.</string>
<string name="live_sync">SYNCHRONISEREN</string>
</resources>
<string name="file">Bestand</string>
<string name="invalid_directory">Ongeldige map</string>
<string name="invalid_source">Ongeldig bestand/Ongeldige inhoudsbron</string>
<string name="invalid_file">Het bestand bestaat niet of u beschikt niet over voldoende machtiging om het te lezen/er naar te schrijven</string>
<string name="file_name_empty_error">De bestandsnaam mag niet leeg zijn</string>
<string name="error_occurred_detail">Er is een fout opgetreden: %1$s</string>
<string name="import_export_title">Importeren/Exporteren</string>
<string name="import_title">Importeren</string>
<string name="import_from">Importeren uit</string>
<string name="export_to">Exporteren naar</string>
<string name="import_ongoing">Bezig met importeren…</string>
<string name="export_ongoing">Bezig met exporteren…</string>
<string name="import_file_title">Bestand importeren</string>
<string name="previous_export">Vorige exportering</string>
<string name="subscriptions_import_unsuccessful">Importeren van abonnementen is mislukt</string>
<string name="subscriptions_export_unsuccessful">Exporteren van abonnementen is mislukt</string>
<string name="import_youtube_instructions">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</string>
<string name="import_soundcloud_instructions">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)</string>
<string name="import_soundcloud_instructions_hint">uwid, soundcloud.com/uwid</string>
<string name="import_network_expensive_warning">Let op: deze actie kan veel MB\'s van uw netwerk gebruiken.
\n
\nWilt u doorgaan?</string>
<string name="download_thumbnail_title">Miniatuurvoorbeelden laden</string>
<string name="download_thumbnail_summary">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.</string>
<string name="thumbnail_cache_wipe_complete_notice">Afbeeldingscache gewist</string>
<string name="metadata_cache_wipe_title">Gecachete metagegevens wissen</string>
<string name="metadata_cache_wipe_summary">Alle gecachete webpagina-gegevens wissen</string>
<string name="metadata_cache_wipe_complete_notice">Metagegevens-cache gewist</string>
<string name="playback_speed_control">Afspeelsnelheid</string>
<string name="playback_tempo">Tempo</string>
<string name="playback_pitch">Toon</string>
<string name="unhook_checkbox">Ontkoppelen (kan ruis veroorzaken)</string>
<string name="playback_nightcore">Nightcore</string>
<string name="playback_default">Standaard</string>
</resources>

View file

@ -397,4 +397,5 @@
<string name="enable_disposed_exceptions_title">Raportuj błędy Out-of-Lifecycle</string>
<string name="enable_disposed_exceptions_summary">Wymusza raportowanie niedostarczonych wyjątków Rx poza cyklem życia fragmentu lub aktywności</string>
</resources>
<string name="use_inexact_seek_title">Użyj szybkiego niedokładnego wyszukiwania</string>
</resources>

View file

@ -385,4 +385,44 @@ abrir em modo popup</string>
<string name="auto_queue_summary">Anexar automaticamente uma stream relacionada quando a reprodução iniciar na última stream em uma fila não repetitiva</string>
<string name="live_sync">Sincronizar</string>
</resources>
<string name="file">Arquivo</string>
<string name="invalid_directory">Diretório inválido</string>
<string name="invalid_source">Origem do arquivo/conteúdo inválido</string>
<string name="invalid_file">Arquivo não existe ou não há permissão para ler ou escrever nele</string>
<string name="file_name_empty_error">Nome do arquivo não pode ser vazio</string>
<string name="error_occurred_detail">Um erro ocorreu: %1$s</string>
<string name="import_export_title">Importar/Exportar</string>
<string name="import_title">Importar</string>
<string name="import_from">Importar de</string>
<string name="export_to">Exportar para</string>
<string name="import_ongoing">Importando…</string>
<string name="export_ongoing">Exportando…</string>
<string name="import_file_title">Importar arquivo</string>
<string name="previous_export">Exportação anteriore</string>
<string name="subscriptions_import_unsuccessful">Importação de inscrições falhou</string>
<string name="subscriptions_export_unsuccessful">Exportação de inscrições falhou</string>
<string name="import_youtube_instructions">"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)"</string>
<string name="import_soundcloud_instructions">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)</string>
<string name="import_soundcloud_instructions_hint">seuid, soundcloud.com/seuid</string>
<string name="import_network_expensive_warning">Tenha em mente que esta operação poderá usar bastante a conexão com a internet.
\n
\nVocê deseja continuar?</string>
</resources>

View file

@ -58,7 +58,7 @@
<string name="main_bg_subtitle">Нажмите поиск, чтобы начать</string>
<string name="msg_wait">Подождите…</string>
<string name="msg_exists">Файл уже существует</string>
<string name="msg_threads">Потоки</string>
<string name="msg_threads">Темы</string>
<string name="finish">OK</string>
<string name="start">Начать</string>
<string name="pause">Пауза</string>
@ -69,7 +69,7 @@
<string name="msg_error">Ошибка</string>
<string name="msg_server_unsupported">Сервер не поддерживается</string>
<string name="msg_running">NewPipe скачивает</string>
<string name="msg_url_malform">Неправильный URL или Интернет не доступен</string>
<string name="msg_url_malform">Неправильный URL или нет доступа к интернету</string>
<string name="msg_running_detail">Нажмите для деталей</string>
<string name="msg_copied">Скопировано в буфер обмена</string>
<string name="no_available_dir">Выберите доступную папку для загрузки</string>
@ -137,19 +137,19 @@
<string name="refresh">Обновить</string>
<string name="clear">Очистить</string>
<string name="use_old_player_title">Использовать старый плеер</string>
<string name="player_gesture_controls_title">Жесты</string>
<string name="player_gesture_controls_title">Контроль жестов</string>
<string name="all">Всё</string>
<string name="filter">Фильтр</string>
<string name="add">Новая миссия</string>
<string name="add">Новая цель</string>
<string name="info_labels">Что:\\nЗапрос:\\nЯзык контента:\\nСервис:\\nВремя по Гринвичу:\\nПакет:\\nВерсия:\\nВерсия ОС:\\nГлобальный диапазон IP:</string>
<string name="msg_popup_permission">Это разрешение нужно для
\nвоспроизведения видео в отдельном окне</string>
\nвоспроизведения в окне</string>
<string name="reCaptchaActivity">reCAPTCHA</string>
<string name="open_in_popup_mode">Открыть в отдельном окне</string>
<string name="show_search_suggestions_summary">Показывать подсказки во время поиска</string>
<string name="show_search_suggestions_summary">Показывать подсказки в поиске</string>
<string name="later">Позже</string>
<string name="disabled">Отключено</string>
@ -162,14 +162,14 @@
<string name="short_thousand"> тыс.</string>
<string name="default_popup_resolution_title">Разрешение в режиме всплывающего окна</string>
<string name="popup_remember_size_pos_summary">Запоминать последний размер и положение всплывающего окна</string>
<string name="show_search_suggestions_title">Живой поиск</string>
<string name="show_search_suggestions_title">Поисковые подсказки</string>
<string name="best_resolution">Лучшее разрешение</string>
<string name="use_old_player_summary">Старый встроенный плеер на Mediaframework</string>
<string name="reCaptcha_title">Запрос reCAPTCHA</string>
<string name="recaptcha_request_toast">Запрошен ввод reCAPTCHA</string>
<string name="show_higher_resolutions_title">Показывать более высокие разрешения</string>
<string name="show_higher_resolutions_title">Показывать более высокое разрешение</string>
<string name="popup_mode_share_menu_title">NewPipe в окне</string>
<string name="title_activity_about">О NewPipe</string>
<string name="action_settings">Настройки</string>
@ -265,7 +265,7 @@
<string name="select_a_kiosk">Выберите киоск</string>
<string name="kiosk">Киоск</string>
<string name="trending">В тренде</string>
<string name="trending">Тренды</string>
<string name="top_50">Топ 50</string>
<string name="new_and_hot">Новое и горячее</string>
<string name="background_player_append">Добавлено в очередь в фоне</string>
@ -275,22 +275,22 @@
<string name="player_stream_failure">Не удалось воспроизвести этот поток</string>
<string name="play_queue_stream_detail">Подробности</string>
<string name="play_queue_audio_settings">Настройки аудио</string>
<string name="no_channel_subscribed_yet">Пока нет подписок</string>
<string name="no_channel_subscribed_yet">Пока нет подписок на каналы</string>
<string name="play_queue_remove">Удалить</string>
<string name="subscribed_button_title">Отписаться</string>
<string name="channel_unsubscribed">Подписка отменена</string>
<string name="show_hold_to_append_title">Подсказка о длинном нажатии</string>
<string name="show_hold_to_append_summary">Отображать подсказку о длинном нажатии на кнопки \"В фоне\" и \"В окне\" для добавления в очередь</string>
<string name="show_hold_to_append_title">Показывать напоминание о длинном нажатии</string>
<string name="show_hold_to_append_summary">Показывать подсказку при нажатии на иконку «В окне» или «В фоне» на странице сведений о видео</string>
<string name="unknown_content">[Неизвестно]</string>
<string name="player_recoverable_failure">Восстановление после ошибки проигрывателя</string>
<string name="title_activity_background_player">Воспроизведение в фоне</string>
<string name="title_activity_popup_player">Воспроизведение в окне</string>
<string name="hold_to_append">Зажмите чтобы добавить в очередь</string>
<string name="enqueue_on_background">Добавить в очередь в фоне</string>
<string name="enqueue_on_popup">Добавить в очередь в окне</string>
<string name="start_here_on_main">Воспроизвести</string>
<string name="title_activity_background_player">В фоне</string>
<string name="title_activity_popup_player">В окне</string>
<string name="hold_to_append">Зажмите, чтобы добавить в очередь</string>
<string name="enqueue_on_background">Добавить в очередь «В фоне»</string>
<string name="enqueue_on_popup">Добавить в очередь «В окне»</string>
<string name="start_here_on_main">Воспроизвести тут</string>
<string name="start_here_on_background">Воспроизвести в фоне</string>
<string name="start_here_on_popup">Воспроизвести в окне</string>
<string name="no_player_found_toast">Ни одного потокового проигрывателя не было найдено (вы можете установить VLC)</string>
@ -326,5 +326,124 @@
<string name="always_ask_player">Всегда спрашивать</string>
<string name="preferred_player_fetcher_notification_title">Получение информации…</string>
<string name="preferred_player_fetcher_notification_message">Загрузка запрашиваемого контента</string>
</resources>
<string name="preferred_player_fetcher_notification_message">Загрузка запрошенного контента</string>
<string name="controls_download_desc">Загрузка файла прямой трансляции.</string>
<string name="show_info">Показать информацию</string>
<string name="tab_bookmarks">Закладки</string>
<string name="controls_add_to_playlist_title">Добавить к</string>
<string name="use_inexact_seek_title">Использовать быстрый, но неточный поиск</string>
<string name="use_inexact_seek_summary">Неточный поиск позволяет плееру искать позицию быстрее, но с пониженной точностью</string>
<string name="auto_queue_title">Автоматическая очередь следующего стрима</string>
<string name="auto_queue_summary">Автоматически добавлять связанные потоки, когда воспроизведение начинается с последнего потока в неповторяющейся очереди воспроизведения.</string>
<string name="settings_category_debug_title">Отладка</string>
<string name="file">Файл</string>
<string name="import_data_title">Импорт данных</string>
<string name="export_data_title">Экспорт данных</string>
<string name="import_data_summary">Ваша текущая история и подписки будут перезаписаны</string>
<string name="export_data_summary">Экспорт истории, подписок и плейлистов.</string>
<string name="invalid_directory">Неправильная директория</string>
<string name="invalid_source">Неправильный файл/контент источника</string>
<string name="invalid_file">Файл не существует или нет разрешения на его прочтение или запись</string>
<string name="file_name_empty_error">Имя файла не может быть пустым</string>
<string name="error_occurred_detail">Произошла ошибка: %1$s</string>
<string name="detail_drag_description">Перетащите, чтобы изменить порядок</string>
<string name="create">Создать</string>
<string name="delete_one">Удалить одно</string>
<string name="delete_all">Удалить всё</string>
<string name="dismiss">Отклонить</string>
<string name="rename">Переименовать</string>
<string name="delete_stream_history_prompt">Вы хотите удалить этот элемент из истории поиска?</string>
<string name="delete_all_history_prompt">Вы уверены, что хотите удалить все элементы из истории?</string>
<string name="title_last_played">Последнее проигрывание</string>
<string name="title_most_played">Наиболее проигрываемые</string>
<string name="export_complete_toast">Экспорт завершён</string>
<string name="import_complete_toast">Импорт завершён</string>
<string name="no_valid_zip_file">Нет верного Zip файла</string>
<string name="could_not_import_all_files">Предупреждение: нет возможности импорта всех файлов.</string>
<string name="override_current_data">Это перезапишет вашу текущую установку.</string>
<string name="drawer_header_action_paceholder_text">Что-то будет тут, скоро ;D</string>
<string name="always_ask_open_action">Всегда спрашивать</string>
<string name="create_playlist">Создать новый плейлист</string>
<string name="delete_playlist">Удалить плейлист</string>
<string name="rename_playlist">Переименовать плейлист</string>
<string name="playlist_name_input">Имя</string>
<string name="append_playlist">Добавить в плейлист</string>
<string name="set_as_playlist_thumbnail">Установить как иконку плейлиста</string>
<string name="bookmark_playlist">Пометить плейлист</string>
<string name="unbookmark_playlist">Удалить пометку</string>
<string name="delete_playlist_prompt">Вы хотите удалить этот плейлист?</string>
<string name="playlist_creation_success">Плейлист успешно создан</string>
<string name="playlist_add_stream_success">Добавлено в плейлист</string>
<string name="playlist_thumbnail_change_success">Иконка плейлиста изменена</string>
<string name="playlist_delete_failure">Ошибка при удалении плейлиста</string>
<string name="caption_none">Без подписи</string>
<string name="resize_fit">Уместить</string>
<string name="resize_fill">Заполнить</string>
<string name="resize_zoom">Приближение</string>
<string name="caption_auto_generated">Автоматически созданный</string>
<string name="caption_font_size_settings_title">Размер шрифта подписи</string>
<string name="smaller_caption_font_size">Маленький шрифт</string>
<string name="normal_caption_font_size">Обычный шрифт</string>
<string name="larger_caption_font_size">Большой шрифт</string>
<string name="live_sync">Синхронизировать</string>
<string name="enable_leak_canary_title">Включить LeakCanary</string>
<string name="enable_leak_canary_summary">Мониторинг утечки памяти может привести к зависанию приложения</string>
<string name="enable_disposed_exceptions_title">Ошибки отчёта вне очереди</string>
<string name="enable_disposed_exceptions_summary">Форсировать отчетность о недопустимых исключениях Rx, возникающих за пределами фрагмента или цикла деятельности, после размещения</string>
<string name="import_export_title">Импорт/Экспорт</string>
<string name="import_title">Импорт</string>
<string name="import_from">Импорт из</string>
<string name="export_to">Экспорт в</string>
<string name="import_ongoing">Импорт…</string>
<string name="export_ongoing">Экспорт…</string>
<string name="import_file_title">Импорт файла</string>
<string name="previous_export">Предыдущий экспорт</string>
<string name="subscriptions_import_unsuccessful">Импорт подписок провален</string>
<string name="subscriptions_export_unsuccessful">Экспорт подписок провален</string>
<string name="import_youtube_instructions">Для импорта подписок из YouTube вам необходимо файл экспорта, которые можно загрузить в соответствии с этими инструкциями:
\n
\n1. Перейдите на: %1$s
\n2. Войдите в ваш аккаунт, если необходимо
\n3. Загрузка должна начаться (это файл экспорта)</string>
<string name="import_soundcloud_instructions">"Для импорта ваших подписок из SoundCloud вы должны знать ссылку на ваш профиль или id. Если вы знаете, просто напишите это в поле ниже и будьте готовы начинать.
\n
\nЕсли вы не знаете, то проследуйте следующей инструкции:
\n
\n1. Включите \"режим рабочего стола\" в браузере (сайт недоступен на телефоне)
\n2. Пройдите на: %1$s
\n3. Войдите в аккаунт, если надо
\n4. Скопируйте адрес из адресной строки (это адрес вашего профиля)
\n
\n"</string>
<string name="import_soundcloud_instructions_hint">вашid, soundcloud.com/вашid</string>
<string name="import_network_expensive_warning">Помните, что за выход в интернет может взиматься плата.
\n
\nВы хотите продолжить?</string>
<string name="download_thumbnail_title">Загрузить превью</string>
</resources>

View file

@ -401,4 +401,44 @@
<string name="auto_queue_summary">Yinelemeyen oynatma kuyruğundaki son akış başladığında ilişkili akışı kuyruğun sonuna kendiliğinden ekle.</string>
<string name="live_sync">EŞZAMANLA</string>
</resources>
<string name="file">Dosya</string>
<string name="invalid_directory">Geçersiz dizin</string>
<string name="invalid_source">Geçersiz dosya/içerik kaynağı</string>
<string name="invalid_file">Dosya yok ya da okuma veya yazma izni yetersiz</string>
<string name="file_name_empty_error">Dosya adı boş olamaz</string>
<string name="error_occurred_detail">Hata oluştu: %1$s</string>
<string name="import_export_title">İçe/Dışa Aktar</string>
<string name="import_title">İçe Aktar</string>
<string name="import_from">Şuradan içe aktar</string>
<string name="export_to">Şuna dışa aktar</string>
<string name="import_ongoing">İçe aktarılıyor…</string>
<string name="export_ongoing">Dışa aktarılıyor…</string>
<string name="import_file_title">Dosyayı içe aktar</string>
<string name="previous_export">Önceki dışa aktarım</string>
<string name="subscriptions_import_unsuccessful">Aboneliklerin içe aktarımı başarısız</string>
<string name="subscriptions_export_unsuccessful">Aboneliklerin dışa aktarımı başarısız</string>
<string name="import_youtube_instructions">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)</string>
<string name="import_soundcloud_instructions">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)</string>
<string name="import_soundcloud_instructions_hint">kimliginiz, soundcloud.com/kimliginiz</string>
<string name="import_network_expensive_warning">Bu sürecin ağ masrafına neden olabileceğini unutmayın.
\n
\nDevam etmek istiyor musunuz?</string>
</resources>

View file

@ -126,9 +126,9 @@
<string name="no_available_dir">Будь ласка, оберіть теку для завантаження</string>
<string name="no_player_found_toast">Потоковий програвач не знайдено (ви можете встановити VLC для відтворення)</string>
<string name="open_in_popup_mode">Відкрити у виринальному режимі</string>
<string name="open_in_popup_mode">Відкрити у віконному режимі</string>
<string name="use_external_video_player_summary">Певні роздільності НЕ МАТИМУТЬ звуку якщо цей параметр увімкнено</string>
<string name="popup_mode_share_menu_title">NewPipe у виринальному вікні</string>
<string name="popup_mode_share_menu_title">NewPipe у віконному режимі</string>
<string name="subscribe_button_title">Підписатися</string>
<string name="subscribed_button_title">Ви підписалися</string>
<string name="channel_unsubscribed">Ви відписалися від каналу</string>
@ -141,14 +141,14 @@
<string name="fragment_whats_new">Новинки</string>
<string name="controls_background_title">Тло</string>
<string name="controls_popup_title">Виринальне вікно</string>
<string name="controls_popup_title">У вікні</string>
<string name="default_popup_resolution_title">Типова роздільна здатність виринального вікна</string>
<string name="default_popup_resolution_title">Типова роздільна здатність вікна</string>
<string name="show_higher_resolutions_summary">Не всі пристрої підтримують програвання 2K/4K відео</string>
<string name="show_higher_resolutions_title">Показувати більші роздільні здатності</string>
<string name="default_video_format_title">Типовий відео формат</string>
<string name="popup_remember_size_pos_title">Пам\'ятати розмір виринального вікна та положення</string>
<string name="popup_remember_size_pos_summary">Пам\'ятати останній розмір та позицію виринального вікна</string>
<string name="popup_remember_size_pos_title">Пам\'ятати розмір та положення вікна</string>
<string name="popup_remember_size_pos_summary">Пам\'ятати останній розмір та позицію вікна</string>
<string name="player_gesture_controls_title">Керування жестами</string>
<string name="player_gesture_controls_summary">Використовувати жести для контролю яскравості та гучності програвача</string>
<string name="show_search_suggestions_title">Шукати схожі</string>
@ -159,16 +159,16 @@
<string name="enable_watch_history_summary">Вести облік перегляду відео</string>
<string name="resume_on_audio_focus_gain_title">Відновити фокус</string>
<string name="resume_on_audio_focus_gain_summary">Продовжувати відтворення опісля переривання (наприклад телефонний дзвінок)</string>
<string name="show_hold_to_append_title">Відображати вказівку Утримуйте для додачі</string>
<string name="show_hold_to_append_title">Показувати стримання для підказки</string>
<string name="default_content_country_title">Усталена країна контенту</string>
<string name="service_title">Сервіс</string>
<string name="settings_category_player_title">Програвач</string>
<string name="settings_category_player_behavior_title">Поведінка</string>
<string name="settings_category_history_title">Історія</string>
<string name="settings_category_popup_title">Виринальне вікно</string>
<string name="popup_playing_toast">Відворення у виринальному вікні</string>
<string name="settings_category_history_title">Історія та кеш</string>
<string name="settings_category_popup_title">Вікно</string>
<string name="popup_playing_toast">Відворення у вікні</string>
<string name="background_player_append">Додано до фонового програвання</string>
<string name="popup_playing_append">Додано до чергу у виринальному вікні</string>
<string name="popup_playing_append">Додано до чергу у вікно</string>
<string name="playlist">Плейлист</string>
<string name="filter">Фільтрувати</string>
<string name="refresh">Оновити</string>
@ -181,12 +181,12 @@
<string name="just_once">Тільки тепер</string>
<string name="notification_channel_name">NewPipe сповіщення</string>
<string name="notification_channel_description">Сповіщення для фонового та виринального програвача NewPipe</string>
<string name="notification_channel_description">Сповіщення для фонового та віконного програвачів NewPipe</string>
<string name="unknown_content">[Невідомо]</string>
<string name="switch_to_background">Перемкнутися до Тла</string>
<string name="switch_to_popup">Перемкнутися до Виринального вікна</string>
<string name="switch_to_popup">Перемкнутися до вікна</string>
<string name="switch_to_main">Перемкнутися до Головної</string>
<string name="import_data_title">Імпортувати базу</string>
@ -204,7 +204,7 @@
<string name="no_videos">Без відео</string>
<string name="msg_url_malform">Помилкова ланка URL або інтернет не є доступним</string>
<string name="msg_popup_permission">Цей дозвіл має бути відкритим
\nу виринальному вікні</string>
\nу вікні</string>
<string name="reCaptchaActivity">«reCAPTCHA»</string>
<string name="settings_category_downloads_title">Завантажити</string>
@ -248,7 +248,7 @@
<string name="controls_add_to_playlist_title">Додати до</string>
<string name="show_hold_to_append_summary">Показувати підказку коли фонова чи виринальна кнопка натиснута на сторінці відео деталей</string>
<string name="show_hold_to_append_summary">Показувати підказку коли натиснута кнопка фону або вікна, на сторінці інформації відео</string>
<string name="toggle_orientation">Перемкнути орієнтацію</string>
<string name="player_unrecoverable_failure">Фатальна помилка програвача</string>
<string name="external_player_unsupported_link_type">Зовнішні програвачі не підтримують такі види ланок</string>
@ -335,14 +335,14 @@
<string name="kiosk">Ятка</string>
<string name="trending">Набуває популярності</string>
<string name="title_activity_background_player">Фоновий програвач</string>
<string name="title_activity_popup_player">Виринальний програвач</string>
<string name="title_activity_popup_player">Віконний програвач</string>
<string name="play_queue_remove">Усунути</string>
<string name="hold_to_append">Затиснути, аби зняти з черги</string>
<string name="enqueue_on_background">Зняти з черги у фоновому програвачеві</string>
<string name="enqueue_on_popup">Зняти з черги у виринальному програвачеві</string>
<string name="enqueue_on_popup">Зняти з черги у віконному програвачеві</string>
<string name="start_here_on_main">Розпочати програвання звідси</string>
<string name="start_here_on_background">Розпочати програвання звідси у фоновому програвачеві</string>
<string name="start_here_on_popup">Розпочати програвання звідси у виринальному програвачеві</string>
<string name="start_here_on_popup">Розпочати програвання у вікні звідси</string>
<string name="drawer_open">Відчинити шухляду</string>
<string name="drawer_close">Зачинити шухляду</string>
@ -383,8 +383,8 @@
<string name="enable_leak_canary_title">Увімкнути LeakCanary</string>
<string name="enable_leak_canary_summary">Під час роботи LeakCanary застосунок може стати несприйнятливим під час гіп-дампінґу</string>
<string name="enable_disposed_exceptions_title">Зазвітувати Out-of-Lifecycle хиби</string>
<string name="enable_disposed_exceptions_summary">Примусове звітування про неможливість доставлення Rx винятків, яку відбуваються за межами фраґменту, або діяльності життєвого циклу після усунення</string>
<string name="enable_disposed_exceptions_title">Зазвітувати Out-of-lifecycle хиби</string>
<string name="enable_disposed_exceptions_summary">Примусове звітування про неможливість доставлення Rx винятків, які відбуваються за межами фраґменту або діяльності життєвого циклу після усунення</string>
<string name="use_inexact_seek_title">Використовувати неточне шукання</string>
<string name="use_inexact_seek_summary">Неточне шукання дозволяє програвачеві рухатися позиціями швидше, проте з меншою точністю</string>
@ -392,4 +392,57 @@
<string name="auto_queue_summary">Автоматично додавати пов\'язаний стрим, під час початку програвання останнього стриму.</string>
<string name="live_sync">СИНХРОНІЗАЦІЯ</string>
</resources>
<string name="file">Файл</string>
<string name="invalid_directory">Неправильна тека</string>
<string name="invalid_source">Неправильний файл/контент джерела</string>
<string name="invalid_file">Файл не існує, або немає дозволу на його запис чи читання</string>
<string name="file_name_empty_error">Ім\'я файлу не повинно бути порожнім</string>
<string name="error_occurred_detail">Трапилась помилка: %1$s</string>
<string name="import_export_title">Імпортування/Експортування</string>
<string name="import_title">Імпортування</string>
<string name="import_from">Імпортувати з</string>
<string name="export_to">Експортувати до</string>
<string name="import_ongoing">Імпортування…</string>
<string name="export_ongoing">Експортування…</string>
<string name="import_file_title">Імпортування файлу</string>
<string name="previous_export">Попереднє експортування</string>
<string name="subscriptions_import_unsuccessful">Не вдалось імпортувати підписки</string>
<string name="subscriptions_export_unsuccessful">Не вдалося експортувати підписки</string>
<string name="import_youtube_instructions">Аби імпортувати ваші підписання Ютюб, вам буде потрібно експортувати файл, який можна буде завантажити наступним чином:
\n
\n1. Перейдіть за цією ланкою: %1$s
\n2. За запитом увійдіть до вашої обліківки
\n3. Завантаження має початися (експортований файл)</string>
<string name="import_soundcloud_instructions">Для імпортування ваших підписок з SoundCloud, ви маєте знати url вашого профайлу або ID. Якщо ви знаєте їх, упишіть їх нижче та можна працювати.
\n
\nЯкщо ви не маєте їх, зробіть наступним чином:
\n
\n1. Увімкніть режимі \"desktop\" у будь-якому з переглядачів (сайт не має підтримки мобільних ґаджетів)
\n
\n2. Перейдіть за цією ланкою: %1$s
\n3. За запитом увійдіть до вашої обліківки
\n4. Скопіюйте url, до якого вас відішле (це й є url вашого профайлу)</string>
<string name="import_soundcloud_instructions_hint">yourid, soundcloud.com/yourid</string>
<string name="import_network_expensive_warning">Майте на увазі: ця операція може потребувати багато трафіку.
\n
\nПродовжуватимете?</string>
<string name="download_thumbnail_title">Завантажити ескізи</string>
<string name="download_thumbnail_summary">Відключити аби зупинити завантаження ескізів та заощадити використання ресурсів та пам\'яті. Увімкнення функції призведе до повного вичищення кешу зображень.</string>
<string name="thumbnail_cache_wipe_complete_notice">Кеш зображень стерто</string>
<string name="metadata_cache_wipe_title">Стерти кеш метаданих</string>
<string name="metadata_cache_wipe_summary">"Усунути всі кешовані дані веб-сторінки "</string>
<string name="metadata_cache_wipe_complete_notice">Кеш метаданих стерто</string>
<string name="playback_speed_control">Керування швидкістю програвання</string>
<string name="playback_tempo">Темп</string>
<string name="playback_pitch">Тон</string>
<string name="unhook_checkbox">Від\'єднати (може спричинити спотворення)</string>
<string name="playback_nightcore">Nightcore</string>
<string name="playback_default">Усталено</string>
</resources>

View file

@ -181,7 +181,7 @@
<string name="resume_on_audio_focus_gain_summary">在干擾結束後繼續播放(例如有來電)</string>
<string name="settings_category_player_title">播放器</string>
<string name="settings_category_player_behavior_title">行為</string>
<string name="settings_category_history_title">歷史紀錄</string>
<string name="settings_category_history_title">歷史記錄和快取</string>
<string name="playlist">播放清單</string>
<string name="undo">復原</string>
@ -241,7 +241,7 @@
<string name="item_deleted">項目已刪除</string>
<string name="delete_item_search_history">確定要刪除此項搜尋紀錄嗎?</string>
<string name="no_player_found_toast">沒有找到串流播放器(你可以安裝 VLC播放器 來播放)</string>
<string name="show_hold_to_append_title">顯示鎖定到附加指引上</string>
<string name="show_hold_to_append_title">顯示鎖定到附加提示</string>
<string name="default_content_country_title">預設內容國家</string>
<string name="service_title">服務</string>
<string name="background_player_append">在背景播放器上等候</string>
@ -387,4 +387,55 @@
<string name="auto_queue_summary">在非重複播放佇列中的最後一個串流上開始播放時,自動附上相關串流。</string>
<string name="live_sync">同步</string>
</resources>
<string name="file">檔案</string>
<string name="invalid_directory">無效的目錄</string>
<string name="invalid_source">無效的檔案/內容來源</string>
<string name="file_name_empty_error">檔案名稱不能留空</string>
<string name="error_occurred_detail">發生錯誤:%1$s</string>
<string name="import_export_title">匯入/匯出</string>
<string name="import_title">匯入</string>
<string name="import_from">匯入來自</string>
<string name="export_to">匯出到</string>
<string name="import_ongoing">正在匯入…</string>
<string name="export_ongoing">正在匯出…</string>
<string name="import_file_title">匯入檔案</string>
<string name="subscriptions_import_unsuccessful">訂閱匯入失敗</string>
<string name="subscriptions_export_unsuccessful">訂閱匯出失敗</string>
<string name="previous_export">之前的匯出</string>
<string name="invalid_file">檔案不存在或沒有足夠的權限讀取或寫入</string>
<string name="import_youtube_instructions">要匯入您的 YouTube 訂閱,您必須匯出檔案,可以按照以下說明進行下載:
\n
\n1. 轉到此網址:%1$s
\n2. 當被詢問時登入您的帳戶
\n3. 下載應該開始 ( 這就是匯出的檔案 )</string>
<string name="import_soundcloud_instructions_hint">yourid, soundcloud.com/yourid</string>
<string name="import_network_expensive_warning">請記住,此操作可能會造成網路昂貴花費。
\n
\n您想繼續嗎</string>
<string name="import_soundcloud_instructions">要匯入您的 SoundCloud您必須知道您的個人資料網址或 ID。 如果您這樣做,只需在下方的輸入中鍵入其中的任意一個,然後就可以開始了。
\n
\n如果您不這樣做您可以按照以下步驟操作
\n1. 在一些瀏覽器中啟用「桌面模式」(該網站不適用於行動裝置)
\n2. 移至此網址:%1$s
\n3. 詢問時登入到您的帳號
\n4. 複製網址您會被重新導向(這是您的個人資料網址)</string>
<string name="download_thumbnail_title">載入縮圖</string>
<string name="download_thumbnail_summary">停用可以停止載入所有的縮圖和儲存資料與使用的記憶體。更改此動作將清除在記憶體和磁碟上的影像快取。</string>
<string name="thumbnail_cache_wipe_complete_notice">圖片快取被抹除</string>
<string name="metadata_cache_wipe_title">抹除快取中介資料</string>
<string name="metadata_cache_wipe_summary">移除所有快取網頁的資料</string>
<string name="metadata_cache_wipe_complete_notice">中介資料快取已抹除</string>
<string name="playback_speed_control">重播速度控制</string>
<string name="playback_tempo">節拍</string>
<string name="playback_pitch">間距</string>
<string name="unhook_checkbox">解除 (可能導致失真)</string>
<string name="playback_nightcore">Nightcore</string>
<string name="playback_default">預設</string>
</resources>

View file

@ -160,6 +160,10 @@
<string name="import_data">import_data</string>
<string name="export_data">export_data</string>
<string name="download_thumbnail_key" translatable="false">download_thumbnail_key</string>
<string name="metadata_cache_wipe_key" translatable="false">cache_wipe_key</string>
<!-- FileName Downloads -->
<string name="settings_file_charset_key" translatable="false">file_rename</string>
<string name="settings_file_replacement_character_key" translatable="false">file_replacement_character</string>

View file

@ -74,6 +74,12 @@
<string name="popup_remember_size_pos_summary">Remember last size and position of popup</string>
<string name="use_inexact_seek_title">Use fast inexact seek</string>
<string name="use_inexact_seek_summary">Inexact seek allows the player to seek to positions faster with reduced precision</string>
<string name="download_thumbnail_title">Load thumbnails</string>
<string name="download_thumbnail_summary">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.</string>
<string name="thumbnail_cache_wipe_complete_notice">Image cache wiped</string>
<string name="metadata_cache_wipe_title">Wipe cached metadata</string>
<string name="metadata_cache_wipe_summary">Remove all cached webpage data</string>
<string name="metadata_cache_wipe_complete_notice">Metadata cache wiped</string>
<string name="auto_queue_title">Auto-queue next stream</string>
<string name="auto_queue_summary">Automatically append a related stream when playback starts on the last stream in a non-repeating play queue.</string>
<string name="player_gesture_controls_title">Player gesture controls</string>
@ -89,7 +95,7 @@
<string name="download_dialog_title">Download</string>
<string name="next_video_title">Next video</string>
<string name="show_next_and_similar_title">Show next and similar videos</string>
<string name="show_hold_to_append_title">Show Hold to Append Tip</string>
<string name="show_hold_to_append_title">Show hold to append tip</string>
<string name="show_hold_to_append_summary">Show tip when background or popup button is pressed on video details page</string>
<string name="url_not_supported_toast">URL not supported</string>
<string name="default_content_country_title">Default content country</string>
@ -98,7 +104,7 @@
<string name="settings_category_player_title">Player</string>
<string name="settings_category_player_behavior_title">Behavior</string>
<string name="settings_category_video_audio_title">Video &amp; Audio</string>
<string name="settings_category_history_title">History</string>
<string name="settings_category_history_title">History &amp; Cache</string>
<string name="settings_category_popup_title">Popup</string>
<string name="settings_category_appearance_title">Appearance</string>
<string name="settings_category_other_title">Other</string>
@ -418,18 +424,16 @@
<string name="resize_zoom">ZOOM</string>
<string name="caption_auto_generated">Auto-generated</string>
<string name="caption_font_size_settings_title">Caption Font Size</string>
<string name="smaller_caption_font_size">Smaller Font</string>
<string name="normal_caption_font_size">Normal Font</string>
<string name="larger_caption_font_size">Larger Font</string>
<string name="live_sync">SYNC</string>
<string name="caption_font_size_settings_title">Caption font size</string>
<string name="smaller_caption_font_size">Smaller font</string>
<string name="normal_caption_font_size">Normal font</string>
<string name="larger_caption_font_size">Larger font</string>
<!-- Debug Settings -->
<string name="enable_leak_canary_title">Enable LeakCanary</string>
<string name="enable_leak_canary_summary">Memory leak monitoring may cause app to become unresponsive when heap dumping</string>
<string name="enable_disposed_exceptions_title">Report Out-of-Lifecycle Errors</string>
<string name="enable_disposed_exceptions_title">Report Out-of-lifecycle errors</string>
<string name="enable_disposed_exceptions_summary">Force reporting of undeliverable Rx exceptions occurring outside of fragment or activity lifecycle after dispose</string>
<!-- Subscriptions import/export -->
@ -452,4 +456,12 @@
<string name="import_soundcloud_instructions_hint">yourid, soundcloud.com/yourid</string>
<string name="import_network_expensive_warning">Keep in mind that this operation can be network expensive.\n\nDo you want to continue?</string>
<!-- Playback Parameters -->
<string name="playback_speed_control">Playback Speed Control</string>
<string name="playback_tempo">Tempo</string>
<string name="playback_pitch">Pitch</string>
<string name="unhook_checkbox">Unhook (may cause distortion)</string>
<string name="playback_nightcore">Nightcore</string>
<string name="playback_default">Default</string>
</resources>

View file

@ -37,6 +37,12 @@
android:summary="@string/auto_queue_summary"
android:title="@string/auto_queue_title"/>
<SwitchPreference
android:defaultValue="true"
android:key="@string/download_thumbnail_key"
android:title="@string/download_thumbnail_title"
android:summary="@string/download_thumbnail_summary"/>
<ListPreference
android:defaultValue="@string/kiosk_page_key"
android:entries="@array/main_page_content_names"

View file

@ -16,4 +16,9 @@
android:summary="@string/enable_search_history_summary"
android:title="@string/enable_search_history_title"/>
<Preference
android:key="@string/metadata_cache_wipe_key"
android:summary="@string/metadata_cache_wipe_summary"
android:title="@string/metadata_cache_wipe_title"/>
</PreferenceScreen>

View file

@ -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);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 B

After

Width:  |  Height:  |  Size: 784 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 801 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="30"><link xmlns="" type="text/css" id="dark-mode" rel="stylesheet" href=""/><style xmlns="" type="text/css" id="dark-mode-custom-style"/><rect id="back" fill="#f6c915" x="1" y=".5" width="82" height="29" rx="4"/><svg viewBox="0 0 80 80" height="16" width="16" x="7" y="7"><g transform="translate(-78.37-208.06)" fill="#1a171b"><path d="m104.28 271.1c-3.571 0-6.373-.466-8.41-1.396-2.037-.93-3.495-2.199-4.375-3.809-.88-1.609-1.308-3.457-1.282-5.544.025-2.086.313-4.311.868-6.675l9.579-40.05 11.69-1.81-10.484 43.44c-.202.905-.314 1.735-.339 2.489-.026.754.113 1.421.415 1.999.302.579.817 1.044 1.546 1.395.729.353 1.747.579 3.055.679l-2.263 9.278"/><path d="m146.52 246.14c0 3.671-.604 7.03-1.811 10.07-1.207 3.043-2.879 5.669-5.01 7.881-2.138 2.213-4.702 3.935-7.693 5.167-2.992 1.231-6.248 1.848-9.767 1.848-1.71 0-3.42-.151-5.129-.453l-3.394 13.651h-11.162l12.52-52.19c2.01-.603 4.311-1.143 6.901-1.622 2.589-.477 5.393-.716 8.41-.716 2.815 0 5.242.428 7.278 1.282 2.037.855 3.708 2.024 5.02 3.507 1.307 1.484 2.274 3.219 2.904 5.205.627 1.987.942 4.11.942 6.373m-27.378 15.461c.854.202 1.91.302 3.167.302 1.961 0 3.746-.364 5.355-1.094 1.609-.728 2.979-1.747 4.111-3.055 1.131-1.307 2.01-2.877 2.64-4.714.628-1.835.943-3.858.943-6.071 0-2.161-.479-3.998-1.433-5.506-.956-1.508-2.615-2.263-4.978-2.263-1.61 0-3.118.151-4.525.453l-5.28 21.948"/></g></svg><text fill="#1a171b" text-anchor="middle" font-family="Helvetica Neue,Helvetica,Arial,sans-serif" font-weight="700" font-size="14" x="50" y="20">Donate</text></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Some files were not shown because too many files have changed in this diff Show more