Merge branch 'dev' of github.com:teamnewpipe/NewPipe into dev
27
README.md
|
@ -77,19 +77,24 @@ The more is done the better it gets!
|
|||
If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).
|
||||
|
||||
## Donate
|
||||
If you like NewPipe we'd be happy about a donation. You can either donate via Bitcoin or BountySource. For further information about donating to NewPipe, please visit our [website](https://newpipe.schabi.org/donate/).
|
||||
If you like NewPipe we'd be happy about a donation. You can either donate via Bitcoin, Bountysource or Liberapay. For further information about donating to NewPipe, please visit our [website](https://newpipe.schabi.org/donate).
|
||||
|
||||
<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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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) {}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
|
|
313
app/src/main/res/layout/dialog_playback_parameter.xml
Normal 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>
|
|
@ -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"/>
|
||||
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 & 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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">L’appli/l’interface a crashé</string>
|
||||
<string name="app_ui_crash">L’application 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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 & 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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 & Audio</string>
|
||||
<string name="settings_category_history_title">History</string>
|
||||
<string name="settings_category_history_title">History & 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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 346 B After Width: | Height: | Size: 784 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 801 B |
1
assets/liberapay_donate_button.svg
Normal 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 |
BIN
assets/liberapay_qr_code.png
Normal file
After Width: | Height: | Size: 807 B |
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 65 KiB |