diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 985223881..8073503ad 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,2 +1,3 @@ - [ ] I carefully read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them. - [ ] I checked if the issue/feature exists in the latest version. +- [ ] I did use the [incredible bugreport to markdown converter](https://teamnewpipe.github.io/CrashReportToMarkdown/) to paste bug reports. diff --git a/app/build.gradle b/app/build.gradle index a5ff67bee..f22492aef 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "org.schabi.newpipe" minSdkVersion 15 targetSdkVersion 27 - versionCode 64 - versionName "0.13.5" + versionCode 66 + versionName "0.13.7" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -42,10 +42,10 @@ android { ext { supportLibVersion = '27.1.1' - exoPlayerLibVersion = '2.7.3' - roomDbLibVersion = '1.0.0' + exoPlayerLibVersion = '2.8.2' + roomDbLibVersion = '1.1.1' leakCanaryLibVersion = '1.5.4' - okHttpLibVersion = '1.5.0' + okHttpLibVersion = '3.10.0' icepickLibVersion = '3.2.0' stethoLibVersion = '1.5.0' } @@ -54,10 +54,10 @@ dependencies { exclude module: 'support-annotations' } - implementation 'com.github.TeamNewPipe:NewPipeExtractor:bf1c771' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:1eff8c5708' testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:1.10.19' + testImplementation 'org.mockito:mockito-core:2.8.9' implementation "com.android.support:appcompat-v7:$supportLibVersion" implementation "com.android.support:support-v4:$supportLibVersion" @@ -79,7 +79,7 @@ dependencies { debugImplementation "com.facebook.stetho:stetho-urlconnection:$stethoLibVersion" debugImplementation 'com.android.support:multidex:1.0.3' - implementation 'io.reactivex.rxjava2:rxjava:2.1.10' + implementation 'io.reactivex.rxjava2:rxjava:2.1.14' implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1' @@ -93,6 +93,6 @@ dependencies { debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryLibVersion" releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion" - implementation 'com.squareup.okhttp3:okhttp:3.9.1' - debugImplementation "com.facebook.stetho:stetho-okhttp3:$okHttpLibVersion" + implementation "com.squareup.okhttp3:okhttp:$okHttpLibVersion" + debugImplementation "com.facebook.stetho:stetho-okhttp3:$stethoLibVersion" } diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 0ce6772bb..191332e19 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -61,6 +61,8 @@ import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.ThemeHelper; +import static org.schabi.newpipe.extractor.InfoItem.InfoType.PLAYLIST; + public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); @@ -106,7 +108,19 @@ public class MainActivity extends AppCompatActivity { drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true); - toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open, R.string.drawer_close); + toggle = new ActionBarDrawerToggle(this, drawer, toolbar, + R.string.drawer_open, R.string.drawer_close) { + @Override + public void onDrawerClosed(View view) { super.onDrawerClosed(view); } + + @Override + public void onDrawerOpened(View drawerView) { super.onDrawerOpened(drawerView); } + + @Override + public void onDrawerSlide(View drawerView, float slideOffset) { + super.onDrawerSlide(drawerView, 0); + } + }; toggle.syncState(); drawer.addDrawerListener(toggle); drawer.addDrawerListener(new DrawerLayout.SimpleDrawerListener() { @@ -133,13 +147,11 @@ public class MainActivity extends AppCompatActivity { private boolean changeService(MenuItem item) { - if (item.getGroupId() == R.id.menu_services_group) { - drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(false); - ServiceHelper.setSelectedServiceId(this, item.getItemId()); - drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true); - } else { + if (item.getGroupId() != R.id.menu_services_group) return false; - } + drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(false); + ServiceHelper.setSelectedServiceId(this, item.getItemId()); + drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true); drawer.closeDrawers(); return true; } @@ -382,31 +394,45 @@ public class MainActivity extends AppCompatActivity { } private void handleIntent(Intent intent) { - if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); + try { + if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); - if (intent.hasExtra(Constants.KEY_LINK_TYPE)) { - String url = intent.getStringExtra(Constants.KEY_URL); - int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0); - String title = intent.getStringExtra(Constants.KEY_TITLE); - switch (((StreamingService.LinkType) intent.getSerializableExtra(Constants.KEY_LINK_TYPE))) { - case STREAM: - boolean autoPlay = intent.getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false); - NavigationHelper.openVideoDetailFragment(getSupportFragmentManager(), serviceId, url, title, autoPlay); - break; - case CHANNEL: - NavigationHelper.openChannelFragment(getSupportFragmentManager(), serviceId, url, title); - break; - case PLAYLIST: - NavigationHelper.openPlaylistFragment(getSupportFragmentManager(), serviceId, url, title); - break; + if (intent.hasExtra(Constants.KEY_LINK_TYPE)) { + String url = intent.getStringExtra(Constants.KEY_URL); + int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0); + String title = intent.getStringExtra(Constants.KEY_TITLE); + switch (((StreamingService.LinkType) intent.getSerializableExtra(Constants.KEY_LINK_TYPE))) { + case STREAM: + boolean autoPlay = intent.getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false); + NavigationHelper.openVideoDetailFragment(getSupportFragmentManager(), serviceId, url, title, autoPlay); + break; + case CHANNEL: + NavigationHelper.openChannelFragment(getSupportFragmentManager(), + serviceId, + url, + title); + break; + case PLAYLIST: + NavigationHelper.openPlaylistFragment(getSupportFragmentManager(), + serviceId, + url, + title); + break; + } + } else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) { + String searchString = intent.getStringExtra(Constants.KEY_SEARCH_STRING); + if (searchString == null) searchString = ""; + int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0); + NavigationHelper.openSearchFragment( + getSupportFragmentManager(), + serviceId, + searchString); + + } else { + NavigationHelper.gotoMainFragment(getSupportFragmentManager()); } - } else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) { - String searchQuery = intent.getStringExtra(Constants.KEY_QUERY); - if (searchQuery == null) searchQuery = ""; - int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0); - NavigationHelper.openSearchFragment(getSupportFragmentManager(), serviceId, searchQuery); - } else { - NavigationHelper.gotoMainFragment(getSupportFragmentManager()); + } catch (Exception e) { + ErrorActivity.reportUiError(this, e); } } } diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index a862384cf..4f1fdeab2 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -1,14 +1,19 @@ package org.schabi.newpipe; +import android.annotation.SuppressLint; +import android.app.FragmentManager; import android.app.IntentService; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; import android.support.v4.app.NotificationCompat; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; @@ -23,6 +28,7 @@ import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.Toast; +import org.schabi.newpipe.download.DownloadDialog; import org.schabi.newpipe.extractor.Info; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; @@ -31,6 +37,8 @@ import org.schabi.newpipe.extractor.channel.ChannelInfo; 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.VideoStream; +import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue; @@ -38,16 +46,19 @@ import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ThemeHelper; +import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Observer; import icepick.Icepick; import icepick.State; @@ -77,6 +88,8 @@ public class RouterActivity extends AppCompatActivity { protected String currentUrl; protected CompositeDisposable disposables = new CompositeDisposable(); + private boolean selectionIsDownload = false; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -104,7 +117,7 @@ public class RouterActivity extends AppCompatActivity { @Override protected void onStart() { super.onStart(); - + handleUrl(currentUrl); } @@ -165,6 +178,7 @@ public class RouterActivity extends AppCompatActivity { final String videoPlayerKey = getString(R.string.video_player_key); final String backgroundPlayerKey = getString(R.string.background_player_key); final String popupPlayerKey = getString(R.string.popup_player_key); + final String downloadKey = getString(R.string.download_key); final String alwaysAskKey = getString(R.string.always_ask_open_action_key); if (selectedChoiceKey.equals(alwaysAskKey)) { @@ -179,6 +193,8 @@ public class RouterActivity extends AppCompatActivity { } } else if (selectedChoiceKey.equals(showInfoKey)) { handleChoice(showInfoKey); + } else if (selectedChoiceKey.equals(downloadKey)) { + handleChoice(downloadKey); } else { final boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); final boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); @@ -236,7 +252,9 @@ public class RouterActivity extends AppCompatActivity { .setCancelable(true) .setNegativeButton(R.string.just_once, dialogButtonsClickListener) .setPositiveButton(R.string.always, dialogButtonsClickListener) - .setOnDismissListener((dialog) -> finish()) + .setOnDismissListener((dialog) -> { + if(!selectionIsDownload) finish(); + }) .create(); //noinspection CodeBlock2Expr @@ -316,6 +334,9 @@ public class RouterActivity extends AppCompatActivity { resolveResourceIdFromAttr(context, R.attr.audio))); } + returnList.add(new AdapterChoiceItem(getString(R.string.download_key), getString(R.string.download), + resolveResourceIdFromAttr(context, R.attr.download))); + return returnList; } @@ -347,6 +368,14 @@ public class RouterActivity extends AppCompatActivity { return; } + if (selectedChoiceKey.equals(getString(R.string.download_key))) { + if (PermissionHelper.checkStoragePermissions(this, PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) { + selectionIsDownload = true; + openDownloadDialog(); + } + return; + } + // stop and bypass FetcherService if InfoScreen was selected since // StreamDetailFragment can fetch data itself if (selectedChoiceKey.equals(getString(R.string.show_info_key))) { @@ -373,6 +402,47 @@ public class RouterActivity extends AppCompatActivity { finish(); } + @SuppressLint("CheckResult") + private void openDownloadDialog() { + ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe((@NonNull StreamInfo result) -> { + List sortedVideoStreams = ListHelper.getSortedStreamVideosList(this, + result.getVideoStreams(), + result.getVideoOnlyStreams(), + false); + int selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(this, + sortedVideoStreams); + + android.support.v4.app.FragmentManager fm = getSupportFragmentManager(); + DownloadDialog downloadDialog = DownloadDialog.newInstance(result); + downloadDialog.setVideoStreams(sortedVideoStreams); + downloadDialog.setAudioStreams(result.getAudioStreams()); + downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex); + downloadDialog.show(fm, "downloadDialog"); + fm.executePendingTransactions(); + downloadDialog.getDialog().setOnDismissListener(dialog -> { + finish(); + }); + }, (@NonNull Throwable throwable) -> { + onError(); + }); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + for (int i: grantResults){ + if (i == PackageManager.PERMISSION_DENIED){ + finish(); + return; + } + } + if (requestCode == PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE) { + openDownloadDialog(); + } + } + private static class AdapterChoiceItem { final String description, key; @DrawableRes final int icon; diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java index 486350fc9..e2f2c8b08 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java @@ -71,6 +71,14 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem { info.getUploaderName(), info.getStreamCount()); } + @Ignore + public boolean isIdenticalTo(final PlaylistInfo info) { + return getServiceId() == info.getServiceId() && getName().equals(info.getName()) && + getStreamCount() == info.getStreamCount() && getUrl().equals(info.getUrl()) && + getThumbnailUrl().equals(info.getThumbnailUrl()) && + getUploader().equals(info.getUploaderName()); + } + public long getUid() { return uid; } diff --git a/app/src/main/java/org/schabi/newpipe/download/DeleteDownloadManager.java b/app/src/main/java/org/schabi/newpipe/download/DeleteDownloadManager.java new file mode 100644 index 000000000..f2912a6fa --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/download/DeleteDownloadManager.java @@ -0,0 +1,158 @@ +package org.schabi.newpipe.download; + +import android.app.Activity; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.design.widget.BaseTransientBottomBar; +import android.support.design.widget.Snackbar; +import android.view.View; + +import org.schabi.newpipe.R; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Completable; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.subjects.PublishSubject; +import us.shandian.giga.get.DownloadManager; +import us.shandian.giga.get.DownloadMission; + +public class DeleteDownloadManager { + + private static final String KEY_STATE = "delete_manager_state"; + + private View mView; + private HashSet mPendingMap; + private List mDisposableList; + private DownloadManager mDownloadManager; + private PublishSubject publishSubject = PublishSubject.create(); + + DeleteDownloadManager(Activity activity) { + mPendingMap = new HashSet<>(); + mDisposableList = new ArrayList<>(); + mView = activity.findViewById(android.R.id.content); + } + + public Observable getUndoObservable() { + return publishSubject; + } + + public boolean contains(@NonNull DownloadMission mission) { + return mPendingMap.contains(mission.url); + } + + public void add(@NonNull DownloadMission mission) { + mPendingMap.add(mission.url); + + if (mPendingMap.size() == 1) { + showUndoDeleteSnackbar(mission); + } + } + + public void setDownloadManager(@NonNull DownloadManager downloadManager) { + mDownloadManager = downloadManager; + + if (mPendingMap.size() < 1) return; + + showUndoDeleteSnackbar(); + } + + public void restoreState(@Nullable Bundle savedInstanceState) { + if (savedInstanceState == null) return; + + List list = savedInstanceState.getStringArrayList(KEY_STATE); + if (list != null) { + mPendingMap.addAll(list); + } + } + + public void saveState(@Nullable Bundle outState) { + if (outState == null) return; + + for (Disposable disposable : mDisposableList) { + disposable.dispose(); + } + + outState.putStringArrayList(KEY_STATE, new ArrayList<>(mPendingMap)); + } + + private void showUndoDeleteSnackbar() { + if (mPendingMap.size() < 1) return; + + String url = mPendingMap.iterator().next(); + + for (int i = 0; i < mDownloadManager.getCount(); i++) { + DownloadMission mission = mDownloadManager.getMission(i); + if (url.equals(mission.url)) { + showUndoDeleteSnackbar(mission); + break; + } + } + } + + private void showUndoDeleteSnackbar(@NonNull DownloadMission mission) { + final Snackbar snackbar = Snackbar.make(mView, mission.name, Snackbar.LENGTH_INDEFINITE); + final Disposable disposable = Observable.timer(3, TimeUnit.SECONDS) + .subscribeOn(AndroidSchedulers.mainThread()) + .subscribe(l -> snackbar.dismiss()); + + mDisposableList.add(disposable); + + snackbar.setAction(R.string.undo, v -> { + mPendingMap.remove(mission.url); + publishSubject.onNext(mission); + disposable.dispose(); + snackbar.dismiss(); + }); + + snackbar.addCallback(new BaseTransientBottomBar.BaseCallback() { + @Override + public void onDismissed(Snackbar transientBottomBar, int event) { + if (!disposable.isDisposed()) { + Completable.fromAction(() -> deletePending(mission)) + .subscribeOn(Schedulers.io()) + .subscribe(); + } + mPendingMap.remove(mission.url); + snackbar.removeCallback(this); + mDisposableList.remove(disposable); + showUndoDeleteSnackbar(); + } + }); + + snackbar.show(); + } + + public void deletePending() { + if (mPendingMap.size() < 1) return; + + HashSet idSet = new HashSet<>(); + for (int i = 0; i < mDownloadManager.getCount(); i++) { + if (contains(mDownloadManager.getMission(i))) { + idSet.add(i); + } + } + + for (Integer id : idSet) { + mDownloadManager.deleteMission(id); + } + + mPendingMap.clear(); + } + + private void deletePending(@NonNull DownloadMission mission) { + for (int i = 0; i < mDownloadManager.getCount(); i++) { + if (mission.url.equals(mDownloadManager.getMission(i).url)) { + mDownloadManager.deleteMission(i); + break; + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java index 6512f5270..4a2c85149 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java @@ -15,12 +15,17 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.settings.SettingsActivity; import org.schabi.newpipe.util.ThemeHelper; +import io.reactivex.Completable; +import io.reactivex.schedulers.Schedulers; import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.ui.fragment.AllMissionsFragment; import us.shandian.giga.ui.fragment.MissionsFragment; public class DownloadActivity extends AppCompatActivity { + private static final String MISSIONS_FRAGMENT_TAG = "fragment_tag"; + private DeleteDownloadManager mDeleteDownloadManager; + @Override protected void onCreate(Bundle savedInstanceState) { // Service @@ -42,21 +47,35 @@ public class DownloadActivity extends AppCompatActivity { actionBar.setDisplayShowTitleEnabled(true); } - // Fragment - getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - updateFragments(); - getWindow().getDecorView().getViewTreeObserver().removeGlobalOnLayoutListener(this); - } - }); + mDeleteDownloadManager = new DeleteDownloadManager(this); + mDeleteDownloadManager.restoreState(savedInstanceState); + + MissionsFragment fragment = (MissionsFragment) getFragmentManager().findFragmentByTag(MISSIONS_FRAGMENT_TAG); + if (fragment != null) { + fragment.setDeleteManager(mDeleteDownloadManager); + } else { + getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + updateFragments(); + getWindow().getDecorView().getViewTreeObserver().removeGlobalOnLayoutListener(this); + } + }); + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + mDeleteDownloadManager.saveState(outState); + super.onSaveInstanceState(outState); } private void updateFragments() { - MissionsFragment fragment = new AllMissionsFragment(); + fragment.setDeleteManager(mDeleteDownloadManager); + getFragmentManager().beginTransaction() - .replace(R.id.frame, fragment) + .replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG) .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .commit(); } @@ -80,6 +99,7 @@ public class DownloadActivity extends AppCompatActivity { case R.id.action_settings: { Intent intent = new Intent(this, SettingsActivity.class); startActivity(intent); + deletePending(); return true; } default: @@ -87,4 +107,15 @@ public class DownloadActivity extends AppCompatActivity { } } + @Override + public void onBackPressed() { + super.onBackPressed(); + deletePending(); + } + + private void deletePending() { + Completable.fromAction(mDeleteDownloadManager::deletePending) + .subscribeOn(Schedulers.io()) + .subscribe(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index 31092d3e6..0e3312403 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -10,6 +10,7 @@ import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.view.ViewPager; import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; import android.support.v7.preference.PreferenceManager; import android.util.Log; import android.view.LayoutInflater; @@ -127,7 +128,14 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_search: - NavigationHelper.openSearchFragment(getFragmentManager(), ServiceHelper.getSelectedServiceId(activity), ""); + try { + NavigationHelper.openSearchFragment( + getFragmentManager(), + ServiceHelper.getSelectedServiceId(activity), + ""); + } catch (Exception e) { + ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); + } return true; } return super.onOptionsItemSelected(item); @@ -226,7 +234,9 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte FALLBACK_CHANNEL_URL); String name = preferences.getString(getString(R.string.main_page_selected_channel_name), FALLBACK_CHANNEL_NAME); - ChannelFragment fragment = ChannelFragment.getInstance(serviceId, url, name); + ChannelFragment fragment = ChannelFragment.getInstance(serviceId, + url, + name); fragment.useAsFrontPage(true); return fragment; } else { @@ -255,20 +265,13 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte for (final String ks : kl.getAvailableKiosks()) { menu.add(0, KIOSK_MENU_OFFSET + i, Menu.NONE, KioskTranslator.getTranslatedKioskName(ks, getContext())) - .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem menuItem) { + .setOnMenuItemClickListener(menuItem -> { try { NavigationHelper.openKioskFragment(getFragmentManager(), currentServiceId, ks); } catch (Exception e) { - ErrorActivity.reportError(activity, e, - activity.getClass(), - null, - ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, - "none", "", R.string.app_ui_crash)); + ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); } return true; - } }); i++; } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 68c0a11ff..d91502cdd 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -17,6 +17,7 @@ import android.support.v4.content.ContextCompat; import android.support.v4.view.animation.FastOutSlowInInterpolator; import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; import android.text.Html; import android.text.Spanned; import android.text.TextUtils; @@ -54,14 +55,17 @@ import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ParsingException; -import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; +import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.stream.AudioStream; +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.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BaseStateFragment; +import org.schabi.newpipe.local.history.HistoryRecordManager; +import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.util.StreamItemAdapter; import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; @@ -128,7 +132,7 @@ public class VideoDetailFragment private StreamInfo currentInfo; private Disposable currentWorker; - private CompositeDisposable disposables = new CompositeDisposable(); + @NonNull private CompositeDisposable disposables = new CompositeDisposable(); private List sortedVideoStreams; private int selectedVideoStreamIndex = -1; @@ -363,11 +367,15 @@ public class VideoDetailFragment if (TextUtils.isEmpty(currentInfo.getUploaderUrl())) { Log.w(TAG, "Can't open channel because we got no channel URL"); } else { - NavigationHelper.openChannelFragment( - getFragmentManager(), - currentInfo.getServiceId(), - currentInfo.getUploaderUrl(), - currentInfo.getUploaderName()); + try { + NavigationHelper.openChannelFragment( + getFragmentManager(), + currentInfo.getServiceId(), + currentInfo.getUploaderUrl(), + currentInfo.getUploaderName()); + } catch (Exception e) { + ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); + } } break; case R.id.detail_thumbnail_root_layout: @@ -540,7 +548,8 @@ public class VideoDetailFragment final String[] commands = new String[]{ context.getResources().getString(R.string.enqueue_on_background), context.getResources().getString(R.string.enqueue_on_popup), - context.getResources().getString(R.string.append_playlist) + context.getResources().getString(R.string.append_playlist), + context.getResources().getString(R.string.share) }; final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> { @@ -557,6 +566,9 @@ public class VideoDetailFragment .show(getFragmentManager(), TAG); } break; + case 3: + shareUrl(item.getName(), item.getUrl()); + break; default: break; } @@ -872,10 +884,7 @@ public class VideoDetailFragment if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 16) { openNormalBackgroundPlayer(append); } else { - NavigationHelper.playOnExternalPlayer(activity, - currentInfo.getName(), - currentInfo.getUploaderName(), - audioStream); + startOnExternalPlayer(activity, currentInfo, audioStream); } } @@ -902,10 +911,7 @@ public class VideoDetailFragment if (PreferenceManager.getDefaultSharedPreferences(activity) .getBoolean(this.getString(R.string.use_external_video_player_key), false)) { - NavigationHelper.playOnExternalPlayer(activity, - currentInfo.getName(), - currentInfo.getUploaderName(), - selectedVideoStream); + startOnExternalPlayer(activity, currentInfo, selectedVideoStream); } else { openNormalPlayer(selectedVideoStream); } @@ -949,6 +955,20 @@ public class VideoDetailFragment this.autoPlayEnabled = autoplay; } + private void startOnExternalPlayer(@NonNull final Context context, + @NonNull final StreamInfo info, + @NonNull final Stream selectedStream) { + NavigationHelper.playOnExternalPlayer(context, currentInfo.getName(), + currentInfo.getUploaderName(), selectedStream); + + final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext()); + disposables.add(recordManager.onViewed(info).onErrorComplete() + .subscribe( + ignored -> {/* successful */}, + error -> Log.e(TAG, "Register view failure: ", error) + )); + } + @Nullable private VideoStream getSelectedVideoStream() { return sortedVideoStreams != null ? sortedVideoStreams.get(selectedVideoStreamIndex) : null; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 14ec50775..1db12bba9 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -6,6 +6,7 @@ import android.content.DialogInterface; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; @@ -24,6 +25,7 @@ import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoListAdapter; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; +import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.StateSaver; @@ -152,18 +154,35 @@ public abstract class BaseListFragment extends BaseStateFragment implem infoListAdapter.setOnChannelSelectedListener(new OnClickGesture() { @Override public void selected(ChannelInfoItem selectedItem) { - onItemSelected(selectedItem); - NavigationHelper.openChannelFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(), - selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); + try { + onItemSelected(selectedItem); + NavigationHelper.openChannelFragment(useAsFrontPage ? + getParentFragment().getFragmentManager() + : getFragmentManager(), + selectedItem.getServiceId(), + selectedItem.getUrl(), + selectedItem.getName()); + } catch (Exception e) { + ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); + } } }); infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture() { @Override public void selected(PlaylistInfoItem selectedItem) { - onItemSelected(selectedItem); - NavigationHelper.openPlaylistFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(), - selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); + try { + onItemSelected(selectedItem); + NavigationHelper.openPlaylistFragment( + useAsFrontPage + ? getParentFragment().getFragmentManager() + : getFragmentManager(), + selectedItem.getServiceId(), + selectedItem.getUrl(), + selectedItem.getName()); + } catch (Exception e) { + ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); + } } }); @@ -178,7 +197,9 @@ public abstract class BaseListFragment extends BaseStateFragment implem private void onStreamSelected(StreamInfoItem selectedItem) { onItemSelected(selectedItem); - NavigationHelper.openVideoDetailFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(), + NavigationHelper.openVideoDetailFragment(useAsFrontPage + ? getParentFragment().getFragmentManager() + : getFragmentManager(), selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); } @@ -196,7 +217,8 @@ public abstract class BaseListFragment extends BaseStateFragment implem final String[] commands = new String[]{ context.getResources().getString(R.string.enqueue_on_background), context.getResources().getString(R.string.enqueue_on_popup), - context.getResources().getString(R.string.append_playlist) + context.getResources().getString(R.string.append_playlist), + context.getResources().getString(R.string.share) }; final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { @@ -213,6 +235,9 @@ public abstract class BaseListFragment extends BaseStateFragment implem .show(getFragmentManager(), TAG); } break; + case 3: + shareUrl(item.getName(), item.getUrl()); + break; default: break; } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java index a132213bf..e702c602f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java @@ -8,6 +8,9 @@ import android.view.View; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListInfo; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; +import org.schabi.newpipe.extractor.linkhandler.LinkHandler; import org.schabi.newpipe.util.Constants; import java.util.Queue; @@ -166,7 +169,6 @@ public abstract class BaseListInfoFragment public void handleResult(@NonNull I result) { super.handleResult(result); - url = result.getUrl(); name = result.getName(); setTitle(name); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index dc8d764f3..42ba8e0ac 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -33,6 +33,7 @@ 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.StreamInfoItem; +import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; @@ -161,7 +162,8 @@ public class ChannelFragment extends BaseListInfoFragment { context.getResources().getString(R.string.start_here_on_main), context.getResources().getString(R.string.start_here_on_background), context.getResources().getString(R.string.start_here_on_popup), - context.getResources().getString(R.string.append_playlist) + context.getResources().getString(R.string.append_playlist), + context.getResources().getString(R.string.share) }; final DialogInterface.OnClickListener actions = new DialogInterface.OnClickListener() { @@ -190,6 +192,9 @@ public class ChannelFragment extends BaseListInfoFragment { .show(getFragmentManager(), TAG); } break; + case 6: + shareUrl(item.getName(), item.getUrl()); + break; default: break; } @@ -497,7 +502,11 @@ public class ChannelFragment extends BaseListInfoFragment { if (super.onError(exception)) return true; int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; - onUnrecoverableError(exception, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(serviceId), url, errorId); + onUnrecoverableError(exception, + UserAction.REQUESTED_CHANNEL, + NewPipe.getNameOfService(serviceId), + url, + errorId); return true; } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java index 5dfdcd655..114e92e43 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java @@ -11,22 +11,20 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.UrlIdHandler; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.kiosk.KioskInfo; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory; +import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; +import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; -import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.KioskTranslator; -import org.schabi.newpipe.util.NavigationHelper; import icepick.State; import io.reactivex.Single; @@ -74,10 +72,10 @@ public class KioskFragment extends BaseListInfoFragment { throws ExtractionException { KioskFragment instance = new KioskFragment(); StreamingService service = NewPipe.getService(serviceId); - UrlIdHandler kioskTypeUrlIdHandler = service.getKioskList() - .getUrlIdHandlerByType(kioskId); + ListLinkHandlerFactory kioskLinkHandlerFactory = service.getKioskList() + .getListLinkHandlerFactoryByType(kioskId); instance.setInitialData(serviceId, - kioskTypeUrlIdHandler.getUrl(kioskId), kioskId); + kioskLinkHandlerFactory.fromId(kioskId).getUrl(), kioskId); instance.kioskId = kioskId; return instance; } @@ -136,7 +134,10 @@ public class KioskFragment extends BaseListInfoFragment { .getDefaultSharedPreferences(activity) .getString(getString(R.string.content_country_key), getString(R.string.default_country_value)); - return ExtractorHelper.getKioskInfo(serviceId, url, contentCountry, forceReload); + return ExtractorHelper.getKioskInfo(serviceId, + url, + contentCountry, + forceReload); } @Override @@ -145,7 +146,10 @@ public class KioskFragment extends BaseListInfoFragment { .getDefaultSharedPreferences(activity) .getString(getString(R.string.content_country_key), getString(R.string.default_country_value)); - return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextPageUrl, contentCountry); + return ExtractorHelper.getMoreKioskItems(serviceId, + url, + currentNextPageUrl, + contentCountry); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 0498c95c5..b7a42791c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -6,6 +6,7 @@ import android.content.DialogInterface; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v7.app.AppCompatActivity; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; @@ -19,6 +20,7 @@ import android.widget.TextView; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import org.schabi.newpipe.App; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; @@ -28,12 +30,14 @@ 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.StreamInfoItem; +import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; -import org.schabi.newpipe.local.playlist.RemotePlaylistManager; import org.schabi.newpipe.info_list.InfoItemDialog; +import org.schabi.newpipe.local.playlist.RemotePlaylistManager; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; +import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ImageDisplayConstants; @@ -44,6 +48,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import io.reactivex.Flowable; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; @@ -93,7 +98,8 @@ public class PlaylistFragment extends BaseListInfoFragment { super.onCreate(savedInstanceState); disposables = new CompositeDisposable(); isBookmarkButtonReady = new AtomicBoolean(false); - remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase.getInstance(getContext())); + remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase.getInstance( + requireContext())); } @Override @@ -142,6 +148,7 @@ public class PlaylistFragment extends BaseListInfoFragment { context.getResources().getString(R.string.start_here_on_main), context.getResources().getString(R.string.start_here_on_background), context.getResources().getString(R.string.start_here_on_popup), + context.getResources().getString(R.string.share) }; final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { @@ -162,6 +169,9 @@ public class PlaylistFragment extends BaseListInfoFragment { case 4: NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); break; + case 5: + shareUrl(item.getName(), item.getUrl()); + break; default: break; } @@ -261,11 +271,16 @@ public class PlaylistFragment extends BaseListInfoFragment { if (!TextUtils.isEmpty(result.getUploaderName())) { headerUploaderName.setText(result.getUploaderName()); if (!TextUtils.isEmpty(result.getUploaderUrl())) { - headerUploaderLayout.setOnClickListener(v -> + headerUploaderLayout.setOnClickListener(v -> { + try { NavigationHelper.openChannelFragment(getFragmentManager(), - result.getServiceId(), result.getUploaderUrl(), - result.getUploaderName()) - ); + result.getServiceId(), + result.getUploaderUrl(), + result.getUploaderName()); + } catch (Exception e) { + ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); + } + }); } } @@ -281,14 +296,11 @@ public class PlaylistFragment extends BaseListInfoFragment { } remotePlaylistManager.getPlaylist(result) + .flatMap(lists -> getUpdateProcessor(lists, result), (lists, id) -> lists) .onBackpressureLatest() .observeOn(AndroidSchedulers.mainThread()) .subscribe(getPlaylistBookmarkSubscriber()); - remotePlaylistManager.onUpdate(result) - .subscribeOn(AndroidSchedulers.mainThread()) - .subscribe(integer -> {/* Do nothing*/}, this::onError); - headerPlayAllButton.setOnClickListener(view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); headerPopupButton.setOnClickListener(view -> @@ -336,7 +348,11 @@ public class PlaylistFragment extends BaseListInfoFragment { if (super.onError(exception)) return true; int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; - onUnrecoverableError(exception, UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId), url, errorId); + onUnrecoverableError(exception, + UserAction.REQUESTED_PLAYLIST, + NewPipe.getNameOfService(serviceId), + url, + errorId); return true; } @@ -344,6 +360,17 @@ public class PlaylistFragment extends BaseListInfoFragment { // Utils //////////////////////////////////////////////////////////////////////////*/ + private Flowable getUpdateProcessor(@NonNull List playlists, + @NonNull PlaylistInfo result) { + final Flowable noItemToUpdate = Flowable.just(/*noItemToUpdate=*/-1); + if (playlists.isEmpty()) return noItemToUpdate; + + final PlaylistRemoteEntity playlistEntity = playlists.get(0); + if (playlistEntity.isIdenticalTo(result)) return noItemToUpdate; + + return remotePlaylistManager.onUpdate(playlists.get(0).getUid(), result).toFlowable(); + } + private Subscriber> getPlaylistBookmarkSubscriber() { return new Subscriber>() { @Override @@ -416,4 +443,4 @@ public class PlaylistFragment extends BaseListInfoFragment { playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr)); playlistBookmarkButton.setTitle(titleRes); } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index d07ff6448..411379963 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -37,26 +37,30 @@ import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ParsingException; -import org.schabi.newpipe.extractor.search.SearchEngine; -import org.schabi.newpipe.extractor.search.SearchResult; +import org.schabi.newpipe.extractor.search.SearchExtractor; +import org.schabi.newpipe.extractor.search.SearchInfo; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.list.BaseListFragment; import org.schabi.newpipe.local.history.HistoryRecordManager; +import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.LayoutManagerSmoothScroller; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.ServiceHelper; import java.io.IOException; import java.io.InterruptedIOException; import java.net.SocketException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Queue; -import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import icepick.State; @@ -65,14 +69,15 @@ import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; +import static java.util.Arrays.asList; + import static org.schabi.newpipe.util.AnimationUtils.animateView; public class SearchFragment - extends BaseListFragment + extends BaseListFragment implements BackPressable { /*////////////////////////////////////////////////////////////////////////// @@ -92,19 +97,29 @@ public class SearchFragment @State protected int filterItemCheckedId = -1; - private SearchEngine.Filter filter = SearchEngine.Filter.ANY; @State protected int serviceId = Constants.NO_SERVICE_ID; + + // this three represet the current search query @State - protected String searchQuery; + protected String searchString; @State - protected String lastSearchedQuery; + protected String[] contentFilter; + @State + protected String sortFilter; + + // these represtent the last search + @State + protected String lastSearchedString; + @State protected boolean wasSearchFocused = false; - private int currentPage = 0; - private int currentNextPage = 0; + private Map menuItemToFilterName; + private StreamingService service; + private String currentPageUrl; + private String nextPageUrl; private String contentCountry; private boolean isSuggestionsEnabled = true; private boolean isSearchHistoryEnabled = true; @@ -130,11 +145,11 @@ public class SearchFragment /*////////////////////////////////////////////////////////////////////////*/ - public static SearchFragment getInstance(int serviceId, String query) { + public static SearchFragment getInstance(int serviceId, String searchString) { SearchFragment searchFragment = new SearchFragment(); - searchFragment.setQuery(serviceId, query); + searchFragment.setQuery(serviceId, searchString, new String[0], ""); - if (!TextUtils.isEmpty(query)) { + if (!TextUtils.isEmpty(searchString)) { searchFragment.setSearchOnResume(); } @@ -202,13 +217,22 @@ public class SearchFragment if (DEBUG) Log.d(TAG, "onResume() called"); super.onResume(); - if (!TextUtils.isEmpty(searchQuery)) { + try { + service = NewPipe.getService(serviceId); + } catch (Exception e) { + ErrorActivity.reportError(getActivity(), e, getActivity().getClass(), + getActivity().findViewById(android.R.id.content), + ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, + "", + "", R.string.general_error)); + } + + if (!TextUtils.isEmpty(searchString)) { if (wasLoading.getAndSet(false)) { - if (currentNextPage > currentPage) loadMoreItems(); - else search(searchQuery); + search(searchString, contentFilter, sortFilter); } else if (infoListAdapter.getItemsList().size() == 0) { if (savedState == null) { - search(searchQuery); + search(searchString, contentFilter, sortFilter); } else if (!isLoading.get() && !wasSearchFocused) { infoListAdapter.clearStreamItemList(); showEmptyState(); @@ -218,7 +242,7 @@ public class SearchFragment if (suggestionDisposable == null || suggestionDisposable.isDisposed()) initSuggestionObserver(); - if (TextUtils.isEmpty(searchQuery) || wasSearchFocused) { + if (TextUtils.isEmpty(searchString) || wasSearchFocused) { showKeyboardSearch(); showSuggestionsPanel(); } else { @@ -247,8 +271,9 @@ public class SearchFragment public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case ReCaptchaActivity.RECAPTCHA_REQUEST: - if (resultCode == Activity.RESULT_OK && !TextUtils.isEmpty(searchQuery)) { - search(searchQuery); + if (resultCode == Activity.RESULT_OK + && !TextUtils.isEmpty(searchString)) { + search(searchString, contentFilter, sortFilter); } else Log.e(TAG, "ReCaptcha failed"); break; @@ -282,20 +307,22 @@ public class SearchFragment @Override public void writeTo(Queue objectsToSave) { super.writeTo(objectsToSave); - objectsToSave.add(currentPage); - objectsToSave.add(currentNextPage); + objectsToSave.add(currentPageUrl); + objectsToSave.add(nextPageUrl); } @Override public void readFrom(@NonNull Queue savedObjects) throws Exception { super.readFrom(savedObjects); - currentPage = (int) savedObjects.poll(); - currentNextPage = (int) savedObjects.poll(); + currentPageUrl = (String) savedObjects.poll(); + nextPageUrl = (String) savedObjects.poll(); } @Override public void onSaveInstanceState(Bundle bundle) { - searchQuery = searchEditText != null ? searchEditText.getText().toString() : searchQuery; + searchString = searchEditText != null + ? searchEditText.getText().toString() + : searchString; super.onSaveInstanceState(bundle); } @@ -305,8 +332,11 @@ public class SearchFragment @Override public void reloadContent() { - if (!TextUtils.isEmpty(searchQuery) || (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) { - search(!TextUtils.isEmpty(searchQuery) ? searchQuery : searchEditText.getText().toString()); + if (!TextUtils.isEmpty(searchString) + || (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) { + search(!TextUtils.isEmpty(searchString) + ? searchString + : searchEditText.getText().toString(), new String[0], ""); } else { if (searchEditText != null) { searchEditText.setText(""); @@ -330,22 +360,35 @@ public class SearchFragment supportActionBar.setDisplayHomeAsUpEnabled(true); } - inflater.inflate(R.menu.menu_search, menu); + menuItemToFilterName = new HashMap<>(); + + int itemId = 0; + boolean isFirstItem = true; + final Context c = getContext(); + for(String filter : service.getSearchQIHFactory().getAvailableContentFilter()) { + menuItemToFilterName.put(itemId, filter); + MenuItem item = menu.add(1, + itemId++, + 0, + ServiceHelper.getTranslatedFilterString(filter, c)); + if(isFirstItem) { + item.setChecked(true); + isFirstItem = false; + } + } + menu.setGroupCheckable(1, true, true); + restoreFilterChecked(menu, filterItemCheckedId); } @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_filter_all: - case R.id.menu_filter_video: - case R.id.menu_filter_channel: - case R.id.menu_filter_playlist: - changeFilter(item, getFilterFromMenuId(item.getItemId())); - return true; - default: - return super.onOptionsItemSelected(item); - } + + List contentFilter = new ArrayList<>(1); + contentFilter.add(menuItemToFilterName.get(item.getItemId())); + changeContentFilter(item, contentFilter); + + return true; } private void restoreFilterChecked(Menu menu, int itemId) { @@ -354,21 +397,6 @@ public class SearchFragment if (item == null) return; item.setChecked(true); - filter = getFilterFromMenuId(itemId); - } - } - - private SearchEngine.Filter getFilterFromMenuId(int itemId) { - switch (itemId) { - case R.id.menu_filter_video: - return SearchEngine.Filter.STREAM; - case R.id.menu_filter_channel: - return SearchEngine.Filter.CHANNEL; - case R.id.menu_filter_playlist: - return SearchEngine.Filter.PLAYLIST; - case R.id.menu_filter_all: - default: - return SearchEngine.Filter.ANY; } } @@ -379,14 +407,21 @@ public class SearchFragment private TextWatcher textWatcher; private void showSearchOnStart() { - if (DEBUG) Log.d(TAG, "showSearchOnStart() called, searchQuery → " + searchQuery+", lastSearchedQuery → " + lastSearchedQuery); - searchEditText.setText(searchQuery); + if (DEBUG) Log.d(TAG, "showSearchOnStart() called, searchQuery → " + + searchString + + ", lastSearchedQuery → " + + lastSearchedString); + searchEditText.setText(searchString); - if (TextUtils.isEmpty(searchQuery) || TextUtils.isEmpty(searchEditText.getText())) { + if (TextUtils.isEmpty(searchString) || TextUtils.isEmpty(searchEditText.getText())) { searchToolbarContainer.setTranslationX(100); searchToolbarContainer.setAlpha(0f); searchToolbarContainer.setVisibility(View.VISIBLE); - searchToolbarContainer.animate().translationX(0).alpha(1f).setDuration(200).setInterpolator(new DecelerateInterpolator()).start(); + searchToolbarContainer.animate() + .translationX(0) + .alpha(1f) + .setDuration(200) + .setInterpolator(new DecelerateInterpolator()).start(); } else { searchToolbarContainer.setTranslationX(0); searchToolbarContainer.setAlpha(1f); @@ -396,47 +431,38 @@ public class SearchFragment private void initSearchListeners() { if (DEBUG) Log.d(TAG, "initSearchListeners() called"); - searchClear.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]"); - if (TextUtils.isEmpty(searchEditText.getText())) { - NavigationHelper.gotoMainFragment(getFragmentManager()); - return; - } - - searchEditText.setText(""); - suggestionListAdapter.setItems(new ArrayList()); - showKeyboardSearch(); + searchClear.setOnClickListener(v -> { + if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]"); + if (TextUtils.isEmpty(searchEditText.getText())) { + NavigationHelper.gotoMainFragment(getFragmentManager()); + return; } + + searchEditText.setText(""); + suggestionListAdapter.setItems(new ArrayList<>()); + showKeyboardSearch(); }); TooltipCompat.setTooltipText(searchClear, getString(R.string.clear)); - searchEditText.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]"); - if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) { - showSuggestionsPanel(); - } + searchEditText.setOnClickListener(v -> { + if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]"); + if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) { + showSuggestionsPanel(); } }); - searchEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - if (DEBUG) Log.d(TAG, "onFocusChange() called with: v = [" + v + "], hasFocus = [" + hasFocus + "]"); - if (isSuggestionsEnabled && hasFocus && errorPanelRoot.getVisibility() != View.VISIBLE) { - showSuggestionsPanel(); - } + searchEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> { + if (DEBUG) Log.d(TAG, "onFocusChange() called with: v = [" + v + "], hasFocus = [" + hasFocus + "]"); + if (isSuggestionsEnabled && hasFocus && errorPanelRoot.getVisibility() != View.VISIBLE) { + showSuggestionsPanel(); } }); suggestionListAdapter.setListener(new SuggestionListAdapter.OnSuggestionItemSelected() { @Override public void onSuggestionItemSelected(SuggestionItem item) { - search(item.query); + search(item.query, new String[0], ""); searchEditText.setText(item.query); } @@ -469,21 +495,22 @@ public class SearchFragment } }; searchEditText.addTextChangedListener(textWatcher); - searchEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() { - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if (DEBUG) { - Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]"); - } - if (event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) { - search(searchEditText.getText().toString()); - return true; - } - return false; - } - }); + searchEditText.setOnEditorActionListener( + (TextView v, int actionId, KeyEvent event) -> { + if (DEBUG) { + Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]"); + } + if (event != null + && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER + || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) { + search(searchEditText.getText().toString(), new String[0], ""); + return true; + } + return false; + }); - if (suggestionDisposable == null || suggestionDisposable.isDisposed()) initSuggestionObserver(); + if (suggestionDisposable == null || suggestionDisposable.isDisposed()) + initSuggestionObserver(); } private void unsetSearchListeners() { @@ -513,7 +540,8 @@ public class SearchFragment if (searchEditText == null) return; if (searchEditText.requestFocus()) { - InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); + InputMethodManager imm = (InputMethodManager) activity.getSystemService( + Context.INPUT_METHOD_SERVICE); imm.showSoftInput(searchEditText, InputMethodManager.SHOW_IMPLICIT); } } @@ -522,8 +550,10 @@ public class SearchFragment if (DEBUG) Log.d(TAG, "hideKeyboardSearch() called"); if (searchEditText == null) return; - InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); + InputMethodManager imm = (InputMethodManager) activity.getSystemService( + Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), + InputMethodManager.HIDE_NOT_ALWAYS); searchEditText.clearFocus(); } @@ -554,10 +584,12 @@ public class SearchFragment @Override public boolean onBackPressed() { - if (suggestionsPanel.getVisibility() == View.VISIBLE && infoListAdapter.getItemsList().size() > 0 && !isLoading.get()) { + if (suggestionsPanel.getVisibility() == View.VISIBLE + && infoListAdapter.getItemsList().size() > 0 + && !isLoading.get()) { hideSuggestionsPanel(); hideKeyboardSearch(); - searchEditText.setText(lastSearchedQuery); + searchEditText.setText(lastSearchedString); return true; } return false; @@ -573,8 +605,10 @@ public class SearchFragment final Observable observable = suggestionPublisher .debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS) - .startWith(searchQuery != null ? searchQuery : "") - .filter(query -> isSuggestionsEnabled); + .startWith(searchString != null + ? searchString + : "") + .filter(searchString -> isSuggestionsEnabled); suggestionDisposable = observable .switchMap(query -> { @@ -645,56 +679,44 @@ public class SearchFragment // no-op } - private void search(final String query) { - if (DEBUG) Log.d(TAG, "search() called with: query = [" + query + "]"); - if (query.isEmpty()) return; + private void search(final String searchString, String[] contentFilter, String sortFilter) { + if (DEBUG) Log.d(TAG, "search() called with: query = [" + searchString + "]"); + if (searchString.isEmpty()) return; try { - final StreamingService service = NewPipe.getServiceByUrl(query); + final StreamingService service = NewPipe.getServiceByUrl(searchString); if (service != null) { showLoading(); disposables.add(Observable - .fromCallable(new Callable() { - @Override - public Intent call() throws Exception { - return NavigationHelper.getIntentByLink(activity, service, query); - } - }) + .fromCallable(() -> + NavigationHelper.getIntentByLink(activity, service, searchString)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Consumer() { - @Override - public void accept(Intent intent) throws Exception { - getFragmentManager().popBackStackImmediate(); - activity.startActivity(intent); - } - }, new Consumer() { - @Override - public void accept(Throwable throwable) throws Exception { - showError(getString(R.string.url_not_supported_toast), false); - } - })); + .subscribe(intent -> { + getFragmentManager().popBackStackImmediate(); + activity.startActivity(intent); + }, throwable -> + showError(getString(R.string.url_not_supported_toast), false))); return; } } catch (Exception e) { // Exception occurred, it's not a url } - lastSearchedQuery = query; - searchQuery = query; - currentPage = 0; + lastSearchedString = this.searchString; + this.searchString = searchString; infoListAdapter.clearStreamItemList(); hideSuggestionsPanel(); hideKeyboardSearch(); - historyRecordManager.onSearched(serviceId, query) + historyRecordManager.onSearched(serviceId, searchString) .observeOn(AndroidSchedulers.mainThread()) .subscribe( ignored -> {}, error -> showSnackBarError(error, UserAction.SEARCHED, - NewPipe.getNameOfService(serviceId), query, 0) + NewPipe.getNameOfService(serviceId), searchString, 0) ); - suggestionPublisher.onNext(query); + suggestionPublisher.onNext(searchString); startLoading(false); } @@ -703,11 +725,16 @@ public class SearchFragment super.startLoading(forceLoad); if (disposables != null) disposables.clear(); if (searchDisposable != null) searchDisposable.dispose(); - searchDisposable = ExtractorHelper.searchFor(serviceId, searchQuery, currentPage, contentCountry, filter) + searchDisposable = ExtractorHelper.searchFor(serviceId, + searchString, + Arrays.asList(contentFilter), + sortFilter, + contentCountry) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnEvent((searchResult, throwable) -> isLoading.set(false)) .subscribe(this::handleResult, this::onError); + } @Override @@ -715,8 +742,13 @@ public class SearchFragment isLoading.set(true); showListFooter(true); if (searchDisposable != null) searchDisposable.dispose(); - currentNextPage = currentPage + 1; - searchDisposable = ExtractorHelper.getMoreSearchItems(serviceId, searchQuery, currentNextPage, contentCountry, filter) + searchDisposable = ExtractorHelper.getMoreSearchItems( + serviceId, + searchString, + asList(contentFilter), + sortFilter, + nextPageUrl, + contentCountry) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnEvent((nextItemsResult, throwable) -> isLoading.set(false)) @@ -739,19 +771,22 @@ public class SearchFragment // Utils //////////////////////////////////////////////////////////////////////////*/ - private void changeFilter(MenuItem item, SearchEngine.Filter filter) { - this.filter = filter; + private void changeContentFilter(MenuItem item, List contentFilter) { this.filterItemCheckedId = item.getItemId(); item.setChecked(true); - if (!TextUtils.isEmpty(searchQuery)) { - search(searchQuery); + this.contentFilter = new String[] {contentFilter.get(0)}; + + if (!TextUtils.isEmpty(searchString)) { + search(searchString, this.contentFilter, sortFilter); } } - private void setQuery(int serviceId, String searchQuery) { + private void setQuery(int serviceId, String searchString, String[] contentfilter, String sortFilter) { this.serviceId = serviceId; - this.searchQuery = searchQuery; + this.searchString = searchString; + this.contentFilter = contentfilter; + this.sortFilter = sortFilter; } /*////////////////////////////////////////////////////////////////////////// @@ -772,8 +807,11 @@ public class SearchFragment if (DEBUG) Log.d(TAG, "onSuggestionError() called with: exception = [" + exception + "]"); if (super.onError(exception)) return; - int errorId = exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error; - onUnrecoverableError(exception, UserAction.GET_SUGGESTIONS, NewPipe.getNameOfService(serviceId), searchQuery, errorId); + int errorId = exception instanceof ParsingException + ? R.string.parsing_error + : R.string.general_error; + onUnrecoverableError(exception, UserAction.GET_SUGGESTIONS, + NewPipe.getNameOfService(serviceId), searchString, errorId); } /*////////////////////////////////////////////////////////////////////////// @@ -798,16 +836,19 @@ public class SearchFragment //////////////////////////////////////////////////////////////////////////*/ @Override - public void handleResult(@NonNull SearchResult result) { - if (!result.errors.isEmpty()) { - showSnackBarError(result.errors, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchQuery, 0); + public void handleResult(@NonNull SearchInfo result) { + if (!result.getErrors().isEmpty()) { + showSnackBarError(result.getErrors(), UserAction.SEARCHED, + NewPipe.getNameOfService(serviceId), searchString, 0); } - lastSearchedQuery = searchQuery; + lastSearchedString = searchString; + nextPageUrl = result.getNextPageUrl(); + currentPageUrl = result.getUrl(); if (infoListAdapter.getItemsList().size() == 0) { - if (!result.getResults().isEmpty()) { - infoListAdapter.addInfoItemList(result.getResults()); + if (!result.getRelatedItems().isEmpty()) { + infoListAdapter.addInfoItemList(result.getRelatedItems()); } else { infoListAdapter.clearStreamItemList(); showEmptyState(); @@ -821,12 +862,13 @@ public class SearchFragment @Override public void handleNextItems(ListExtractor.InfoItemsPage result) { showListFooter(false); - currentPage = Integer.parseInt(result.getNextPageUrl()); + currentPageUrl = result.getNextPageUrl(); infoListAdapter.addInfoItemList(result.getItems()); if (!result.getErrors().isEmpty()) { - showSnackBarError(result.getErrors(), UserAction.SEARCHED, NewPipe.getNameOfService(serviceId) - , "\"" + searchQuery + "\" → page " + currentPage, 0); + showSnackBarError(result.getErrors(), UserAction.SEARCHED, + NewPipe.getNameOfService(serviceId) + , "\"" + searchString + "\" → page: " + nextPageUrl, 0); } super.handleNextItems(result); } @@ -835,12 +877,15 @@ public class SearchFragment protected boolean onError(Throwable exception) { if (super.onError(exception)) return true; - if (exception instanceof SearchEngine.NothingFoundException) { + if (exception instanceof SearchExtractor.NothingFoundException) { infoListAdapter.clearStreamItemList(); showEmptyState(); } else { - int errorId = exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error; - onUnrecoverableError(exception, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchQuery, errorId); + int errorId = exception instanceof ParsingException + ? R.string.parsing_error + : R.string.general_error; + onUnrecoverableError(exception, UserAction.SEARCHED, + NewPipe.getNameOfService(serviceId), searchString, errorId); } return true; diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index f3f390c4d..02eabd9ef 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -6,6 +6,7 @@ import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.FragmentManager; +import android.support.v7.app.AppCompatActivity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -19,9 +20,11 @@ import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistLocalItem; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; +import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.local.BaseLocalListFragment; import org.schabi.newpipe.local.playlist.LocalPlaylistManager; import org.schabi.newpipe.local.playlist.RemotePlaylistManager; +import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; @@ -99,19 +102,26 @@ public final class BookmarkFragment itemListAdapter.setSelectedListener(new OnClickGesture() { @Override public void selected(LocalItem selectedItem) { - // Requires the parent fragment to find holder for fragment replacement - if (getParentFragment() == null) return; - final FragmentManager fragmentManager = getParentFragment().getFragmentManager(); + try { + // Requires the parent fragment to find holder for fragment replacement + if (getParentFragment() == null) return; + final FragmentManager fragmentManager = getParentFragment().getFragmentManager(); - if (selectedItem instanceof PlaylistMetadataEntry) { - final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); - NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.uid, - entry.name); + if (selectedItem instanceof PlaylistMetadataEntry) { + final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); + NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.uid, + entry.name); - } else if (selectedItem instanceof PlaylistRemoteEntity) { - final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); - NavigationHelper.openPlaylistFragment(fragmentManager, entry.getServiceId(), - entry.getUrl(), entry.getName()); + } else if (selectedItem instanceof PlaylistRemoteEntity) { + final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); + NavigationHelper.openPlaylistFragment( + fragmentManager, + entry.getServiceId(), + entry.getUrl(), + entry.getName()); + } + } catch (Exception e) { + ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); } } diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java index eac1873a4..c2c813a4d 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java @@ -298,6 +298,7 @@ public class StatisticsPlaylistFragment context.getResources().getString(R.string.start_here_on_background), context.getResources().getString(R.string.start_here_on_popup), context.getResources().getString(R.string.delete), + context.getResources().getString(R.string.share) }; final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { @@ -321,6 +322,9 @@ public class StatisticsPlaylistFragment case 5: deleteEntry(index); break; + case 6: + shareUrl(item.toStreamInfoItem().getName(), item.toStreamInfoItem().getUrl()); + break; default: break; } diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index e51fa50a4..35a1530c9 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -520,7 +520,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment { @@ -549,6 +550,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment onUpdate(final PlaylistInfo playlistInfo) { - return Single.fromCallable(() -> playlistRemoteTable.update(new PlaylistRemoteEntity(playlistInfo))) - .subscribeOn(Schedulers.io()); + public Single onUpdate(final long playlistId, final PlaylistInfo playlistInfo) { + return Single.fromCallable(() -> { + PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo); + playlist.setUid(playlistId); + return playlistRemoteTable.update(playlist); + }).subscribeOn(Schedulers.io()); } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java index 5f6ea42ee..c194069fb 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java @@ -15,6 +15,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.LocalBroadcastManager; import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; @@ -38,6 +39,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService; import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService; @@ -318,9 +320,15 @@ public class SubscriptionFragment extends BaseStateFragment() { @Override public void selected(ChannelInfoItem selectedItem) { - // Requires the parent fragment to find holder for fragment replacement - NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(), - selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); + try { + // Requires the parent fragment to find holder for fragment replacement + NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(), + selectedItem.getServiceId(), + selectedItem.getUrl(), + selectedItem.getName()); + } catch (Exception e) { + ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); + } } }); diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index f25c20bb2..8594ca395 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -26,9 +26,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Bitmap; -import android.os.Build; import android.os.IBinder; -import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; @@ -39,17 +37,16 @@ import android.widget.RemoteViews; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.source.MediaSource; +import com.nostra13.universalimageloader.core.assist.FailReason; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.helper.LockManager; -import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.playqueue.PlayQueueItem; -import org.schabi.newpipe.util.ListHelper; +import org.schabi.newpipe.player.resolver.AudioPlaybackResolver; +import org.schabi.newpipe.player.resolver.MediaSourceTag; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ThemeHelper; @@ -94,7 +91,6 @@ public final class BackgroundPlayer extends Service { private NotificationCompat.Builder notBuilder; private RemoteViews notRemoteView; private RemoteViews bigNotRemoteView; - private final String setAlphaMethodName = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) ? "setImageAlpha" : "setAlpha"; private boolean shouldUpdateOnProgress; @@ -192,7 +188,9 @@ public final class BackgroundPlayer extends Service { .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setCustomContentView(notRemoteView) .setCustomBigContentView(bigNotRemoteView); - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) builder.setPriority(NotificationCompat.PRIORITY_MAX); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + builder.setPriority(NotificationCompat.PRIORITY_MAX); + } return builder; } @@ -249,15 +247,6 @@ public final class BackgroundPlayer extends Service { notificationManager.notify(NOTIFICATION_ID, notBuilder.build()); } - private void setControlsOpacity(@IntRange(from = 0, to = 255) int opacity) { - if (notRemoteView != null) notRemoteView.setInt(R.id.notificationPlayPause, setAlphaMethodName, opacity); - if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationPlayPause, setAlphaMethodName, opacity); - if (notRemoteView != null) notRemoteView.setInt(R.id.notificationFForward, setAlphaMethodName, opacity); - if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationFForward, setAlphaMethodName, opacity); - if (notRemoteView != null) notRemoteView.setInt(R.id.notificationFRewind, setAlphaMethodName, opacity); - if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationFRewind, setAlphaMethodName, opacity); - } - /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ @@ -279,8 +268,16 @@ public final class BackgroundPlayer extends Service { protected class BasePlayerImpl extends BasePlayer { + @NonNull final private AudioPlaybackResolver resolver; + BasePlayerImpl(Context context) { super(context); + this.resolver = new AudioPlaybackResolver(context, dataSource); + } + + @Override + public void initPlayer(boolean playOnReady) { + super.initPlayer(playOnReady); } @Override @@ -293,30 +290,41 @@ public final class BackgroundPlayer extends Service { startForeground(NOTIFICATION_ID, notBuilder.build()); } - @Override - public void initThumbnail(final String url) { - resetNotification(); - if (notRemoteView != null) notRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail); - if (bigNotRemoteView != null) bigNotRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail); - updateNotification(-1); - super.initThumbnail(url); + /*////////////////////////////////////////////////////////////////////////// + // Thumbnail Loading + //////////////////////////////////////////////////////////////////////////*/ + + private void updateNotificationThumbnail() { + if (basePlayerImpl == null) return; + if (notRemoteView != null) { + notRemoteView.setImageViewBitmap(R.id.notificationCover, + basePlayerImpl.getThumbnail()); + } + if (bigNotRemoteView != null) { + bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, + basePlayerImpl.getThumbnail()); + } } @Override public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { super.onLoadingComplete(imageUri, view, loadedImage); - - if (loadedImage != null) { - // rebuild notification here since remote view does not release bitmaps, causing memory leaks - resetNotification(); - - if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); - if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); - - updateNotification(-1); - } + resetNotification(); + updateNotificationThumbnail(); + updateNotification(-1); } + @Override + public void onLoadingFailed(String imageUri, View view, FailReason failReason) { + super.onLoadingFailed(imageUri, view, failReason); + resetNotification(); + updateNotificationThumbnail(); + updateNotification(-1); + } + /*////////////////////////////////////////////////////////////////////////// + // States Implementation + //////////////////////////////////////////////////////////////////////////*/ + @Override public void onPrepared(boolean playWhenReady) { super.onPrepared(playWhenReady); @@ -390,29 +398,18 @@ public final class BackgroundPlayer extends Service { // Playback Listener //////////////////////////////////////////////////////////////////////////*/ - protected void onMetadataChanged(@NonNull final PlayQueueItem item, - @Nullable final StreamInfo info, - final int newPlayQueueIndex, - final boolean hasPlayQueueItemChanged) { - if (shouldUpdateOnProgress || hasPlayQueueItemChanged) { - resetNotification(); - updateNotification(-1); - updateMetadata(); - } + protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { + super.onMetadataChanged(tag); + resetNotification(); + updateNotificationThumbnail(); + updateNotification(-1); + updateMetadata(); } @Override @Nullable public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { - final MediaSource liveSource = super.sourceOf(item, info); - if (liveSource != null) return liveSource; - - final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams()); - if (index < 0 || index >= info.getAudioStreams().size()) return null; - - final AudioStream audio = info.getAudioStreams().get(index); - return buildMediaSource(audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio), - MediaFormat.getSuffixById(audio.getFormatId())); + return resolver.resolve(info); } @Override @@ -439,8 +436,8 @@ public final class BackgroundPlayer extends Service { } private void updateMetadata() { - if (activityListener != null && currentInfo != null) { - activityListener.onMetadataUpdate(currentInfo); + if (activityListener != null && getCurrentMetadata() != null) { + activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata()); } } @@ -531,44 +528,36 @@ public final class BackgroundPlayer extends Service { updatePlayback(); } - @Override - public void onBlocked() { - super.onBlocked(); - - setControlsOpacity(77); - updateNotification(-1); - } - @Override public void onPlaying() { super.onPlaying(); - - setControlsOpacity(255); + resetNotification(); + updateNotificationThumbnail(); updateNotification(R.drawable.ic_pause_white); - lockManager.acquireWifiAndCpu(); } @Override public void onPaused() { super.onPaused(); - + resetNotification(); + updateNotificationThumbnail(); updateNotification(R.drawable.ic_play_arrow_white); - lockManager.releaseWifiAndCpu(); } @Override public void onCompleted() { super.onCompleted(); - - setControlsOpacity(255); - resetNotification(); - if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false); - if (notRemoteView != null) notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false); + if (bigNotRemoteView != null) { + bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false); + } + if (notRemoteView != null) { + notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false); + } + updateNotificationThumbnail(); updateNotification(R.drawable.ic_replay_white); - lockManager.releaseWifiAndCpu(); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 4c3d70421..7339dd50f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -24,16 +24,14 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.media.AudioManager; -import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.text.TextUtils; import android.util.Log; import android.view.View; import android.widget.Toast; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerFactory; @@ -49,7 +47,6 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; -import com.google.android.exoplayer2.util.Util; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; @@ -57,7 +54,6 @@ 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.local.history.HistoryRecordManager; import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.LoadController; @@ -72,6 +68,8 @@ import org.schabi.newpipe.player.playback.PlaybackListener; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueueAdapter; import org.schabi.newpipe.player.playqueue.PlayQueueItem; +import org.schabi.newpipe.player.resolver.MediaSourceTag; +import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.SerializedCache; import java.io.IOException; @@ -82,12 +80,12 @@ import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; +import io.reactivex.disposables.SerialDisposable; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT; -import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; /** * Base for the players, joining the common properties @@ -108,17 +106,26 @@ public abstract class BasePlayer implements @NonNull final protected HistoryRecordManager recordManager; + @NonNull final protected CustomTrackSelector trackSelector; + @NonNull final protected PlayerDataSource dataSource; + + @NonNull final private LoadControl loadControl; + @NonNull final private RenderersFactory renderFactory; + + @NonNull final private SerialDisposable progressUpdateReactor; + @NonNull final private CompositeDisposable databaseUpdateReactor; /*////////////////////////////////////////////////////////////////////////// // Intent //////////////////////////////////////////////////////////////////////////*/ - public static final String REPEAT_MODE = "repeat_mode"; - public static final String PLAYBACK_PITCH = "playback_pitch"; - public static final String PLAYBACK_SPEED = "playback_speed"; - public static final String PLAYBACK_QUALITY = "playback_quality"; - public static final String PLAY_QUEUE_KEY = "play_queue_key"; - public static final String APPEND_ONLY = "append_only"; - public static final String SELECT_ON_APPEND = "select_on_append"; + @NonNull public static final String REPEAT_MODE = "repeat_mode"; + @NonNull public static final String PLAYBACK_PITCH = "playback_pitch"; + @NonNull public static final String PLAYBACK_SPEED = "playback_speed"; + @NonNull public static final String PLAYBACK_SKIP_SILENCE = "playback_skip_silence"; + @NonNull public static final String PLAYBACK_QUALITY = "playback_quality"; + @NonNull public static final String PLAY_QUEUE_KEY = "play_queue_key"; + @NonNull public static final String APPEND_ONLY = "append_only"; + @NonNull public static final String SELECT_ON_APPEND = "select_on_append"; /*////////////////////////////////////////////////////////////////////////// // Playback @@ -129,12 +136,13 @@ public abstract class BasePlayer implements protected PlayQueue playQueue; protected PlayQueueAdapter playQueueAdapter; - protected MediaSourceManager playbackManager; + @Nullable protected MediaSourceManager playbackManager; - protected StreamInfo currentInfo; - protected PlayQueueItem currentItem; + @Nullable private PlayQueueItem currentItem; + @Nullable private MediaSourceTag currentMetadata; + @Nullable private Bitmap currentThumbnail; - protected Toast errorToast; + @Nullable protected Toast errorToast; /*////////////////////////////////////////////////////////////////////////// // Player @@ -145,18 +153,11 @@ public abstract class BasePlayer implements protected final static int PROGRESS_LOOP_INTERVAL_MILLIS = 500; protected final static int RECOVERY_SKIP_THRESHOLD_MILLIS = 3000; // 3 seconds - protected CustomTrackSelector trackSelector; - protected PlayerDataSource dataSource; - protected SimpleExoPlayer simpleExoPlayer; protected AudioReactor audioReactor; protected MediaSessionManager mediaSessionManager; private boolean isPrepared = false; - private boolean isSynchronizing = false; - - protected Disposable progressUpdateReactor; - protected CompositeDisposable databaseUpdateReactor; //////////////////////////////////////////////////////////////////////////*/ @@ -174,29 +175,32 @@ public abstract class BasePlayer implements context.registerReceiver(broadcastReceiver, intentFilter); this.recordManager = new HistoryRecordManager(context); + + this.progressUpdateReactor = new SerialDisposable(); + this.databaseUpdateReactor = new CompositeDisposable(); + + final String userAgent = Downloader.USER_AGENT; + final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); + this.dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter); + + final TrackSelection.Factory trackSelectionFactory = + PlayerHelper.getQualitySelector(context, bandwidthMeter); + this.trackSelector = new CustomTrackSelector(trackSelectionFactory); + + this.loadControl = new LoadController(context); + this.renderFactory = new DefaultRenderersFactory(context); } public void setup() { - if (simpleExoPlayer == null) initPlayer(/*playOnInit=*/true); + if (simpleExoPlayer == null) { + initPlayer(/*playOnInit=*/true); + } initListeners(); } public void initPlayer(final boolean playOnReady) { if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); - if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); - databaseUpdateReactor = new CompositeDisposable(); - - final String userAgent = Downloader.USER_AGENT; - final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); - dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter); - - final TrackSelection.Factory trackSelectionFactory = - PlayerHelper.getQualitySelector(context, bandwidthMeter); - trackSelector = new CustomTrackSelector(trackSelectionFactory); - - final LoadControl loadControl = new LoadController(context); - final RenderersFactory renderFactory = new DefaultRenderersFactory(context); simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl); simpleExoPlayer.addListener(this); simpleExoPlayer.setPlayWhenReady(playOnReady); @@ -235,20 +239,24 @@ public abstract class BasePlayer implements final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode()); final float playbackSpeed = intent.getFloatExtra(PLAYBACK_SPEED, getPlaybackSpeed()); final float playbackPitch = intent.getFloatExtra(PLAYBACK_PITCH, getPlaybackPitch()); + final boolean playbackSkipSilence = intent.getBooleanExtra(PLAYBACK_SKIP_SILENCE, + getPlaybackSkipSilence()); // Good to go... - initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, /*playOnInit=*/true); + initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, + /*playOnInit=*/true); } protected void initPlayback(@NonNull final PlayQueue queue, @Player.RepeatMode final int repeatMode, final float playbackSpeed, final float playbackPitch, + final boolean playbackSkipSilence, final boolean playOnReady) { destroyPlayer(); initPlayer(playOnReady); setRepeatMode(repeatMode); - setPlaybackParameters(playbackSpeed, playbackPitch); + setPlaybackParameters(playbackSpeed, playbackPitch, playbackSkipSilence); playQueue = queue; playQueue.init(); @@ -270,7 +278,6 @@ public abstract class BasePlayer implements if (playQueue != null) playQueue.dispose(); if (audioReactor != null) audioReactor.dispose(); if (playbackManager != null) playbackManager.dispose(); - if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); if (mediaSessionManager != null) mediaSessionManager.dispose(); if (playQueueAdapter != null) { @@ -284,20 +291,22 @@ public abstract class BasePlayer implements destroyPlayer(); unregisterBroadcastReceiver(); - trackSelector = null; + databaseUpdateReactor.clear(); + progressUpdateReactor.set(null); + simpleExoPlayer = null; - mediaSessionManager = null; } /*////////////////////////////////////////////////////////////////////////// // Thumbnail Loading //////////////////////////////////////////////////////////////////////////*/ - public void initThumbnail(final String url) { + private void initThumbnail(final String url) { if (DEBUG) Log.d(TAG, "Thumbnail - initThumbnail() called"); if (url == null || url.isEmpty()) return; ImageLoader.getInstance().resume(); - ImageLoader.getInstance().loadImage(url, this); + ImageLoader.getInstance().loadImage(url, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, + this); } @Override @@ -310,6 +319,7 @@ public abstract class BasePlayer implements public void onLoadingFailed(String imageUri, View view, FailReason failReason) { Log.e(TAG, "Thumbnail - onLoadingFailed() called on imageUri = [" + imageUri + "]", failReason.getCause()); + currentThumbnail = null; } @Override @@ -317,64 +327,14 @@ public abstract class BasePlayer implements if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingComplete() called with: " + "imageUri = [" + imageUri + "], view = [" + view + "], " + "loadedImage = [" + loadedImage + "]"); + currentThumbnail = loadedImage; } @Override public void onLoadingCancelled(String imageUri, View view) { if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " + "imageUri = [" + imageUri + "], view = [" + view + "]"); - } - /*////////////////////////////////////////////////////////////////////////// - // MediaSource Building - //////////////////////////////////////////////////////////////////////////*/ - - public MediaSource buildLiveMediaSource(@NonNull final String sourceUrl, - @C.ContentType final int type) { - if (DEBUG) { - Log.d(TAG, "buildLiveMediaSource() called with: url = [" + sourceUrl + - "], content type = [" + type + "]"); - } - if (dataSource == null) return null; - - final Uri uri = Uri.parse(sourceUrl); - switch (type) { - case C.TYPE_SS: - return dataSource.getLiveSsMediaSourceFactory().createMediaSource(uri); - case C.TYPE_DASH: - return dataSource.getLiveDashMediaSourceFactory().createMediaSource(uri); - case C.TYPE_HLS: - return dataSource.getLiveHlsMediaSourceFactory().createMediaSource(uri); - default: - throw new IllegalStateException("Unsupported type: " + type); - } - } - - public MediaSource buildMediaSource(@NonNull final String sourceUrl, - @NonNull final String cacheKey, - @NonNull final String overrideExtension) { - if (DEBUG) { - Log.d(TAG, "buildMediaSource() called with: url = [" + sourceUrl + - "], cacheKey = [" + cacheKey + "]" + - "], overrideExtension = [" + overrideExtension + "]"); - } - if (dataSource == null) return null; - - final Uri uri = Uri.parse(sourceUrl); - @C.ContentType final int type = TextUtils.isEmpty(overrideExtension) ? - Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); - - switch (type) { - case C.TYPE_SS: - return dataSource.getLiveSsMediaSourceFactory().createMediaSource(uri); - case C.TYPE_DASH: - return dataSource.getDashMediaSourceFactory().createMediaSource(uri); - case C.TYPE_HLS: - return dataSource.getHlsMediaSourceFactory().createMediaSource(uri); - case C.TYPE_OTHER: - return dataSource.getExtractorMediaSourceFactory(cacheKey).createMediaSource(uri); - default: - throw new IllegalStateException("Unsupported type: " + type); - } + currentThumbnail = null; } /*////////////////////////////////////////////////////////////////////////// @@ -510,13 +470,11 @@ public abstract class BasePlayer implements public abstract void onUpdateProgress(int currentProgress, int duration, int bufferPercent); protected void startProgressLoop() { - if (progressUpdateReactor != null) progressUpdateReactor.dispose(); - progressUpdateReactor = getProgressReactor(); + progressUpdateReactor.set(getProgressReactor()); } protected void stopProgressLoop() { - if (progressUpdateReactor != null) progressUpdateReactor.dispose(); - progressUpdateReactor = null; + progressUpdateReactor.set(null); } public void triggerProgressUpdate() { @@ -531,7 +489,8 @@ public abstract class BasePlayer implements private Disposable getProgressReactor() { return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignored -> triggerProgressUpdate()); + .subscribe(ignored -> triggerProgressUpdate(), + error -> Log.e(TAG, "Progress update failure: ", error)); } /*////////////////////////////////////////////////////////////////////////// @@ -545,28 +504,16 @@ 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 - // 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 to default position."); - seekToDefault(); - } - break; - } + maybeUpdateCurrentMetadata(); } @Override public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { if (DEBUG) Log.d(TAG, "ExoPlayer - onTracksChanged(), " + "track group size = " + trackGroups.length); + + maybeUpdateCurrentMetadata(); } @Override @@ -586,6 +533,8 @@ public abstract class BasePlayer implements } else if (isLoading && !isProgressLoopRunning()) { startProgressLoop(); } + + maybeUpdateCurrentMetadata(); } @Override @@ -609,6 +558,7 @@ public abstract class BasePlayer implements } break; case Player.STATE_READY: //3 + maybeUpdateCurrentMetadata(); maybeCorrectSeekPosition(); if (!isPrepared) { isPrepared = true; @@ -625,38 +575,19 @@ public abstract class BasePlayer implements } private void maybeCorrectSeekPosition() { - if (playQueue == null || simpleExoPlayer == null || currentInfo == null) return; + if (playQueue == null || simpleExoPlayer == null || currentMetadata == null) return; - final int currentSourceIndex = playQueue.getIndex(); final PlayQueueItem currentSourceItem = playQueue.getItem(); if (currentSourceItem == null) return; - final long recoveryPositionMillis = currentSourceItem.getRecoveryPosition(); - final boolean isCurrentWindowCorrect = - simpleExoPlayer.getCurrentPeriodIndex() == currentSourceIndex; + final StreamInfo currentInfo = currentMetadata.getMetadata(); final long presetStartPositionMillis = currentInfo.getStartPosition() * 1000; - - 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 && isLive()) { - if (DEBUG) Log.d(TAG, "Playback - Synchronizing livestream to default time"); - // Is still synchronizing? - seekToDefault(); - - } else if (isSynchronizing && presetStartPositionMillis > 0L) { + if (presetStartPositionMillis > 0L) { + // Has another start position? if (DEBUG) Log.d(TAG, "Playback - Seeking to preset start " + "position=[" + presetStartPositionMillis + "]"); - // Has another start position? seekTo(presetStartPositionMillis); - currentInfo.setStartPosition(0); } - - isSynchronizing = false; } /** @@ -708,7 +639,7 @@ public abstract class BasePlayer implements setRecovery(); final Throwable cause = error.getCause(); - if (cause instanceof BehindLiveWindowException) { + if (error instanceof BehindLiveWindowException) { reload(); } else if (cause instanceof UnknownHostException) { playQueue.error(/*isNetworkProblem=*/true); @@ -727,22 +658,29 @@ public abstract class BasePlayer implements public void onPositionDiscontinuity(@Player.DiscontinuityReason final int reason) { if (DEBUG) Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with " + "reason = [" + reason + "]"); - // Refresh the playback if there is a transition to the next video - final int newPeriodIndex = simpleExoPlayer.getCurrentPeriodIndex(); + if (playQueue == null) return; - /* Discontinuity reasons!! Thank you ExoPlayer lords */ + // Refresh the playback if there is a transition to the next video + final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); switch (reason) { case DISCONTINUITY_REASON_PERIOD_TRANSITION: - if (newPeriodIndex == playQueue.getIndex()) { + // When player is in single repeat mode and a period transition occurs, + // we need to register a view count here since no metadata has changed + if (getRepeatMode() == Player.REPEAT_MODE_ONE && + newWindowIndex == playQueue.getIndex()) { registerView(); - } else { - playQueue.offsetIndex(+1); + break; } case DISCONTINUITY_REASON_SEEK: case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: case DISCONTINUITY_REASON_INTERNAL: + if (playQueue.getIndex() != newWindowIndex) { + playQueue.setIndex(newWindowIndex); + } break; } + + maybeUpdateCurrentMetadata(); } @Override @@ -788,7 +726,7 @@ public abstract class BasePlayer implements if (DEBUG) Log.d(TAG, "Playback - onPlaybackBlock() called"); currentItem = null; - currentInfo = null; + currentMetadata = null; simpleExoPlayer.stop(); isPrepared = false; @@ -805,42 +743,21 @@ public abstract class BasePlayer implements simpleExoPlayer.prepare(mediaSource); } - @Override - public void onPlaybackSynchronize(@NonNull final PlayQueueItem item, - @Nullable final StreamInfo info) { + public void onPlaybackSynchronize(@NonNull final PlayQueueItem item) { 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; - } - + if (!hasPlayQueueItemChanged) return; currentItem = item; - currentInfo = info; - if (hasPlayQueueItemChanged) { - // updates only to the stream info should not trigger another view count - registerView(); - initThumbnail(info == null ? item.getThumbnailUrl() : info.getThumbnailUrl()); - } - onMetadataChanged(item, info, currentPlayQueueIndex, hasPlayQueueItemChanged); // Check if on wrong window if (currentPlayQueueIndex != playQueue.getIndex()) { @@ -855,39 +772,29 @@ public abstract class BasePlayer implements "index=[" + currentPlayQueueIndex + "] with " + "playlist length=[" + currentPlaylistSize + "]"); - // 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); + + if (item.getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { + simpleExoPlayer.seekTo(currentPlayQueueIndex, item.getRecoveryPosition()); + playQueue.unsetRecovery(currentPlayQueueIndex); + } else { + simpleExoPlayer.seekToDefaultPosition(currentPlayQueueIndex); + } } } - abstract protected void onMetadataChanged(@NonNull final PlayQueueItem item, - @Nullable final StreamInfo info, - final int newPlayQueueIndex, - final boolean hasPlayQueueItemChanged); - - @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; + protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { + final StreamInfo info = tag.getMetadata(); + if (DEBUG) { + Log.d(TAG, "Playback - onMetadataChanged() called, playing: " + info.getName()); } - if (!info.getHlsUrl().isEmpty()) { - return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS); - } else if (!info.getDashMpdUrl().isEmpty()) { - return buildLiveMediaSource(info.getDashMpdUrl(), C.TYPE_DASH); - } - - return null; + initThumbnail(info.getThumbnailUrl()); + registerView(); } @Override @@ -1020,9 +927,7 @@ public abstract class BasePlayer implements 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); + if (simpleExoPlayer != null) simpleExoPlayer.seekTo(positionMillis); } public void seekBy(long offsetMillis) { @@ -1046,12 +951,14 @@ public abstract class BasePlayer implements //////////////////////////////////////////////////////////////////////////*/ private void registerView() { - if (databaseUpdateReactor == null || currentInfo == null) return; - databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete() + if (currentMetadata == null) return; + final StreamInfo currentInfo = currentMetadata.getMetadata(); + final Disposable viewRegister = recordManager.onViewed(currentInfo).onErrorComplete() .subscribe( ignored -> {/* successful */}, error -> Log.e(TAG, "Player onViewed() failure: ", error) - )); + ); + databaseUpdateReactor.add(viewRegister); } protected void reload() { @@ -1065,7 +972,7 @@ public abstract class BasePlayer implements } protected void savePlaybackState(final StreamInfo info, final long progress) { - if (info == null || databaseUpdateReactor == null) return; + if (info == null) return; final Disposable stateSaver = recordManager.saveStreamState(info, progress) .observeOn(AndroidSchedulers.mainThread()) .onErrorComplete() @@ -1077,7 +984,8 @@ public abstract class BasePlayer implements } private void savePlaybackState() { - if (simpleExoPlayer == null || currentInfo == null) return; + if (simpleExoPlayer == null || currentMetadata == null) return; + final StreamInfo currentInfo = currentMetadata.getMetadata(); if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD_MILLIS && simpleExoPlayer.getCurrentPosition() < @@ -1085,6 +993,34 @@ public abstract class BasePlayer implements savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition()); } } + + private void maybeUpdateCurrentMetadata() { + if (simpleExoPlayer == null) return; + + final MediaSourceTag metadata; + try { + metadata = (MediaSourceTag) simpleExoPlayer.getCurrentTag(); + } catch (IndexOutOfBoundsException | ClassCastException error) { + return; + } + + if (metadata == null) return; + maybeAutoQueueNextStream(metadata); + + if (currentMetadata == metadata) return; + currentMetadata = metadata; + onMetadataChanged(metadata); + } + + private void maybeAutoQueueNextStream(@NonNull final MediaSourceTag currentMetadata) { + if (playQueue == null || playQueue.getIndex() != playQueue.size() - 1 || + getRepeatMode() != Player.REPEAT_MODE_OFF || + !PlayerHelper.isAutoQueueEnabled(context)) return; + // auto queue when starting playback on the last item when not repeating + final PlayQueue autoQueue = PlayerHelper.autoQueueOf(currentMetadata.getMetadata(), + playQueue.getStreams()); + if (autoQueue != null) playQueue.append(autoQueue.getStreams()); + } /*////////////////////////////////////////////////////////////////////////// // Getters and Setters //////////////////////////////////////////////////////////////////////////*/ @@ -1101,19 +1037,35 @@ public abstract class BasePlayer implements return currentState; } + @Nullable + public MediaSourceTag getCurrentMetadata() { + return currentMetadata; + } + + @NonNull public String getVideoUrl() { - return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getUrl(); + return currentMetadata == null ? context.getString(R.string.unknown_content) : currentMetadata.getMetadata().getUrl(); } + @NonNull public String getVideoTitle() { - return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getTitle(); + return currentMetadata == null ? context.getString(R.string.unknown_content) : currentMetadata.getMetadata().getName(); } + @NonNull public String getUploaderName() { - return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getUploader(); + return currentMetadata == null ? context.getString(R.string.unknown_content) : currentMetadata.getMetadata().getUploaderName(); + } + + @Nullable + public Bitmap getThumbnail() { + return currentThumbnail == null ? + BitmapFactory.decodeResource(context.getResources(), R.drawable.dummy_thumbnail) : + currentThumbnail; } /** Checks if the current playback is a livestream AND is playing at or beyond the live edge */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") public boolean isLiveEdge() { if (simpleExoPlayer == null || !isLive()) return false; @@ -1147,11 +1099,11 @@ public abstract class BasePlayer implements @Player.RepeatMode public int getRepeatMode() { - return simpleExoPlayer.getRepeatMode(); + return simpleExoPlayer == null ? Player.REPEAT_MODE_OFF : simpleExoPlayer.getRepeatMode(); } public void setRepeatMode(@Player.RepeatMode final int repeatMode) { - simpleExoPlayer.setRepeatMode(repeatMode); + if (simpleExoPlayer != null) simpleExoPlayer.setRepeatMode(repeatMode); } public float getPlaybackSpeed() { @@ -1162,19 +1114,22 @@ public abstract class BasePlayer implements return getPlaybackParameters().pitch; } + public boolean getPlaybackSkipSilence() { + return getPlaybackParameters().skipSilence; + } + public void setPlaybackSpeed(float speed) { - setPlaybackParameters(speed, getPlaybackPitch()); + setPlaybackParameters(speed, getPlaybackPitch(), getPlaybackSkipSilence()); } public PlaybackParameters getPlaybackParameters() { - final PlaybackParameters defaultParameters = new PlaybackParameters(1f, 1f); - if (simpleExoPlayer == null) return defaultParameters; + if (simpleExoPlayer == null) return PlaybackParameters.DEFAULT; final PlaybackParameters parameters = simpleExoPlayer.getPlaybackParameters(); - return parameters == null ? defaultParameters : parameters; + return parameters == null ? PlaybackParameters.DEFAULT : parameters; } - public void setPlaybackParameters(float speed, float pitch) { - simpleExoPlayer.setPlaybackParameters(new PlaybackParameters(speed, pitch)); + public void setPlaybackParameters(float speed, float pitch, boolean skipSilence) { + simpleExoPlayer.setPlaybackParameters(new PlaybackParameters(speed, pitch, skipSilence)); } public PlayQueue getPlayQueue() { @@ -1190,7 +1145,7 @@ public abstract class BasePlayer implements } public boolean isProgressLoopRunning() { - return progressUpdateReactor != null && !progressUpdateReactor.isDisposed(); + return progressUpdateReactor.get() != null; } public void setRecovery() { diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 0dea47e56..9f3b5d020 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -58,7 +58,6 @@ import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.SubtitleView; 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; @@ -67,6 +66,8 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder; import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder; import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; +import org.schabi.newpipe.player.resolver.MediaSourceTag; +import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; @@ -104,6 +105,7 @@ public final class MainVideoPlayer extends AppCompatActivity @Nullable private PlayerState playerState; private boolean isInMultiWindow; + private boolean isBackPressed; /*////////////////////////////////////////////////////////////////////////// // Activity LifeCycle @@ -125,7 +127,7 @@ public final class MainVideoPlayer extends AppCompatActivity hideSystemUi(); setContentView(R.layout.activity_main_player); - playerImpl = new VideoPlayerImpl(this); + playerImpl = new VideoPlayerImpl(this); playerImpl.setup(findViewById(android.R.id.content)); if (savedInstanceState != null && savedInstanceState.get(KEY_SAVED_STATE) != null) { @@ -152,7 +154,10 @@ public final class MainVideoPlayer extends AppCompatActivity protected void onNewIntent(Intent intent) { if (DEBUG) Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]"); super.onNewIntent(intent); - playerImpl.handleIntent(intent); + if (intent != null) { + playerState = null; + playerImpl.handleIntent(intent); + } } @Override @@ -177,7 +182,7 @@ public final class MainVideoPlayer extends AppCompatActivity playerImpl.setPlaybackQuality(playerState.getPlaybackQuality()); playerImpl.initPlayback(playerState.getPlayQueue(), playerState.getRepeatMode(), playerState.getPlaybackSpeed(), playerState.getPlaybackPitch(), - playerState.wasPlaying()); + playerState.isPlaybackSkipSilence(), playerState.wasPlaying()); } } @@ -191,6 +196,12 @@ public final class MainVideoPlayer extends AppCompatActivity } } + @Override + public void onBackPressed() { + super.onBackPressed(); + isBackPressed = true; + } + @Override protected void onSaveInstanceState(Bundle outState) { if (DEBUG) Log.d(TAG, "onSaveInstanceState() called"); @@ -200,7 +211,8 @@ public final class MainVideoPlayer extends AppCompatActivity playerImpl.setRecovery(); playerState = new PlayerState(playerImpl.getPlayQueue(), playerImpl.getRepeatMode(), playerImpl.getPlaybackSpeed(), playerImpl.getPlaybackPitch(), - playerImpl.getPlaybackQuality(), playerImpl.isPlaying()); + playerImpl.getPlaybackQuality(), playerImpl.getPlaybackSkipSilence(), + playerImpl.isPlaying()); StateSaver.tryToSave(isChangingConfigurations(), null, outState, this); } @@ -208,10 +220,17 @@ public final class MainVideoPlayer extends AppCompatActivity protected void onStop() { if (DEBUG) Log.d(TAG, "onStop() called"); super.onStop(); - playerImpl.destroy(); - PlayerHelper.setScreenBrightness(getApplicationContext(), getWindow().getAttributes().screenBrightness); + + if (playerImpl == null) return; + if (!isBackPressed) { + playerImpl.minimize(); + } + playerImpl.destroy(); + + isInMultiWindow = false; + isBackPressed = false; } /*////////////////////////////////////////////////////////////////////////// @@ -335,8 +354,11 @@ public final class MainVideoPlayer extends AppCompatActivity //////////////////////////////////////////////////////////////////////////// @Override - public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) { - if (playerImpl != null) playerImpl.setPlaybackParameters(playbackTempo, playbackPitch); + public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch, + boolean playbackSkipSilence) { + if (playerImpl != null) { + playerImpl.setPlaybackParameters(playbackTempo, playbackPitch, playbackSkipSilence); + } } /////////////////////////////////////////////////////////////////////////// @@ -441,6 +463,21 @@ public final class MainVideoPlayer extends AppCompatActivity switchPopupButton.setOnClickListener(this); } + public void minimize() { + switch (PlayerHelper.getMinimizeOnExitAction(context)) { + case PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND: + onPlayBackgroundButtonClicked(); + break; + case PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP: + onFullScreenButtonClicked(); + break; + case PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE: + default: + // No action + break; + } + } + /*////////////////////////////////////////////////////////////////////////// // ExoPlayer Video Listener //////////////////////////////////////////////////////////////////////////*/ @@ -461,14 +498,11 @@ public final class MainVideoPlayer extends AppCompatActivity // Playback Listener //////////////////////////////////////////////////////////////////////////*/ - protected void onMetadataChanged(@NonNull final PlayQueueItem item, - @Nullable final StreamInfo info, - final int newPlayQueueIndex, - final boolean hasPlayQueueItemChanged) { - super.onMetadataChanged(item, info, newPlayQueueIndex, false); + protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { + super.onMetadataChanged(tag); - titleTextView.setText(getVideoTitle()); - channelTextView.setText(getUploaderName()); + titleTextView.setText(tag.getMetadata().getName()); + channelTextView.setText(tag.getMetadata().getUploaderName()); } @Override @@ -501,6 +535,7 @@ public final class MainVideoPlayer extends AppCompatActivity this.getRepeatMode(), this.getPlaybackSpeed(), this.getPlaybackPitch(), + this.getPlaybackSkipSilence(), this.getPlaybackQuality() ); context.startService(intent); @@ -522,6 +557,7 @@ public final class MainVideoPlayer extends AppCompatActivity this.getRepeatMode(), this.getPlaybackSpeed(), this.getPlaybackPitch(), + this.getPlaybackSkipSilence(), this.getPlaybackQuality() ); context.startService(intent); @@ -617,7 +653,8 @@ public final class MainVideoPlayer extends AppCompatActivity @Override public void onPlaybackSpeedClicked() { - PlaybackParameterDialog.newInstance(getPlaybackSpeed(), getPlaybackPitch()) + PlaybackParameterDialog + .newInstance(getPlaybackSpeed(), getPlaybackPitch(), getPlaybackSkipSilence()) .show(getSupportFragmentManager(), TAG); } @@ -647,14 +684,19 @@ public final class MainVideoPlayer extends AppCompatActivity } @Override - protected int getDefaultResolutionIndex(final List sortedVideos) { - return ListHelper.getDefaultResolutionIndex(context, sortedVideos); - } + protected VideoPlaybackResolver.QualityResolver getQualityResolver() { + return new VideoPlaybackResolver.QualityResolver() { + @Override + public int getDefaultResolutionIndex(List sortedVideos) { + return ListHelper.getDefaultResolutionIndex(context, sortedVideos); + } - @Override - protected int getOverrideResolutionIndex(final List sortedVideos, - final String playbackQuality) { - return ListHelper.getResolutionIndex(context, sortedVideos, playbackQuality); + @Override + public int getOverrideResolutionIndex(List sortedVideos, + String playbackQuality) { + return ListHelper.getResolutionIndex(context, sortedVideos, playbackQuality); + } + }; } /*////////////////////////////////////////////////////////////////////////// @@ -678,7 +720,6 @@ public final class MainVideoPlayer extends AppCompatActivity @Override public void onBuffering() { super.onBuffering(); - animatePlayButtons(false, 100); getRootView().setKeepScreenOn(true); } @@ -854,7 +895,6 @@ public final class MainVideoPlayer extends AppCompatActivity @Override public boolean onDoubleTap(MotionEvent e) { if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY()); - if (!playerImpl.isPlaying()) return false; if (e.getX() > playerImpl.getRootView().getWidth() * 2 / 3) { playerImpl.onFastForward(); diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerState.java b/app/src/main/java/org/schabi/newpipe/player/PlayerState.java index 8ffcb6b29..359159809 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerState.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerState.java @@ -14,21 +14,26 @@ public class PlayerState implements Serializable { private final float playbackSpeed; private final float playbackPitch; @Nullable private final String playbackQuality; + private final boolean playbackSkipSilence; private final boolean wasPlaying; PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode, - final float playbackSpeed, final float playbackPitch, final boolean wasPlaying) { - this(playQueue, repeatMode, playbackSpeed, playbackPitch, null, wasPlaying); + final float playbackSpeed, final float playbackPitch, + final boolean playbackSkipSilence, final boolean wasPlaying) { + this(playQueue, repeatMode, playbackSpeed, playbackPitch, null, + playbackSkipSilence, wasPlaying); } PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode, final float playbackSpeed, final float playbackPitch, - @Nullable final String playbackQuality, final boolean wasPlaying) { + @Nullable final String playbackQuality, final boolean playbackSkipSilence, + final boolean wasPlaying) { this.playQueue = playQueue; this.repeatMode = repeatMode; this.playbackSpeed = playbackSpeed; this.playbackPitch = playbackPitch; this.playbackQuality = playbackQuality; + this.playbackSkipSilence = playbackSkipSilence; this.wasPlaying = wasPlaying; } @@ -62,6 +67,10 @@ public class PlayerState implements Serializable { return playbackQuality; } + public boolean isPlaybackSkipSilence() { + return playbackSkipSilence; + } + public boolean wasPlaying() { return wasPlaying; } diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 8107345a1..86998e0ea 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -34,7 +34,6 @@ import android.os.Build; import android.os.IBinder; import android.preference.PreferenceManager; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; import android.util.DisplayMetrics; import android.util.Log; @@ -56,16 +55,17 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.text.CaptionStyleCompat; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.SubtitleView; +import com.nostra13.universalimageloader.core.assist.FailReason; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.helper.LockManager; import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.old.PlayVideoActivity; -import org.schabi.newpipe.player.playqueue.PlayQueueItem; +import org.schabi.newpipe.player.resolver.MediaSourceTag; +import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ThemeHelper; @@ -98,6 +98,11 @@ public final class PopupVideoPlayer extends Service { private static final int MINIMUM_SHOW_EXTRA_WIDTH_DP = 300; + private static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | + WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + private static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS | + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; + private WindowManager windowManager; private WindowManager.LayoutParams windowLayoutParams; private GestureDetector gestureDetector; @@ -191,14 +196,17 @@ public final class PopupVideoPlayer extends Service { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); popupWidth = popupRememberSizeAndPos ? sharedPreferences.getFloat(POPUP_SAVED_WIDTH, defaultSize) : defaultSize; - final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_PHONE : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O ? + WindowManager.LayoutParams.TYPE_PHONE : + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; windowLayoutParams = new WindowManager.LayoutParams( (int) popupWidth, (int) getMinimumVideoHeight(popupWidth), layoutParamType, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + IDLE_WINDOW_FLAGS, PixelFormat.TRANSLUCENT); windowLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; + windowLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; int centerX = (int) (screenWidth / 2f - popupWidth / 2f); int centerY = (int) (screenHeight / 2f - popupHeight / 2f); @@ -228,6 +236,7 @@ public final class PopupVideoPlayer extends Service { notRemoteView.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle()); notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getUploaderName()); + notRemoteView.setImageViewBitmap(R.id.notificationCover, playerImpl.getThumbnail()); notRemoteView.setOnClickPendingIntent(R.id.notificationPlayPause, PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT)); @@ -243,11 +252,15 @@ public final class PopupVideoPlayer extends Service { setRepeatModeRemote(notRemoteView, playerImpl.getRepeatMode()); - return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) .setOngoing(true) .setSmallIcon(R.drawable.ic_newpipe_triangle_white) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setContent(notRemoteView); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + builder.setPriority(NotificationCompat.PRIORITY_MAX); + } + return builder; } /** @@ -366,6 +379,12 @@ public final class PopupVideoPlayer extends Service { } } + private void updateWindowFlags(final int flags) { + if (windowLayoutParams == null || windowManager == null || playerImpl == null) return; + + windowLayoutParams.flags = flags; + windowManager.updateViewLayout(playerImpl.getRootView(), windowLayoutParams); + } /////////////////////////////////////////////////////////////////////////// protected class VideoPlayerImpl extends VideoPlayer implements View.OnLayoutChangeListener { @@ -428,21 +447,6 @@ public final class PopupVideoPlayer extends Service { super.destroy(); } - @Override - public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { - super.onLoadingComplete(imageUri, view, loadedImage); - if (loadedImage != null) { - // rebuild notification here since remote view does not release bitmaps, causing memory leaks - notBuilder = createNotification(); - - if (notRemoteView != null) { - notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); - } - - updateNotification(-1); - } - } - @Override public void onFullScreenButtonClicked() { super.onFullScreenButtonClicked(); @@ -459,6 +463,7 @@ public final class PopupVideoPlayer extends Service { this.getRepeatMode(), this.getPlaybackSpeed(), this.getPlaybackPitch(), + this.getPlaybackSkipSilence(), this.getPlaybackQuality() ); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); @@ -510,14 +515,47 @@ public final class PopupVideoPlayer extends Service { } @Override - protected int getDefaultResolutionIndex(final List sortedVideos) { - return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos); + protected VideoPlaybackResolver.QualityResolver getQualityResolver() { + return new VideoPlaybackResolver.QualityResolver() { + @Override + public int getDefaultResolutionIndex(List sortedVideos) { + return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos); + } + + @Override + public int getOverrideResolutionIndex(List sortedVideos, + String playbackQuality) { + return ListHelper.getPopupResolutionIndex(context, sortedVideos, + playbackQuality); + } + }; + } + + /*////////////////////////////////////////////////////////////////////////// + // Thumbnail Loading + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + super.onLoadingComplete(imageUri, view, loadedImage); + // rebuild notification here since remote view does not release bitmaps, + // causing memory leaks + resetNotification(); + updateNotification(-1); } @Override - protected int getOverrideResolutionIndex(final List sortedVideos, - final String playbackQuality) { - return ListHelper.getPopupResolutionIndex(context, sortedVideos, playbackQuality); + public void onLoadingFailed(String imageUri, View view, FailReason failReason) { + super.onLoadingFailed(imageUri, view, failReason); + resetNotification(); + updateNotification(-1); + } + + @Override + public void onLoadingCancelled(String imageUri, View view) { + super.onLoadingCancelled(imageUri, view); + resetNotification(); + updateNotification(-1); } /*////////////////////////////////////////////////////////////////////////// @@ -538,8 +576,8 @@ public final class PopupVideoPlayer extends Service { } private void updateMetadata() { - if (activityListener != null && currentInfo != null) { - activityListener.onMetadataUpdate(currentInfo); + if (activityListener != null && getCurrentMetadata() != null) { + activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata()); } } @@ -571,8 +609,9 @@ public final class PopupVideoPlayer extends Service { public void onRepeatModeChanged(int i) { super.onRepeatModeChanged(i); setRepeatModeRemote(notRemoteView, i); - updateNotification(-1); updatePlayback(); + resetNotification(); + updateNotification(-1); } @Override @@ -585,11 +624,10 @@ public final class PopupVideoPlayer extends Service { // Playback Listener //////////////////////////////////////////////////////////////////////////*/ - protected void onMetadataChanged(@NonNull final PlayQueueItem item, - @Nullable final StreamInfo info, - final int newPlayQueueIndex, - final boolean hasPlayQueueItemChanged) { - super.onMetadataChanged(item, info, newPlayQueueIndex, false); + protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { + super.onMetadataChanged(tag); + resetNotification(); + updateNotification(-1); updateMetadata(); } @@ -652,46 +690,70 @@ public final class PopupVideoPlayer extends Service { @Override public void onBlocked() { super.onBlocked(); + resetNotification(); updateNotification(R.drawable.ic_play_arrow_white); } @Override public void onPlaying() { super.onPlaying(); - updateNotification(R.drawable.ic_pause_white); - videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white); - lockManager.acquireWifiAndCpu(); + updateWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS); + + resetNotification(); + updateNotification(R.drawable.ic_pause_white); + + videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white); hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + + startForeground(NOTIFICATION_ID, notBuilder.build()); + lockManager.acquireWifiAndCpu(); } @Override public void onBuffering() { super.onBuffering(); + resetNotification(); updateNotification(R.drawable.ic_play_arrow_white); } @Override public void onPaused() { super.onPaused(); + + updateWindowFlags(IDLE_WINDOW_FLAGS); + + resetNotification(); updateNotification(R.drawable.ic_play_arrow_white); + videoPlayPause.setBackgroundResource(R.drawable.ic_play_arrow_white); lockManager.releaseWifiAndCpu(); + + stopForeground(false); } @Override public void onPausedSeek() { super.onPausedSeek(); - videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white); + resetNotification(); updateNotification(R.drawable.ic_play_arrow_white); + + videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white); } @Override public void onCompleted() { super.onCompleted(); + + updateWindowFlags(IDLE_WINDOW_FLAGS); + + resetNotification(); updateNotification(R.drawable.ic_replay_white); + videoPlayPause.setBackgroundResource(R.drawable.ic_replay_white); lockManager.releaseWifiAndCpu(); + + stopForeground(false); } @Override @@ -709,16 +771,15 @@ public final class PopupVideoPlayer extends Service { super.hideControlsAndButton(duration, delay, videoPlayPause); } - - /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ /*package-private*/ void enableVideoRenderer(final boolean enable) { final int videoRendererIndex = getRendererIndex(C.TRACK_TYPE_VIDEO); - if (trackSelector != null && videoRendererIndex != RENDERER_UNAVAILABLE) { - trackSelector.setRendererDisabled(videoRendererIndex, !enable); + if (videoRendererIndex != RENDERER_UNAVAILABLE) { + trackSelector.setParameters(trackSelector.buildUponParameters() + .setRendererDisabled(videoRendererIndex, !enable)); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 8b96b651e..b57a710ed 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -187,6 +187,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity this.player.getRepeatMode(), this.player.getPlaybackSpeed(), this.player.getPlaybackPitch(), + this.player.getPlaybackSkipSilence(), null ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } @@ -340,6 +341,13 @@ public abstract class ServicePlayerActivity extends AppCompatActivity return true; }); + final MenuItem share = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/3, + Menu.NONE, R.string.share); + share.setOnMenuItemClickListener(menuItem -> { + shareUrl(item.getTitle(), item.getUrl()); + return true; + }); + menu.show(); } @@ -459,13 +467,16 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private void openPlaybackParameterDialog() { if (player == null) return; - PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), - player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag()); + PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(), + player.getPlaybackSkipSilence()).show(getSupportFragmentManager(), getTag()); } @Override - public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) { - if (player != null) player.setPlaybackParameters(playbackTempo, playbackPitch); + public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch, + boolean playbackSkipSilence) { + if (player != null) { + player.setPlaybackParameters(playbackTempo, playbackPitch, playbackSkipSilence); + } } //////////////////////////////////////////////////////////////////////////// @@ -509,6 +520,18 @@ public abstract class ServicePlayerActivity extends AppCompatActivity .show(getSupportFragmentManager(), getTag()); } + //////////////////////////////////////////////////////////////////////////// + // Share + //////////////////////////////////////////////////////////////////////////// + + private void shareUrl(String subject, String url) { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_SUBJECT, subject); + intent.putExtra(Intent.EXTRA_TEXT, url); + startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title))); + } + //////////////////////////////////////////////////////////////////////////// // Binding Service Listener //////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index 5ea1c74a0..679fc6645 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -29,7 +29,6 @@ import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.PorterDuff; -import android.net.Uri; import android.os.Build; import android.os.Handler; import android.support.annotation.NonNull; @@ -47,11 +46,9 @@ import android.widget.SeekBar; import android.widget.TextView; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.CaptionStyleCompat; @@ -62,21 +59,17 @@ import com.google.android.exoplayer2.video.VideoListener; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.Subtitles; -import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.playqueue.PlayQueueItem; +import org.schabi.newpipe.player.resolver.MediaSourceTag; +import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.util.AnimationUtils; -import org.schabi.newpipe.util.ListHelper; import java.util.ArrayList; import java.util.List; -import static com.google.android.exoplayer2.C.SELECTION_FLAG_AUTOSELECT; -import static com.google.android.exoplayer2.C.TIME_UNSET; import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -105,13 +98,12 @@ public abstract class VideoPlayer extends BasePlayer public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds - private ArrayList availableStreams; + private List availableStreams; private int selectedStreamIndex; - protected String playbackQuality; - protected boolean wasPlaying = false; + @NonNull final private VideoPlaybackResolver resolver; /*////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////*/ @@ -162,6 +154,7 @@ public abstract class VideoPlayer extends BasePlayer public VideoPlayer(String debugTag, Context context) { super(context); this.TAG = debugTag; + this.resolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver()); } public void setup(View rootView) { @@ -241,7 +234,8 @@ public abstract class VideoPlayer extends BasePlayer // Setup audio session with onboard equalizer if (Build.VERSION.SDK_INT >= 21) { - trackSelector.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context)); + trackSelector.setParameters(trackSelector.buildUponParameters() + .setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context))); } } @@ -297,8 +291,9 @@ public abstract class VideoPlayer extends BasePlayer 0, Menu.NONE, R.string.caption_none); captionOffItem.setOnMenuItemClickListener(menuItem -> { final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); - if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) { - trackSelector.setRendererDisabled(textRendererIndex, true); + if (textRendererIndex != RENDERER_UNAVAILABLE) { + trackSelector.setParameters(trackSelector.buildUponParameters() + .setRendererDisabled(textRendererIndex, true)); } return true; }); @@ -310,68 +305,61 @@ public abstract class VideoPlayer extends BasePlayer i + 1, Menu.NONE, captionLanguage); captionItem.setOnMenuItemClickListener(menuItem -> { final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); - if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) { + if (textRendererIndex != RENDERER_UNAVAILABLE) { trackSelector.setPreferredTextLanguage(captionLanguage); - trackSelector.setRendererDisabled(textRendererIndex, false); + trackSelector.setParameters(trackSelector.buildUponParameters() + .setRendererDisabled(textRendererIndex, false)); } return true; }); } captionPopupMenu.setOnDismissListener(this); } - /*////////////////////////////////////////////////////////////////////////// - // Playback Listener - //////////////////////////////////////////////////////////////////////////*/ - protected abstract int getDefaultResolutionIndex(final List sortedVideos); - protected abstract int getOverrideResolutionIndex(final List sortedVideos, final String playbackQuality); + private void updateStreamRelatedViews() { + if (getCurrentMetadata() == null) return; + + final MediaSourceTag tag = getCurrentMetadata(); + final StreamInfo metadata = tag.getMetadata(); - protected void onMetadataChanged(@NonNull final PlayQueueItem item, - @Nullable final StreamInfo info, - final int newPlayQueueIndex, - final boolean hasPlayQueueItemChanged) { qualityTextView.setVisibility(View.GONE); playbackSpeedTextView.setVisibility(View.GONE); playbackEndTime.setVisibility(View.GONE); playbackLiveSync.setVisibility(View.GONE); - final StreamType streamType = info == null ? StreamType.NONE : info.getStreamType(); - - switch (streamType) { + switch (metadata.getStreamType()) { case AUDIO_STREAM: surfaceView.setVisibility(View.GONE); + endScreen.setVisibility(View.VISIBLE); playbackEndTime.setVisibility(View.VISIBLE); break; case AUDIO_LIVE_STREAM: surfaceView.setVisibility(View.GONE); + endScreen.setVisibility(View.VISIBLE); playbackLiveSync.setVisibility(View.VISIBLE); break; case LIVE_STREAM: surfaceView.setVisibility(View.VISIBLE); + endScreen.setVisibility(View.GONE); playbackLiveSync.setVisibility(View.VISIBLE); break; case VIDEO_STREAM: - if (info.getVideoStreams().size() + info.getVideoOnlyStreams().size() == 0) break; - - final List videos = ListHelper.getSortedStreamVideosList(context, - info.getVideoStreams(), info.getVideoOnlyStreams(), false); - availableStreams = new ArrayList<>(videos); - if (playbackQuality == null) { - selectedStreamIndex = getDefaultResolutionIndex(videos); - } else { - selectedStreamIndex = getOverrideResolutionIndex(videos, getPlaybackQuality()); - } + if (metadata.getVideoStreams().size() + metadata.getVideoOnlyStreams().size() == 0) + break; + availableStreams = tag.getSortedAvailableVideoStreams(); + selectedStreamIndex = tag.getSelectedVideoStreamIndex(); buildQualityMenu(); - qualityTextView.setVisibility(View.VISIBLE); + qualityTextView.setVisibility(View.VISIBLE); surfaceView.setVisibility(View.VISIBLE); default: + endScreen.setVisibility(View.GONE); playbackEndTime.setVisibility(View.VISIBLE); break; } @@ -379,69 +367,21 @@ public abstract class VideoPlayer extends BasePlayer buildPlaybackSpeedMenu(); playbackSpeedTextView.setVisibility(View.VISIBLE); } + /*////////////////////////////////////////////////////////////////////////// + // Playback Listener + //////////////////////////////////////////////////////////////////////////*/ + + protected abstract VideoPlaybackResolver.QualityResolver getQualityResolver(); + + protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { + super.onMetadataChanged(tag); + updateStreamRelatedViews(); + } @Override @Nullable public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { - final MediaSource liveSource = super.sourceOf(item, info); - if (liveSource != null) return liveSource; - - List mediaSources = new ArrayList<>(); - - // Create video stream source - final List videos = ListHelper.getSortedStreamVideosList(context, - info.getVideoStreams(), info.getVideoOnlyStreams(), false); - final int index; - if (videos.isEmpty()) { - index = -1; - } else if (playbackQuality == null) { - index = getDefaultResolutionIndex(videos); - } else { - index = getOverrideResolutionIndex(videos, getPlaybackQuality()); - } - final VideoStream video = index >= 0 && index < videos.size() ? videos.get(index) : null; - if (video != null) { - final MediaSource streamSource = buildMediaSource(video.getUrl(), - PlayerHelper.cacheKeyOf(info, video), - MediaFormat.getSuffixById(video.getFormatId())); - mediaSources.add(streamSource); - } - - // Create optional audio stream source - final List audioStreams = info.getAudioStreams(); - final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get( - ListHelper.getDefaultAudioFormat(context, audioStreams)); - // Use the audio stream if there is no video stream, or - // Merge with audio stream in case if video does not contain audio - if (audio != null && ((video != null && video.isVideoOnly) || video == null)) { - final MediaSource audioSource = buildMediaSource(audio.getUrl(), - PlayerHelper.cacheKeyOf(info, audio), - MediaFormat.getSuffixById(audio.getFormatId())); - mediaSources.add(audioSource); - } - - // If there is no audio or video sources, then this media source cannot be played back - if (mediaSources.isEmpty()) return null; - // Below are auxiliary media sources - - // Create subtitle sources - for (final Subtitles subtitle : info.getSubtitles()) { - final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType()); - if (mimeType == null) continue; - - final Format textFormat = Format.createTextSampleFormat(null, mimeType, - SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle)); - final MediaSource textSource = dataSource.getSampleMediaSourceFactory() - .createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET); - mediaSources.add(textSource); - } - - if (mediaSources.size() == 1) { - return mediaSources.get(0); - } else { - return new MergingMediaSource(mediaSources.toArray( - new MediaSource[mediaSources.size()])); - } + return resolver.resolve(info); } /*////////////////////////////////////////////////////////////////////////// @@ -460,7 +400,6 @@ public abstract class VideoPlayer extends BasePlayer if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); - animateView(endScreen, false, 0); loadingPanel.setBackgroundColor(Color.BLACK); animateView(loadingPanel, true, 0); animateView(surfaceForeground, true, 100); @@ -470,6 +409,8 @@ public abstract class VideoPlayer extends BasePlayer public void onPlaying() { super.onPlaying(); + updateStreamRelatedViews(); + showAndAnimateControl(-1, true); playbackSeekBar.setEnabled(true); @@ -480,14 +421,12 @@ public abstract class VideoPlayer extends BasePlayer loadingPanel.setVisibility(View.GONE); animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); - animateView(endScreen, false, 0); } @Override public void onBuffering() { if (DEBUG) Log.d(TAG, "onBuffering() called"); loadingPanel.setBackgroundColor(Color.TRANSPARENT); - animateView(loadingPanel, true, 500); } @Override @@ -552,8 +491,7 @@ public abstract class VideoPlayer extends BasePlayer final int textRenderer = getRendererIndex(C.TRACK_TYPE_TEXT); if (captionTextView == null) return; - if (trackSelector == null || trackSelector.getCurrentMappedTrackInfo() == null || - textRenderer == RENDERER_UNAVAILABLE) { + if (trackSelector.getCurrentMappedTrackInfo() == null || textRenderer == RENDERER_UNAVAILABLE) { captionTextView.setVisibility(View.GONE); return; } @@ -575,8 +513,8 @@ public abstract class VideoPlayer extends BasePlayer // Build UI buildCaptionMenu(availableLanguages); - if (trackSelector.getRendererDisabled(textRenderer) || preferredLanguage == null || - !availableLanguages.contains(preferredLanguage)) { + if (trackSelector.getParameters().getRendererDisabled(textRenderer) || + preferredLanguage == null || !availableLanguages.contains(preferredLanguage)) { captionTextView.setText(R.string.caption_none); } else { captionTextView.setText(preferredLanguage); @@ -905,11 +843,12 @@ public abstract class VideoPlayer extends BasePlayer //////////////////////////////////////////////////////////////////////////*/ public void setPlaybackQuality(final String quality) { - this.playbackQuality = quality; + this.resolver.setPlaybackQuality(quality); } + @Nullable public String getPlaybackQuality() { - return playbackQuality; + return resolver.getPlaybackQuality(); } public AspectRatioFrameLayout getAspectRatioFrameLayout() { diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java index b174ed3ed..63c0bf333 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java @@ -39,10 +39,13 @@ public class MediaSessionManager { return MediaButtonReceiver.handleIntent(mediaSession, intent); } + /** + * Should be called on player destruction to prevent leakage. + * */ public void dispose() { this.sessionConnector.setPlayer(null, null); this.sessionConnector.setQueueNavigator(null); this.mediaSession.setActive(false); this.mediaSession.release(); - } + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java index 7c7d87791..d6453f579 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java @@ -21,25 +21,34 @@ import static org.schabi.newpipe.player.BasePlayer.DEBUG; public class PlaybackParameterDialog extends DialogFragment { @NonNull private static final String TAG = "PlaybackParameterDialog"; - public static final double MINIMUM_PLAYBACK_VALUE = 0.25f; + // Minimum allowable range in ExoPlayer + public static final double MINIMUM_PLAYBACK_VALUE = 0.10f; public static final double MAXIMUM_PLAYBACK_VALUE = 3.00f; public static final char STEP_UP_SIGN = '+'; public static final char STEP_DOWN_SIGN = '-'; - public static final double PLAYBACK_STEP_VALUE = 0.05f; - public static final double NIGHTCORE_TEMPO = 1.20f; - public static final double NIGHTCORE_PITCH_LOWER = 1.15f; - public static final double NIGHTCORE_PITCH_UPPER = 1.25f; + public static final double STEP_ONE_PERCENT_VALUE = 0.01f; + public static final double STEP_FIVE_PERCENT_VALUE = 0.05f; + public static final double STEP_TEN_PERCENT_VALUE = 0.10f; + public static final double STEP_TWENTY_FIVE_PERCENT_VALUE = 0.25f; + public static final double STEP_ONE_HUNDRED_PERCENT_VALUE = 1.00f; public static final double DEFAULT_TEMPO = 1.00f; public static final double DEFAULT_PITCH = 1.00f; + public static final double DEFAULT_STEP = STEP_TWENTY_FIVE_PERCENT_VALUE; + public static final boolean DEFAULT_SKIP_SILENCE = false; @NonNull private static final String INITIAL_TEMPO_KEY = "initial_tempo_key"; @NonNull private static final String INITIAL_PITCH_KEY = "initial_pitch_key"; + @NonNull private static final String TEMPO_KEY = "tempo_key"; + @NonNull private static final String PITCH_KEY = "pitch_key"; + @NonNull private static final String STEP_SIZE_KEY = "step_size_key"; + public interface Callback { - void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch); + void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch, + final boolean playbackSkipSilence); } @Nullable private Callback callback; @@ -50,6 +59,11 @@ public class PlaybackParameterDialog extends DialogFragment { private double initialTempo = DEFAULT_TEMPO; private double initialPitch = DEFAULT_PITCH; + private boolean initialSkipSilence = DEFAULT_SKIP_SILENCE; + + private double tempo = DEFAULT_TEMPO; + private double pitch = DEFAULT_PITCH; + private double stepSize = DEFAULT_STEP; @Nullable private SeekBar tempoSlider; @Nullable private TextView tempoMinimumText; @@ -65,16 +79,26 @@ public class PlaybackParameterDialog extends DialogFragment { @Nullable private TextView pitchStepDownText; @Nullable private TextView pitchStepUpText; - @Nullable private CheckBox unhookingCheckbox; + @Nullable private TextView stepSizeOnePercentText; + @Nullable private TextView stepSizeFivePercentText; + @Nullable private TextView stepSizeTenPercentText; + @Nullable private TextView stepSizeTwentyFivePercentText; + @Nullable private TextView stepSizeOneHundredPercentText; - @Nullable private TextView nightCorePresetText; - @Nullable private TextView resetPresetText; + @Nullable private CheckBox unhookingCheckbox; + @Nullable private CheckBox skipSilenceCheckbox; public static PlaybackParameterDialog newInstance(final double playbackTempo, - final double playbackPitch) { + final double playbackPitch, + final boolean playbackSkipSilence) { PlaybackParameterDialog dialog = new PlaybackParameterDialog(); dialog.initialTempo = playbackTempo; dialog.initialPitch = playbackPitch; + + dialog.tempo = playbackTempo; + dialog.pitch = playbackPitch; + + dialog.initialSkipSilence = playbackSkipSilence; return dialog; } @@ -98,6 +122,10 @@ public class PlaybackParameterDialog extends DialogFragment { if (savedInstanceState != null) { initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO); initialPitch = savedInstanceState.getDouble(INITIAL_PITCH_KEY, DEFAULT_PITCH); + + tempo = savedInstanceState.getDouble(TEMPO_KEY, DEFAULT_TEMPO); + pitch = savedInstanceState.getDouble(PITCH_KEY, DEFAULT_PITCH); + stepSize = savedInstanceState.getDouble(STEP_SIZE_KEY, DEFAULT_STEP); } } @@ -106,6 +134,10 @@ public class PlaybackParameterDialog extends DialogFragment { super.onSaveInstanceState(outState); outState.putDouble(INITIAL_TEMPO_KEY, initialTempo); outState.putDouble(INITIAL_PITCH_KEY, initialPitch); + + outState.putDouble(TEMPO_KEY, getCurrentTempo()); + outState.putDouble(PITCH_KEY, getCurrentPitch()); + outState.putDouble(STEP_SIZE_KEY, getCurrentStepSize()); } /*////////////////////////////////////////////////////////////////////////// @@ -123,7 +155,9 @@ public class PlaybackParameterDialog extends DialogFragment { .setView(view) .setCancelable(true) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> - setPlaybackParameters(initialTempo, initialPitch)) + setPlaybackParameters(initialTempo, initialPitch, initialSkipSilence)) + .setNeutralButton(R.string.playback_reset, (dialogInterface, i) -> + setPlaybackParameters(DEFAULT_TEMPO, DEFAULT_PITCH, DEFAULT_SKIP_SILENCE)) .setPositiveButton(R.string.finish, (dialogInterface, i) -> setCurrentPlaybackParameters()); @@ -136,9 +170,13 @@ public class PlaybackParameterDialog extends DialogFragment { private void setupControlViews(@NonNull View rootView) { setupHookingControl(rootView); + setupSkipSilenceControl(rootView); + setupTempoControl(rootView); setupPitchControl(rootView); - setupPresetControl(rootView); + + changeStepSize(stepSize); + setupStepSizeSelector(rootView); } private void setupTempoControl(@NonNull View rootView) { @@ -150,31 +188,15 @@ public class PlaybackParameterDialog extends DialogFragment { tempoStepDownText = rootView.findViewById(R.id.tempoStepDown); if (tempoCurrentText != null) - tempoCurrentText.setText(PlayerHelper.formatSpeed(initialTempo)); + tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo)); if (tempoMaximumText != null) tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE)); if (tempoMinimumText != null) tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE)); - if (tempoStepUpText != null) { - tempoStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); - tempoStepUpText.setOnClickListener(view -> { - onTempoSliderUpdated(getCurrentTempo() + PLAYBACK_STEP_VALUE); - setCurrentPlaybackParameters(); - }); - } - - if (tempoStepDownText != null) { - tempoStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); - tempoStepDownText.setOnClickListener(view -> { - onTempoSliderUpdated(getCurrentTempo() - PLAYBACK_STEP_VALUE); - setCurrentPlaybackParameters(); - }); - } - if (tempoSlider != null) { tempoSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE)); - tempoSlider.setProgress(strategy.progressOf(initialTempo)); + tempoSlider.setProgress(strategy.progressOf(tempo)); tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener()); } } @@ -188,31 +210,15 @@ public class PlaybackParameterDialog extends DialogFragment { pitchStepUpText = rootView.findViewById(R.id.pitchStepUp); if (pitchCurrentText != null) - pitchCurrentText.setText(PlayerHelper.formatPitch(initialPitch)); + pitchCurrentText.setText(PlayerHelper.formatPitch(pitch)); if (pitchMaximumText != null) pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE)); if (pitchMinimumText != null) pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE)); - if (pitchStepUpText != null) { - pitchStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); - pitchStepUpText.setOnClickListener(view -> { - onPitchSliderUpdated(getCurrentPitch() + PLAYBACK_STEP_VALUE); - setCurrentPlaybackParameters(); - }); - } - - if (pitchStepDownText != null) { - pitchStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); - pitchStepDownText.setOnClickListener(view -> { - onPitchSliderUpdated(getCurrentPitch() - PLAYBACK_STEP_VALUE); - setCurrentPlaybackParameters(); - }); - } - if (pitchSlider != null) { pitchSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE)); - pitchSlider.setProgress(strategy.progressOf(initialPitch)); + pitchSlider.setProgress(strategy.progressOf(pitch)); pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener()); } } @@ -220,7 +226,7 @@ public class PlaybackParameterDialog extends DialogFragment { private void setupHookingControl(@NonNull View rootView) { unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox); if (unhookingCheckbox != null) { - unhookingCheckbox.setChecked(initialPitch != initialTempo); + unhookingCheckbox.setChecked(pitch != tempo); unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> { if (isChecked) return; // When unchecked, slide back to the minimum of current tempo or pitch @@ -231,24 +237,84 @@ public class PlaybackParameterDialog extends DialogFragment { } } - private void setupPresetControl(@NonNull View rootView) { - nightCorePresetText = rootView.findViewById(R.id.presetNightcore); - if (nightCorePresetText != null) { - nightCorePresetText.setOnClickListener(view -> { - final double randomPitch = NIGHTCORE_PITCH_LOWER + - Math.random() * (NIGHTCORE_PITCH_UPPER - NIGHTCORE_PITCH_LOWER); + private void setupSkipSilenceControl(@NonNull View rootView) { + skipSilenceCheckbox = rootView.findViewById(R.id.skipSilenceCheckbox); + if (skipSilenceCheckbox != null) { + skipSilenceCheckbox.setChecked(initialSkipSilence); + skipSilenceCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> + setCurrentPlaybackParameters()); + } + } - setTempoSlider(NIGHTCORE_TEMPO); - setPitchSlider(randomPitch); + private void setupStepSizeSelector(@NonNull final View rootView) { + stepSizeOnePercentText = rootView.findViewById(R.id.stepSizeOnePercent); + stepSizeFivePercentText = rootView.findViewById(R.id.stepSizeFivePercent); + stepSizeTenPercentText = rootView.findViewById(R.id.stepSizeTenPercent); + stepSizeTwentyFivePercentText = rootView.findViewById(R.id.stepSizeTwentyFivePercent); + stepSizeOneHundredPercentText = rootView.findViewById(R.id.stepSizeOneHundredPercent); + + if (stepSizeOnePercentText != null) { + stepSizeOnePercentText.setText(getPercentString(STEP_ONE_PERCENT_VALUE)); + stepSizeOnePercentText.setOnClickListener(view -> + changeStepSize(STEP_ONE_PERCENT_VALUE)); + } + + if (stepSizeFivePercentText != null) { + stepSizeFivePercentText.setText(getPercentString(STEP_FIVE_PERCENT_VALUE)); + stepSizeFivePercentText.setOnClickListener(view -> + changeStepSize(STEP_FIVE_PERCENT_VALUE)); + } + + if (stepSizeTenPercentText != null) { + stepSizeTenPercentText.setText(getPercentString(STEP_TEN_PERCENT_VALUE)); + stepSizeTenPercentText.setOnClickListener(view -> + changeStepSize(STEP_TEN_PERCENT_VALUE)); + } + + if (stepSizeTwentyFivePercentText != null) { + stepSizeTwentyFivePercentText.setText(getPercentString(STEP_TWENTY_FIVE_PERCENT_VALUE)); + stepSizeTwentyFivePercentText.setOnClickListener(view -> + changeStepSize(STEP_TWENTY_FIVE_PERCENT_VALUE)); + } + + if (stepSizeOneHundredPercentText != null) { + stepSizeOneHundredPercentText.setText(getPercentString(STEP_ONE_HUNDRED_PERCENT_VALUE)); + stepSizeOneHundredPercentText.setOnClickListener(view -> + changeStepSize(STEP_ONE_HUNDRED_PERCENT_VALUE)); + } + } + + private void changeStepSize(final double stepSize) { + this.stepSize = stepSize; + + if (tempoStepUpText != null) { + tempoStepUpText.setText(getStepUpPercentString(stepSize)); + tempoStepUpText.setOnClickListener(view -> { + onTempoSliderUpdated(getCurrentTempo() + stepSize); setCurrentPlaybackParameters(); }); } - resetPresetText = rootView.findViewById(R.id.presetReset); - if (resetPresetText != null) { - resetPresetText.setOnClickListener(view -> { - setTempoSlider(DEFAULT_TEMPO); - setPitchSlider(DEFAULT_PITCH); + if (tempoStepDownText != null) { + tempoStepDownText.setText(getStepDownPercentString(stepSize)); + tempoStepDownText.setOnClickListener(view -> { + onTempoSliderUpdated(getCurrentTempo() - stepSize); + setCurrentPlaybackParameters(); + }); + } + + if (pitchStepUpText != null) { + pitchStepUpText.setText(getStepUpPercentString(stepSize)); + pitchStepUpText.setOnClickListener(view -> { + onPitchSliderUpdated(getCurrentPitch() + stepSize); + setCurrentPlaybackParameters(); + }); + } + + if (pitchStepDownText != null) { + pitchStepDownText.setText(getStepDownPercentString(stepSize)); + pitchStepDownText.setOnClickListener(view -> { + onPitchSliderUpdated(getCurrentPitch() - stepSize); setCurrentPlaybackParameters(); }); } @@ -342,10 +408,11 @@ public class PlaybackParameterDialog extends DialogFragment { //////////////////////////////////////////////////////////////////////////*/ private void setCurrentPlaybackParameters() { - setPlaybackParameters(getCurrentTempo(), getCurrentPitch()); + setPlaybackParameters(getCurrentTempo(), getCurrentPitch(), getCurrentSkipSilence()); } - private void setPlaybackParameters(final double tempo, final double pitch) { + private void setPlaybackParameters(final double tempo, final double pitch, + final boolean skipSilence) { if (callback != null && tempoCurrentText != null && pitchCurrentText != null) { if (DEBUG) Log.d(TAG, "Setting playback parameters to " + "tempo=[" + tempo + "], " + @@ -353,27 +420,40 @@ public class PlaybackParameterDialog extends DialogFragment { tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo)); pitchCurrentText.setText(PlayerHelper.formatPitch(pitch)); - callback.onPlaybackParameterChanged((float) tempo, (float) pitch); + callback.onPlaybackParameterChanged((float) tempo, (float) pitch, skipSilence); } } private double getCurrentTempo() { - return tempoSlider == null ? initialTempo : strategy.valueOf( + return tempoSlider == null ? tempo : strategy.valueOf( tempoSlider.getProgress()); } private double getCurrentPitch() { - return pitchSlider == null ? initialPitch : strategy.valueOf( + return pitchSlider == null ? pitch : strategy.valueOf( pitchSlider.getProgress()); } + private double getCurrentStepSize() { + return stepSize; + } + + private boolean getCurrentSkipSilence() { + return skipSilenceCheckbox != null && skipSilenceCheckbox.isChecked(); + } + @NonNull private static String getStepUpPercentString(final double percent) { - return STEP_UP_SIGN + PlayerHelper.formatPitch(percent); + return STEP_UP_SIGN + getPercentString(percent); } @NonNull private static String getStepDownPercentString(final double percent) { - return STEP_DOWN_SIGN + PlayerHelper.formatPitch(percent); + return STEP_DOWN_SIGN + getPercentString(percent); + } + + @NonNull + private static String getPercentString(final double percent) { + return PlayerHelper.formatPitch(percent); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index dbe0e9f46..275f488e3 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -4,6 +4,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import android.preference.PreferenceManager; +import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.view.accessibility.CaptioningManager; @@ -28,6 +29,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; +import java.lang.annotation.Retention; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.ArrayList; @@ -42,6 +44,8 @@ import java.util.concurrent.TimeUnit; import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FILL; import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT; import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM; +import static java.lang.annotation.RetentionPolicy.SOURCE; +import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.*; public class PlayerHelper { private PlayerHelper() {} @@ -51,6 +55,14 @@ public class PlayerHelper { private static final NumberFormat speedFormatter = new DecimalFormat("0.##x"); private static final NumberFormat pitchFormatter = new DecimalFormat("##%"); + @Retention(SOURCE) + @IntDef({MINIMIZE_ON_EXIT_MODE_NONE, MINIMIZE_ON_EXIT_MODE_BACKGROUND, + MINIMIZE_ON_EXIT_MODE_POPUP}) + public @interface MinimizeMode { + int MINIMIZE_ON_EXIT_MODE_NONE = 0; + int MINIMIZE_ON_EXIT_MODE_BACKGROUND = 1; + int MINIMIZE_ON_EXIT_MODE_POPUP = 2; + } //////////////////////////////////////////////////////////////////////////// // Exposed helpers //////////////////////////////////////////////////////////////////////////// @@ -173,6 +185,22 @@ public class PlayerHelper { return isAutoQueueEnabled(context, false); } + @MinimizeMode + public static int getMinimizeOnExitAction(@NonNull final Context context) { + final String defaultAction = context.getString(R.string.minimize_on_exit_none_key); + final String popupAction = context.getString(R.string.minimize_on_exit_popup_key); + final String backgroundAction = context.getString(R.string.minimize_on_exit_background_key); + + final String action = getMinimizeOnExitAction(context, defaultAction); + if (action.equals(popupAction)) { + return MINIMIZE_ON_EXIT_MODE_POPUP; + } else if (action.equals(backgroundAction)) { + return MINIMIZE_ON_EXIT_MODE_BACKGROUND; + } else { + return MINIMIZE_ON_EXIT_MODE_NONE; + } + } + @NonNull public static SeekParameters getSeekParameters(@NonNull final Context context) { return isUsingInexactSeek(context, false) ? @@ -213,7 +241,6 @@ public class PlayerHelper { public static TrackSelection.Factory getQualitySelector(@NonNull final Context context, @NonNull final BandwidthMeter meter) { return new AdaptiveTrackSelection.Factory(meter, - AdaptiveTrackSelection.DEFAULT_MAX_INITIAL_BITRATE, /*bufferDurationRequiredForQualityIncrease=*/1000, AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, @@ -225,7 +252,7 @@ public class PlayerHelper { } public static int getShutdownFlingVelocity(@NonNull final Context context) { - return 10000; + return 6000; } public static int getTossFlingVelocity(@NonNull final Context context) { @@ -249,7 +276,6 @@ public class PlayerHelper { * System font scaling: * Very small - 0.25f, Small - 0.5f, Normal - 1.0f, Large - 1.5f, Very Large - 2.0f * */ - @NonNull public static float getCaptionScale(@NonNull final Context context) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return 1f; @@ -322,4 +348,10 @@ public class PlayerHelper { return sp.getFloat(context.getString(R.string.screen_brightness_key), screenBrightness); } } + + private static String getMinimizeOnExitAction(@NonNull final Context context, + final String key) { + return getPreferences(context).getString(context.getString(R.string.minimize_on_exit_key), + key); + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java index 8d498a9bf..2f233c464 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java @@ -4,6 +4,7 @@ import android.support.annotation.NonNull; import android.util.Log; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.upstream.Allocator; @@ -11,7 +12,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem; import java.io.IOException; -public class FailedMediaSource implements ManagedMediaSource { +public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSource { private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode()); public static class FailedMediaSourceException extends Exception { @@ -72,11 +73,6 @@ public class FailedMediaSource implements ManagedMediaSource { return System.currentTimeMillis() >= retryTimestamp; } - @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - Log.e(TAG, "Loading failed source: ", error); - } - @Override public void maybeThrowSourceInfoRefreshError() throws IOException { throw new IOException(error); @@ -90,8 +86,14 @@ public class FailedMediaSource implements ManagedMediaSource { @Override public void releasePeriod(MediaPeriod mediaPeriod) {} + @Override - public void releaseSource() {} + protected void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { + Log.e(TAG, "Loading failed source: ", error); + } + + @Override + protected void releaseSourceInternal() {} @Override public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity, diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java index 1a9cfeb4d..c39b0a03d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java @@ -1,10 +1,12 @@ package org.schabi.newpipe.player.mediasource; +import android.os.Handler; import android.support.annotation.NonNull; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.upstream.Allocator; import org.schabi.newpipe.player.playqueue.PlayQueueItem; @@ -34,7 +36,8 @@ public class LoadedMediaSource implements ManagedMediaSource { } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, + SourceInfoRefreshListener listener) { source.prepareSource(player, isTopLevelSource, listener); } @@ -54,8 +57,18 @@ public class LoadedMediaSource implements ManagedMediaSource { } @Override - public void releaseSource() { - source.releaseSource(); + public void releaseSource(SourceInfoRefreshListener listener) { + source.releaseSource(listener); + } + + @Override + public void addEventListener(Handler handler, MediaSourceEventListener eventListener) { + source.addEventListener(handler, eventListener); + } + + @Override + public void removeEventListener(MediaSourceEventListener eventListener) { + source.removeEventListener(eventListener); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java index 310f1062b..5fe107657 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java @@ -3,14 +3,14 @@ package org.schabi.newpipe.player.mediasource; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; +import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ShuffleOrder; public class ManagedMediaSourcePlaylist { - @NonNull private final DynamicConcatenatingMediaSource internalSource; + @NonNull private final ConcatenatingMediaSource internalSource; public ManagedMediaSourcePlaylist() { - internalSource = new DynamicConcatenatingMediaSource(/*isPlaylistAtomic=*/false, + internalSource = new ConcatenatingMediaSource(/*isPlaylistAtomic=*/false, new ShuffleOrder.UnshuffledShuffleOrder(0)); } @@ -32,12 +32,8 @@ public class ManagedMediaSourcePlaylist { null : (ManagedMediaSource) internalSource.getMediaSource(index); } - public void dispose() { - internalSource.releaseSource(); - } - @NonNull - public DynamicConcatenatingMediaSource getParentMediaSource() { + public ConcatenatingMediaSource getParentMediaSource() { return internalSource; } @@ -46,7 +42,7 @@ public class ManagedMediaSourcePlaylist { //////////////////////////////////////////////////////////////////////////*/ /** - * Expands the {@link DynamicConcatenatingMediaSource} by appending it with a + * Expands the {@link ConcatenatingMediaSource} by appending it with a * {@link PlaceholderMediaSource}. * * @see #append(ManagedMediaSource) @@ -56,17 +52,17 @@ public class ManagedMediaSourcePlaylist { } /** - * Appends a {@link ManagedMediaSource} to the end of {@link DynamicConcatenatingMediaSource}. - * @see DynamicConcatenatingMediaSource#addMediaSource + * Appends a {@link ManagedMediaSource} to the end of {@link ConcatenatingMediaSource}. + * @see ConcatenatingMediaSource#addMediaSource * */ public synchronized void append(@NonNull final ManagedMediaSource source) { internalSource.addMediaSource(source); } /** - * Removes a {@link ManagedMediaSource} from {@link DynamicConcatenatingMediaSource} + * Removes a {@link ManagedMediaSource} from {@link ConcatenatingMediaSource} * at the given index. If this index is out of bound, then the removal is ignored. - * @see DynamicConcatenatingMediaSource#removeMediaSource(int) + * @see ConcatenatingMediaSource#removeMediaSource(int) * */ public synchronized void remove(final int index) { if (index < 0 || index > internalSource.getSize()) return; @@ -75,10 +71,10 @@ public class ManagedMediaSourcePlaylist { } /** - * Moves a {@link ManagedMediaSource} in {@link DynamicConcatenatingMediaSource} + * Moves a {@link ManagedMediaSource} in {@link ConcatenatingMediaSource} * from the given source index to the target index. If either index is out of bound, * then the call is ignored. - * @see DynamicConcatenatingMediaSource#moveMediaSource(int, int) + * @see ConcatenatingMediaSource#moveMediaSource(int, int) * */ public synchronized void move(final int source, final int target) { if (source < 0 || target < 0) return; @@ -99,7 +95,7 @@ public class ManagedMediaSourcePlaylist { } /** - * Updates the {@link ManagedMediaSource} in {@link DynamicConcatenatingMediaSource} + * Updates the {@link ManagedMediaSource} in {@link ConcatenatingMediaSource} * at the given index with a given {@link ManagedMediaSource}. * @see #update(int, ManagedMediaSource, Runnable) * */ @@ -108,11 +104,11 @@ public class ManagedMediaSourcePlaylist { } /** - * Updates the {@link ManagedMediaSource} in {@link DynamicConcatenatingMediaSource} + * Updates the {@link ManagedMediaSource} in {@link ConcatenatingMediaSource} * at the given index with a given {@link ManagedMediaSource}. If the index is out of bound, * then the replacement is ignored. - * @see DynamicConcatenatingMediaSource#addMediaSource - * @see DynamicConcatenatingMediaSource#removeMediaSource(int, Runnable) + * @see ConcatenatingMediaSource#addMediaSource + * @see ConcatenatingMediaSource#removeMediaSource(int, Runnable) * */ public synchronized void update(final int index, @NonNull final ManagedMediaSource source, @Nullable final Runnable finalizingAction) { diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java index 318f9a316..bfd734393 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java @@ -3,20 +3,19 @@ package org.schabi.newpipe.player.mediasource; import android.support.annotation.NonNull; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.upstream.Allocator; import org.schabi.newpipe.player.playqueue.PlayQueueItem; -import java.io.IOException; - -public class PlaceholderMediaSource implements ManagedMediaSource { +public class PlaceholderMediaSource extends BaseMediaSource implements ManagedMediaSource { // Do nothing, so this will stall the playback - @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {} - @Override public void maybeThrowSourceInfoRefreshError() throws IOException {} + @Override public void maybeThrowSourceInfoRefreshError() {} @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { return null; } @Override public void releasePeriod(MediaPeriod mediaPeriod) {} - @Override public void releaseSource() {} + @Override protected void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {} + @Override protected void releaseSourceInternal() {} @Override public boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity, diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index 8ab3cba98..b27dc3dd6 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -5,12 +5,10 @@ import android.support.annotation.Nullable; import android.support.v4.util.ArraySet; import android.util.Log; -import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.player.mediasource.FailedMediaSource; import org.schabi.newpipe.player.mediasource.LoadedMediaSource; import org.schabi.newpipe.player.mediasource.ManagedMediaSource; @@ -24,10 +22,8 @@ import org.schabi.newpipe.player.playqueue.events.RemoveEvent; import org.schabi.newpipe.player.playqueue.events.ReorderEvent; import org.schabi.newpipe.util.ServiceHelper; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -37,8 +33,6 @@ import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; -import io.reactivex.disposables.SerialDisposable; -import io.reactivex.functions.Consumer; import io.reactivex.internal.subscriptions.EmptySubscription; import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; @@ -104,7 +98,6 @@ public class MediaSourceManager { private final static int MAXIMUM_LOADER_SIZE = WINDOW_SIZE * 2 + 1; @NonNull private final CompositeDisposable loaderReactor; @NonNull private final Set loadingItems; - @NonNull private final SerialDisposable syncReactor; @NonNull private final AtomicBoolean isBlocked; @@ -144,7 +137,6 @@ public class MediaSourceManager { this.playQueueReactor = EmptySubscription.INSTANCE; this.loaderReactor = new CompositeDisposable(); - this.syncReactor = new SerialDisposable(); this.isBlocked = new AtomicBoolean(false); @@ -171,8 +163,6 @@ public class MediaSourceManager { playQueueReactor.cancel(); loaderReactor.dispose(); - syncReactor.dispose(); - playlist.dispose(); } /*////////////////////////////////////////////////////////////////////////// @@ -311,21 +301,7 @@ public class MediaSourceManager { final PlayQueueItem currentItem = playQueue.getItem(); if (isBlocked.get() || currentItem == null) return; - final Consumer onSuccess = info -> syncInternal(currentItem, info); - final Consumer onError = throwable -> syncInternal(currentItem, null); - - final Disposable sync = currentItem.getStream() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(onSuccess, onError); - syncReactor.set(sync); - } - - private void syncInternal(@NonNull final PlayQueueItem item, - @Nullable final StreamInfo info) { - // Ensure the current item is up to date with the play queue - if (playQueue.getItem() == item) { - playbackListener.onPlaybackSynchronize(item, info); - } + playbackListener.onPlaybackSynchronize(currentItem); } private synchronized void maybeSynchronizePlayer() { @@ -424,7 +400,8 @@ public class MediaSourceManager { } /** - * Checks if the corresponding MediaSource in {@link DynamicConcatenatingMediaSource} + * Checks if the corresponding MediaSource in + * {@link com.google.android.exoplayer2.source.ConcatenatingMediaSource} * for a given {@link PlayQueueItem} needs replacement, either due to gapless playback * readiness or playlist desynchronization. *

@@ -481,8 +458,6 @@ public class MediaSourceManager { private void resetSources() { if (DEBUG) Log.d(TAG, "resetSources() called."); - - playlist.dispose(); playlist = new ManagedMediaSourcePlaylist(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java index 4dcb30aa3..238bdfcd0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java @@ -45,7 +45,7 @@ public interface PlaybackListener { * * May be called anytime at any amount once unblock is called. * */ - void onPlaybackSynchronize(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info); + void onPlaybackSynchronize(@NonNull final PlayQueueItem item); /** * Requests the listener to resolve a stream info into a media source diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java new file mode 100644 index 000000000..6bb556850 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java @@ -0,0 +1,41 @@ +package org.schabi.newpipe.player.resolver; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.google.android.exoplayer2.source.MediaSource; + +import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.player.helper.PlayerDataSource; +import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.util.ListHelper; + +public class AudioPlaybackResolver implements PlaybackResolver { + + @NonNull private final Context context; + @NonNull private final PlayerDataSource dataSource; + + public AudioPlaybackResolver(@NonNull final Context context, + @NonNull final PlayerDataSource dataSource) { + this.context = context; + this.dataSource = dataSource; + } + + @Override + @Nullable + public MediaSource resolve(@NonNull StreamInfo info) { + final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info); + if (liveSource != null) return liveSource; + + final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams()); + if (index < 0 || index >= info.getAudioStreams().size()) return null; + + final AudioStream audio = info.getAudioStreams().get(index); + final MediaSourceTag tag = new MediaSourceTag(info); + return buildMediaSource(dataSource, audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio), + MediaFormat.getSuffixById(audio.getFormatId()), tag); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/MediaSourceTag.java b/app/src/main/java/org/schabi/newpipe/player/resolver/MediaSourceTag.java new file mode 100644 index 000000000..bbe5d33ca --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/MediaSourceTag.java @@ -0,0 +1,51 @@ +package org.schabi.newpipe.player.resolver; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.VideoStream; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; + +public class MediaSourceTag implements Serializable { + @NonNull private final StreamInfo metadata; + + @NonNull private final List sortedAvailableVideoStreams; + private final int selectedVideoStreamIndex; + + public MediaSourceTag(@NonNull final StreamInfo metadata, + @NonNull final List sortedAvailableVideoStreams, + final int selectedVideoStreamIndex) { + this.metadata = metadata; + this.sortedAvailableVideoStreams = sortedAvailableVideoStreams; + this.selectedVideoStreamIndex = selectedVideoStreamIndex; + } + + public MediaSourceTag(@NonNull final StreamInfo metadata) { + this(metadata, Collections.emptyList(), /*indexNotAvailable=*/-1); + } + + @NonNull + public StreamInfo getMetadata() { + return metadata; + } + + @NonNull + public List getSortedAvailableVideoStreams() { + return sortedAvailableVideoStreams; + } + + public int getSelectedVideoStreamIndex() { + return selectedVideoStreamIndex; + } + + @Nullable + public VideoStream getSelectedVideoStream() { + return selectedVideoStreamIndex < 0 || + selectedVideoStreamIndex >= sortedAvailableVideoStreams.size() ? null : + sortedAvailableVideoStreams.get(selectedVideoStreamIndex); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java new file mode 100644 index 000000000..1da3ec211 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java @@ -0,0 +1,84 @@ +package org.schabi.newpipe.player.resolver; + +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.util.Util; + +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.player.helper.PlayerDataSource; + +public interface PlaybackResolver extends Resolver { + + @Nullable + default MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource dataSource, + @NonNull final StreamInfo info) { + final StreamType streamType = info.getStreamType(); + if (!(streamType == StreamType.AUDIO_LIVE_STREAM || streamType == StreamType.LIVE_STREAM)) { + return null; + } + + final MediaSourceTag tag = new MediaSourceTag(info); + if (!info.getHlsUrl().isEmpty()) { + return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.TYPE_HLS, tag); + } else if (!info.getDashMpdUrl().isEmpty()) { + return buildLiveMediaSource(dataSource, info.getDashMpdUrl(), C.TYPE_DASH, tag); + } + + return null; + } + + @NonNull + default MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSource, + @NonNull final String sourceUrl, + @C.ContentType final int type, + @NonNull final MediaSourceTag metadata) { + final Uri uri = Uri.parse(sourceUrl); + switch (type) { + case C.TYPE_SS: + return dataSource.getLiveSsMediaSourceFactory().setTag(metadata) + .createMediaSource(uri); + case C.TYPE_DASH: + return dataSource.getLiveDashMediaSourceFactory().setTag(metadata) + .createMediaSource(uri); + case C.TYPE_HLS: + return dataSource.getLiveHlsMediaSourceFactory().setTag(metadata) + .createMediaSource(uri); + default: + throw new IllegalStateException("Unsupported type: " + type); + } + } + + @NonNull + default MediaSource buildMediaSource(@NonNull final PlayerDataSource dataSource, + @NonNull final String sourceUrl, + @NonNull final String cacheKey, + @NonNull final String overrideExtension, + @NonNull final MediaSourceTag metadata) { + final Uri uri = Uri.parse(sourceUrl); + @C.ContentType final int type = TextUtils.isEmpty(overrideExtension) ? + Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); + + switch (type) { + case C.TYPE_SS: + return dataSource.getLiveSsMediaSourceFactory().setTag(metadata) + .createMediaSource(uri); + case C.TYPE_DASH: + return dataSource.getDashMediaSourceFactory().setTag(metadata) + .createMediaSource(uri); + case C.TYPE_HLS: + return dataSource.getHlsMediaSourceFactory().setTag(metadata) + .createMediaSource(uri); + case C.TYPE_OTHER: + return dataSource.getExtractorMediaSourceFactory(cacheKey).setTag(metadata) + .createMediaSource(uri); + default: + throw new IllegalStateException("Unsupported type: " + type); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java new file mode 100644 index 000000000..4bd795574 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java @@ -0,0 +1,8 @@ +package org.schabi.newpipe.player.resolver; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +public interface Resolver { + @Nullable Product resolve(@NonNull Source source); +} diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java new file mode 100644 index 000000000..8f91f4886 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java @@ -0,0 +1,123 @@ +package org.schabi.newpipe.player.resolver; + +import android.content.Context; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MergingMediaSource; + +import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.Subtitles; +import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.player.helper.PlayerDataSource; +import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.util.ListHelper; + +import java.util.ArrayList; +import java.util.List; + +import static com.google.android.exoplayer2.C.SELECTION_FLAG_AUTOSELECT; +import static com.google.android.exoplayer2.C.TIME_UNSET; + +public class VideoPlaybackResolver implements PlaybackResolver { + + public interface QualityResolver { + int getDefaultResolutionIndex(final List sortedVideos); + int getOverrideResolutionIndex(final List sortedVideos, + final String playbackQuality); + } + + @NonNull private final Context context; + @NonNull private final PlayerDataSource dataSource; + @NonNull private final QualityResolver qualityResolver; + + @Nullable private String playbackQuality; + + public VideoPlaybackResolver(@NonNull final Context context, + @NonNull final PlayerDataSource dataSource, + @NonNull final QualityResolver qualityResolver) { + this.context = context; + this.dataSource = dataSource; + this.qualityResolver = qualityResolver; + } + + @Override + @Nullable + public MediaSource resolve(@NonNull StreamInfo info) { + final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info); + if (liveSource != null) return liveSource; + + List mediaSources = new ArrayList<>(); + + // Create video stream source + final List videos = ListHelper.getSortedStreamVideosList(context, + info.getVideoStreams(), info.getVideoOnlyStreams(), false); + final int index; + if (videos.isEmpty()) { + index = -1; + } else if (playbackQuality == null) { + index = qualityResolver.getDefaultResolutionIndex(videos); + } else { + index = qualityResolver.getOverrideResolutionIndex(videos, getPlaybackQuality()); + } + final MediaSourceTag tag = new MediaSourceTag(info, videos, index); + @Nullable final VideoStream video = tag.getSelectedVideoStream(); + + if (video != null) { + final MediaSource streamSource = buildMediaSource(dataSource, video.getUrl(), + PlayerHelper.cacheKeyOf(info, video), + MediaFormat.getSuffixById(video.getFormatId()), tag); + mediaSources.add(streamSource); + } + + // Create optional audio stream source + final List audioStreams = info.getAudioStreams(); + final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get( + ListHelper.getDefaultAudioFormat(context, audioStreams)); + // Use the audio stream if there is no video stream, or + // Merge with audio stream in case if video does not contain audio + if (audio != null && ((video != null && video.isVideoOnly) || video == null)) { + final MediaSource audioSource = buildMediaSource(dataSource, audio.getUrl(), + PlayerHelper.cacheKeyOf(info, audio), + MediaFormat.getSuffixById(audio.getFormatId()), tag); + mediaSources.add(audioSource); + } + + // If there is no audio or video sources, then this media source cannot be played back + if (mediaSources.isEmpty()) return null; + // Below are auxiliary media sources + + // Create subtitle sources + for (final Subtitles subtitle : info.getSubtitles()) { + final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType()); + if (mimeType == null) continue; + + final Format textFormat = Format.createTextSampleFormat(null, mimeType, + SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle)); + final MediaSource textSource = dataSource.getSampleMediaSourceFactory() + .createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET); + mediaSources.add(textSource); + } + + if (mediaSources.size() == 1) { + return mediaSources.get(0); + } else { + return new MergingMediaSource(mediaSources.toArray( + new MediaSource[mediaSources.size()])); + } + } + + @Nullable + public String getPlaybackQuality() { + return playbackQuality; + } + + public void setPlaybackQuality(@Nullable String playbackQuality) { + this.playbackQuality = playbackQuality; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index a02a9df34..8f4e6d471 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -56,6 +56,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment { private File databasesDir; private File newpipe_db; private File newpipe_db_journal; + private File newpipe_db_shm; + private File newpipe_db_wal; private File newpipe_settings; private String thumbnailLoadToggleKey; @@ -88,6 +90,9 @@ public class ContentSettingsFragment extends BasePreferenceFragment { databasesDir = new File(homeDir + "/databases"); newpipe_db = new File(homeDir + "/databases/newpipe.db"); newpipe_db_journal = new File(homeDir + "/databases/newpipe.db-journal"); + newpipe_db_shm = new File(homeDir + "/databases/newpipe.db-shm"); + newpipe_db_wal = new File(homeDir + "/databases/newpipe.db-wal"); + newpipe_settings = new File(homeDir + "/databases/newpipe.settings"); newpipe_settings.delete(); @@ -207,7 +212,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { new BufferedOutputStream( new FileOutputStream(path))); ZipHelper.addFileToZip(outZip, newpipe_db.getPath(), "newpipe.db"); - ZipHelper.addFileToZip(outZip, newpipe_db_journal.getPath(), "newpipe.db-journal"); + saveSharedPreferencesToFile(newpipe_settings); ZipHelper.addFileToZip(outZip, newpipe_settings.getPath(), "newpipe.settings"); @@ -263,8 +268,16 @@ public class ContentSettingsFragment extends BasePreferenceFragment { throw new Exception("Could not create databases dir"); } - if(!(ZipHelper.extractFileFromZip(filePath, newpipe_db.getPath(), "newpipe.db") - && ZipHelper.extractFileFromZip(filePath, newpipe_db_journal.getPath(), "newpipe.db-journal"))) { + final boolean isDbFileExtracted = ZipHelper.extractFileFromZip(filePath, + newpipe_db.getPath(), "newpipe.db"); + + if (isDbFileExtracted) { + newpipe_db_journal.delete(); + newpipe_db_wal.delete(); + newpipe_db_shm.delete(); + + } else { + Toast.makeText(getContext(), R.string.could_not_import_all_files, Toast.LENGTH_LONG) .show(); } diff --git a/app/src/main/java/org/schabi/newpipe/util/Constants.java b/app/src/main/java/org/schabi/newpipe/util/Constants.java index a6aec96e2..b01b6df6a 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Constants.java +++ b/app/src/main/java/org/schabi/newpipe/util/Constants.java @@ -6,7 +6,7 @@ public class Constants { public static final String KEY_TITLE = "key_title"; public static final String KEY_LINK_TYPE = "key_link_type"; public static final String KEY_OPEN_SEARCH = "key_open_search"; - public static final String KEY_QUERY = "key_query"; + public static final String KEY_SEARCH_STRING = "key_search_string"; public static final String KEY_THEME_CHANGE = "key_theme_change"; public static final String KEY_MAIN_PAGE_CHANGE = "key_main_page_change"; diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java index 1897589c6..74e2d4cd5 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -37,9 +37,8 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.kiosk.KioskInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; -import org.schabi.newpipe.extractor.search.SearchEngine; -import org.schabi.newpipe.extractor.search.SearchResult; -import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; +import org.schabi.newpipe.extractor.search.SearchInfo; +import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; @@ -50,7 +49,6 @@ import java.util.List; import io.reactivex.Maybe; import io.reactivex.Single; -import io.reactivex.annotations.NonNull; public final class ExtractorHelper { private static final String TAG = ExtractorHelper.class.getSimpleName(); @@ -66,29 +64,35 @@ public final class ExtractorHelper { } } - public static Single searchFor(final int serviceId, - final String query, - final int pageNumber, - final String contentCountry, - final SearchEngine.Filter filter) { + public static Single searchFor(final int serviceId, + final String searchString, + final List contentFilter, + final String sortFilter, + final String contentCountry) { checkServiceId(serviceId); return Single.fromCallable(() -> - SearchResult.getSearchResult(NewPipe.getService(serviceId).getSearchEngine(), - query, pageNumber, contentCountry, filter) - ); + SearchInfo.getInfo(NewPipe.getService(serviceId), + NewPipe.getService(serviceId) + .getSearchQIHFactory() + .fromQuery(searchString, contentFilter, sortFilter), + contentCountry)); } public static Single getMoreSearchItems(final int serviceId, - final String query, - final int nextPageNumber, - final String searchLanguage, - final SearchEngine.Filter filter) { + final String searchString, + final List contentFilter, + final String sortFilter, + final String pageUrl, + final String contentCountry) { checkServiceId(serviceId); - return searchFor(serviceId, query, nextPageNumber, searchLanguage, filter) - .map((@NonNull SearchResult searchResult) -> - new InfoItemsPage(searchResult.resultList, - nextPageNumber + "", - searchResult.errors)); + return Single.fromCallable(() -> + SearchInfo.getMoreItems(NewPipe.getService(serviceId), + NewPipe.getService(serviceId) + .getSearchQIHFactory() + .fromQuery(searchString, contentFilter, sortFilter), + contentCountry, + pageUrl)); + } public static Single> suggestionsFor(final int serviceId, @@ -233,7 +237,6 @@ public final class ExtractorHelper { serviceId == -1 ? "none" : NewPipe.getNameOfService(serviceId), url + (optionalErrorMessage == null ? "" : optionalErrorMessage), errorId)); } }); - } /** diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index ebbeb06f8..85367e2a5 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -26,10 +26,13 @@ import org.schabi.newpipe.download.DownloadActivity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.search.SearchExtractor; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; +import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler; import org.schabi.newpipe.fragments.MainFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.fragments.list.channel.ChannelFragment; @@ -100,11 +103,13 @@ public class NavigationHelper { final int repeatMode, final float playbackSpeed, final float playbackPitch, + final boolean playbackSkipSilence, @Nullable final String playbackQuality) { return getPlayerIntent(context, targetClazz, playQueue, playbackQuality) .putExtra(BasePlayer.REPEAT_MODE, repeatMode) .putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed) - .putExtra(BasePlayer.PLAYBACK_PITCH, playbackPitch); + .putExtra(BasePlayer.PLAYBACK_PITCH, playbackPitch) + .putExtra(BasePlayer.PLAYBACK_SKIP_SILENCE, playbackSkipSilence); } public static void playOnMainPlayer(final Context context, final PlayQueue queue) { @@ -281,9 +286,11 @@ public class NavigationHelper { return fragmentManager.popBackStackImmediate(SEARCH_FRAGMENT_TAG, 0); } - public static void openSearchFragment(FragmentManager fragmentManager, int serviceId, String query) { + public static void openSearchFragment(FragmentManager fragmentManager, + int serviceId, + String searchString) { defaultTransaction(fragmentManager) - .replace(R.id.fragment_holder, SearchFragment.getInstance(serviceId, query)) + .replace(R.id.fragment_holder, SearchFragment.getInstance(serviceId, searchString)) .addToBackStack(SEARCH_FRAGMENT_TAG) .commit(); } @@ -312,7 +319,11 @@ public class NavigationHelper { .commit(); } - public static void openChannelFragment(FragmentManager fragmentManager, int serviceId, String url, String name) { + public static void openChannelFragment( + FragmentManager fragmentManager, + int serviceId, + String url, + String name) { if (name == null) name = ""; defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, ChannelFragment.getInstance(serviceId, url, name)) @@ -320,7 +331,10 @@ public class NavigationHelper { .commit(); } - public static void openPlaylistFragment(FragmentManager fragmentManager, int serviceId, String url, String name) { + public static void openPlaylistFragment(FragmentManager fragmentManager, + int serviceId, + String url, + String name) { if (name == null) name = ""; defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, PlaylistFragment.getInstance(serviceId, url, name)) @@ -368,10 +382,10 @@ public class NavigationHelper { // Through Intents //////////////////////////////////////////////////////////////////////////*/ - public static void openSearch(Context context, int serviceId, String query) { + public static void openSearch(Context context, int serviceId, String searchString) { Intent mIntent = new Intent(context, MainActivity.class); mIntent.putExtra(Constants.KEY_SERVICE_ID, serviceId); - mIntent.putExtra(Constants.KEY_QUERY, query); + mIntent.putExtra(Constants.KEY_SEARCH_STRING, searchString); mIntent.putExtra(Constants.KEY_OPEN_SEARCH, true); context.startActivity(mIntent); } @@ -465,7 +479,8 @@ public class NavigationHelper { switch (linkType) { case STREAM: - rIntent.putExtra(VideoDetailFragment.AUTO_PLAY, PreferenceManager.getDefaultSharedPreferences(context) + rIntent.putExtra(VideoDetailFragment.AUTO_PLAY, + PreferenceManager.getDefaultSharedPreferences(context) .getBoolean(context.getString(R.string.autoplay_through_intent_key), false)); break; } diff --git a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java index d86f27f2f..7c781eb14 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java @@ -5,7 +5,6 @@ import android.preference.PreferenceManager; import android.support.annotation.DrawableRes; import android.support.annotation.StringRes; -import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.ServiceList; @@ -31,6 +30,18 @@ public class ServiceHelper { } } + public static String getTranslatedFilterString(String filter, Context c) { + switch(filter) { + case "all": return c.getString(R.string.all); + case "videos": return c.getString(R.string.videos); + case "channels": return c.getString(R.string.channels); + case "playlists": return c.getString(R.string.playlists); + case "tracks": return c.getString(R.string.tracks); + case "users": return c.getString(R.string.users); + default: return filter; + } + } + /** * Get a resource string with instructions for importing subscriptions for each service. * diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index 12c81c127..8127c3467 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -25,10 +25,13 @@ import android.widget.TextView; import android.widget.Toast; import org.schabi.newpipe.R; +import org.schabi.newpipe.download.DeleteDownloadManager; import java.io.File; import java.lang.ref.WeakReference; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; @@ -52,18 +55,34 @@ public class MissionAdapter extends RecyclerView.Adapter mItemList; private DownloadManagerService.DMBinder mBinder; private int mLayout; - public MissionAdapter(Activity context, DownloadManagerService.DMBinder binder, DownloadManager manager, boolean isLinear) { + public MissionAdapter(Activity context, DownloadManagerService.DMBinder binder, DownloadManager downloadManager, DeleteDownloadManager deleteDownloadManager, boolean isLinear) { mContext = context; - mManager = manager; + mDownloadManager = downloadManager; + mDeleteDownloadManager = deleteDownloadManager; mBinder = binder; mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mLayout = isLinear ? R.layout.mission_item_linear : R.layout.mission_item; + + mItemList = new ArrayList<>(); + updateItemList(); + } + + public void updateItemList() { + mItemList.clear(); + + for (int i = 0; i < mDownloadManager.getCount(); i++) { + DownloadMission mission = mDownloadManager.getMission(i); + if (!mDeleteDownloadManager.contains(mission)) { + mItemList.add(mDownloadManager.getMission(i)); + } + } } @Override @@ -102,7 +121,7 @@ public class MissionAdapter extends RecyclerView.Adapter= Build.VERSION_CODES.LOLLIPOP) { - intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION); - } - //mContext.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); - Log.v(TAG, "Starting intent: " + intent); - mContext.startActivity(intent); - } - private void viewFileWithFileProvider(File file, String mimetype) { String ourPackage = mContext.getApplicationContext().getPackageName(); Uri uri = FileProvider.getUriForFile(mContext, ourPackage + ".provider", file); diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index 2ff83086f..14439f6c8 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -10,6 +10,8 @@ import android.content.SharedPreferences; import android.os.Bundle; import android.os.IBinder; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -19,13 +21,15 @@ import android.view.View; import android.view.ViewGroup; import org.schabi.newpipe.R; +import org.schabi.newpipe.download.DeleteDownloadManager; +import io.reactivex.disposables.Disposable; import us.shandian.giga.get.DownloadManager; import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.ui.adapter.MissionAdapter; public abstract class MissionsFragment extends Fragment { - private DownloadManager mManager; + private DownloadManager mDownloadManager; private DownloadManagerService.DMBinder mBinder; private SharedPreferences mPrefs; @@ -37,14 +41,19 @@ public abstract class MissionsFragment extends Fragment { private GridLayoutManager mGridManager; private LinearLayoutManager mLinearManager; private Context mActivity; + private DeleteDownloadManager mDeleteDownloadManager; + private Disposable mDeleteDisposable; private ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder binder) { mBinder = (DownloadManagerService.DMBinder) binder; - mManager = setupDownloadManager(mBinder); - updateList(); + mDownloadManager = setupDownloadManager(mBinder); + if (mDeleteDownloadManager != null) { + mDeleteDownloadManager.setDownloadManager(mDownloadManager); + updateList(); + } } @Override @@ -55,6 +64,14 @@ public abstract class MissionsFragment extends Fragment { }; + public void setDeleteManager(@NonNull DeleteDownloadManager deleteDownloadManager) { + mDeleteDownloadManager = deleteDownloadManager; + if (mDownloadManager != null) { + mDeleteDownloadManager.setDownloadManager(mDownloadManager); + updateList(); + } + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.missions, container, false); @@ -104,10 +121,26 @@ public abstract class MissionsFragment extends Fragment { mActivity = activity; } + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + if (mDeleteDownloadManager != null) { + mDeleteDisposable = mDeleteDownloadManager.getUndoObservable().subscribe(mission -> { + if (mAdapter != null) { + mAdapter.updateItemList(); + mAdapter.notifyDataSetChanged(); + } + }); + } + } + @Override public void onDestroyView() { super.onDestroyView(); getActivity().unbindService(mConnection); + if (mDeleteDisposable != null) { + mDeleteDisposable.dispose(); + } } @Override @@ -129,7 +162,7 @@ public abstract class MissionsFragment extends Fragment { } private void updateList() { - mAdapter = new MissionAdapter((Activity) mActivity, mBinder, mManager, mLinear); + mAdapter = new MissionAdapter((Activity) mActivity, mBinder, mDownloadManager, mDeleteDownloadManager, mLinear); if (mLinear) { mList.setLayoutManager(mLinearManager); @@ -143,7 +176,7 @@ public abstract class MissionsFragment extends Fragment { mSwitch.setIcon(mLinear ? R.drawable.grid : R.drawable.list); } - mPrefs.edit().putBoolean("linear", mLinear).commit(); + mPrefs.edit().putBoolean("linear", mLinear).apply(); } protected abstract DownloadManager setupDownloadManager(DownloadManagerService.DMBinder binder); diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index f2cf85802..27aa56025 100644 --- a/app/src/main/res/layout/activity_main_player.xml +++ b/app/src/main/res/layout/activity_main_player.xml @@ -41,7 +41,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" - android:scaleType="centerInside" android:visibility="gone" tools:background="@android:color/white" tools:ignore="ContentDescription" diff --git a/app/src/main/res/layout/dialog_playback_parameter.xml b/app/src/main/res/layout/dialog_playback_parameter.xml index a8c6a5dcd..a5933397e 100644 --- a/app/src/main/res/layout/dialog_playback_parameter.xml +++ b/app/src/main/res/layout/dialog_playback_parameter.xml @@ -260,13 +260,95 @@ + + + + + + + + + + + + + + + + + - - - - - - + android:layout_height="wrap_content" + android:checked="false" + android:clickable="true" + android:focusable="true" + android:text="@string/skip_silence_checkbox" + android:maxLines="1" + android:layout_centerHorizontal="true" + android:layout_below="@id/unhookCheckbox"/> diff --git a/app/src/main/res/layout/fragment_video_detail.xml b/app/src/main/res/layout/fragment_video_detail.xml index 2b1f94c4a..7c6568b67 100644 --- a/app/src/main/res/layout/fragment_video_detail.xml +++ b/app/src/main/res/layout/fragment_video_detail.xml @@ -101,7 +101,8 @@ + android:layout_height="match_parent" + android:background="?android:windowBackground"> + android:focusable="true" + android:background="?attr/selectableItemBackground"> + - جاري التشغيل في الخلفية + يتم التشغيل في الخلفية إلغاء - إختر متصفح + اختر المتصفح مظلم صيغة الصوت الإفتراضية الدقة الإفتراضية @@ -25,7 +25,7 @@ تطبيق Kore غير موجود. هل تريد تثبيته ؟ مضيء صور معاينة الفيديو - M4A — جودة أفضل + جودة أفضل — M4A خطأ في الشبكة الفيديو التالي لا يوجد مشغل فيديو. هل تريد تثبيت VLC ؟ @@ -42,7 +42,7 @@ الفيديو والصوتيات مشاركة مشاركة بواسطة - عرض\'مقاطع\'الفيديو\'التالية\'و\'المشابهة\' + عرض الفديوهات \'التالية\'و\'المماثلة\' عرض خيار لتشغيل الفيديو بواسطة Kodi Media Center عرض خيار التشغيل بواسطة Kodi السمة @@ -61,10 +61,10 @@ تعذرت عملية تحليل الموقع تعذر فك تشفير توقيع رابط الفيديو اضغط بحث للبدء - إشتراك + اشتراك مشترك الرئيسية - الإشتراكات + الاشتراكات ما الجديد @@ -72,7 +72,7 @@ تشغيل تلقائي اسود التاريخ وذاكرة التخزين المؤقت - التأريخ و ذاكرة التخزين المؤقتة + التاريخ و ذاكرة التخزين المؤقتة المحتوى التنزيلات التنزيلات @@ -84,24 +84,24 @@ عن التطبيق التاريخ التاريخ - فتح في وضع النوافذ المنبثقة - "بعض القرارات لن يكون الصوت عند تمكين هذا الخيار " + فتح في وضع النافذة المنبثقة + "بعض الخيارات الدقة لن تحتوي على صوت عند تمكين هذا الخيار " وضع النوافذ المنبثقة NewPipe - تم إلغاء اشتراك القناة - تعذر تغيير في الاشتراك + تم إلغاء الاشتراك في القناة + تعذر تغيير حالة الاشتراك تعذر تحديث الاشتراك نافذة تشغيل مقطع الفيديو عند إستدعاء NewPipe من تطبيق آخر - "دقة النوافذ المنبثقة الافتراضية " + الدقة الافتراضية لنوافذ المنبثقة "عرض أعلى جودة " بعض الأجهزة فقط تدعم تشغيل مقاطع الفيديو 2K / 4K تنسيق الفيديو الافتراضي تذكر حجم النافذة و وضعها - تذكر آخر حجم ومكان للنافذة + تذكر آخر مكان و حجم للنافذة المنبثقة اعدادات إيماءة المشغل - استخدم إيماءات للتحكم في سطوع وحجم المشغل + استخدم إيماءات التحكم في سطوع وصوت المشغل اقتراحات البحث عرض الاقتراحات عند البحث سجل البحث @@ -114,7 +114,7 @@ المشغل السلوك المنبثق - التشغيل في الوضع المنبثق + يتم التشغيل في الوضع المنبثق تم وضعه على قائمة الانتظار في مشغل الخلفية تم وضعه على قائمة الانتظار في مشغل النافذة المنبثقة عرض المحتوى المقيّد بحسب العُمر @@ -170,9 +170,9 @@ استخدام المشغل القديم المشغل القديم المدمج في إطار Mediaframework - ك - م - ب + الف + مليون + بليون صفر لا تقم با الإختيار (في بعض اللغات) لأنها ليست \"حالة خاصة\" للأندرويد @@ -186,14 +186,14 @@ لاتوجد مشاهدات لاتوجد فديوهات - بداية + تشغيل إيقاف شغل حذف التوقيع مهمة جديدة - حسنا + حسناً اسم الملف العمليات @@ -238,7 +238,7 @@ البحث - شاهد + تمت مشاهدته تم تعطيل السجل التاريخ فارغ تم مسح التاريخ @@ -251,7 +251,7 @@ صفحة الخلاصة صفحة القناة حدد قناة - لم يتم الاشتراك في القناة بعد + لم يتم الاشتراك في اي قناة بعد الترند أفضل 50 جديد & وساخن @@ -265,9 +265,9 @@ تحدي ريكابتشا اضغط للإدراج بقائمة الانتظار - لم تتم المشاهدة + لاتوجد مشاهدة %s مشاهدة - "اثنان مشاهدات " + مشاهدتين %s مشاهدات كثيرة %s عدد المشاهدات %s أقصى مشاهدات @@ -276,10 +276,10 @@ صفر واحد - اثنان - قليل - عدد كثير - "أخرى " + %s اثنان + %s قليل + %s عدد كثير + "%s أخرى " إعادة طلب كلمة التحقق @@ -289,27 +289,27 @@ حدد كشك كشك - إدراج بقائمة الانتظار على خلفية + إدراج بقائمة الانتظار في مشغل الخلفية إدراج بقائمة الانتظار على المنبثقة ابدأ هنا على خلفية المصدر المحتوى الإفتراضي حسب البلد تغيير الإتجاه - الإنتقال إلى الخلفية - الإنتقال إلى نافذة منبثقة - التحول إلى الرئيسية + الإنتقال إلى التشغيل في الخلفية + الإنتقال إلى التشغيل في النافذة المنبثقة + الإنتقال إلى الرئيسية الخدمة فتح الدرج إغلاق الدرج - دائمًا + دائماً مرة واحدة فقط العنوان خاطئ - لم يتم العثور على مشغل بث (يمكنك تثبيت VLC لتشغيله) + لم يتم العثور على مشغل الفديو (يمكنك تثبيت VLC لتشغيله) استيراد قاعدة البيانات تصدير قاعدة البيانات "سيقوم بالكتابة على سجل التاريخ والاشتراكات الحالية " - تصدير سجل, الاشتراكات وقوائم التشغيل. + تصدير سجل, الاشتراكات وقوائم التشغيل عرض المعلومات إضافة إلى @@ -334,7 +334,7 @@ مشغل الفديو - السؤال دائمًا + السؤال دائماً الحصول على المعلومات … تحميل المحتوى المطلوب @@ -342,14 +342,14 @@ إنشاء قائمة تشغيل جديدة حذف قائمة التشغيل "إعادة تسمية قائمة التشغيل " - التسمية + الأسم إضافة إلى قائمة تشغيل هل تريد حذف قائمة التشغيل هذه ؟ قائمة التشغيل التي تم إنشاؤها تمت إضافتها إلى قائمة التشغيل لا يمكن حذف قائمة التشغيل - ملئ + ملئ الشاشة تكبير حجم خط التسمية @@ -359,7 +359,7 @@ مُزامَنة - تنزيل الملف البث. + تنزيل ملف البث. الإشارات مرجعية استعمال التقديم السريع الغير دقيق @@ -389,38 +389,38 @@ "معرفك , soundcloud.com/ الخاص بك " إفتراضي -تعطيل إيقاف جميع الصور المصغرة من تحميل البيانات واستخدام الذاكرة وحفظها. سيؤدي التغيير هذا إلى محو-ذاكرة التخزين المؤقت في الذاكرة-وذاكرة على القرص. +تعطيل إيقاف جميع الصور المصغرة من تحميل البيانات واستخدام الذاكرة وحفظها. سيؤدي التغيير هذا إلى محو-ذاكرة التخزين المؤقت في الذاكرة-وذاكرة على القرص امسح البيانات الوصفية المخزنة مؤقتًا إزالة جميع بيانات صفحات الويب المخزنة مؤقتًا تم محو ذاكرة التخزين المؤقت للبيانات الوصفية وضع البث القادم تلقائيا في قائمة الإنتظار - رفض البث المشابه في حال كون البث السابق يعمل في حالة عدم التكرار. - إضافة إلى قائمة التشغيل كصورة مصغرة + رفض البث المشابه في حال كون البث السابق يعمل في حالة عدم التكرار + إضافة صورة مصغرة إلى قائمة التشغيل قائمة التشغيل المخزنة تم تغيير الصورة المصغرة لقائمة التشغيل بدون تسميات توضيحية تسميات توضيحية - تعديل مشغل نص التسمية التوضيحية وأنماط الخلفية. يتطلب إعادة تشغيل التطبيق ليصبح ساري المفعول. + تعديل مشغل نص التسمية التوضيحية وأنماط الخلفية. يتطلب إعادة تشغيل التطبيق لتصبح التغييرات سارية المفعول تمكين LeakCanary - قد يتسبب مراقبة في تسرب الذاكرة في عدم استجابة التطبيق عند تفريغ السجلات + قد تتسبب مراقبة تسرب الذاكرة في عدم استجابة التطبيق عند تفريغ السجلات تقرير الأخطاء خارج دورة الحياة فرض الإبلاغ عن استثناءات Rx غير القابلة للتسليم خارج دورة حياة الجزء أو النشاط بعد التخلص منها محو سجل المشاهدة - احذف محفوظات التدفقات التي تم تشغيلها. - حذف سجل المشاهدة بالكامل. - سجل المشاهدة المحذوف. + احذف محفوظات الفديوهات التي تم تشغيلها + حذف سجل المشاهدة بالكامل + سجل المشاهدة محذوف. محو سجل البحث - يحذف تاريخ البحث عن الكلمات الرئيسية. - حذف محفوظات البحث بالكامل. + يحذف تاريخ البحث عن الكلمات الرئيسية + حذف محفوظات البحث بالكامل سجل البحث المحذوف. المشغل الخارجي لا يدعم هذه الأنواع من الروابط مصدر ملف / مصدر غير صالح - الملف غير موجود أو ليس لديه الإذن الكافي للقراءة أو الكتابة إليه + الملف غير موجود أو ليس لديك الإذن الكافي للقراءة أو الكتابة إليه لا توجد تدفقات متاحة للتنزيل تم حذف عنصر واحد. @@ -433,7 +433,7 @@ آخر ما تم تشغيله الأكثر تشغيلا - هذا سوف يتجاوز الإعداد الحالي الخاص بك. + هذا سوف يتجاوز الإعدادت الحالية الخاصة بك. طريقة \'التشغيل\' المفضلة "الإجراء الافتراضي عند فتح المحتوى — %s" @@ -445,25 +445,25 @@ تعذر استيراد الاشتراكات لا يمكن تصدير الاشتراكات - استيراد اشتراكات YouTube عن طريق تنزيل ملف التصدير: -\n -\n1. انتقل إلى عنوان URL هذا: %1$s -\n2. تسجيل الدخول عندما يتطلب -\n3. يجب أن يبدأ تنزيل (وهذا ملف التصدير) - قم باستيراد ملف تعريف SoundCloud عن طريق كتابة عنوان URL أو معرفك: -\n -\n1. تمكين \"وضع سطح المكتب\" في متصفح الويب (الموقع غير متاح للأجهزة المحمولة) -\n2. انتقل إلى عنوان URL هذا: %1$s -\n3. تسجيل الدخول عندما يتطلب + استيراد اشتراكات YouTube عن طريق تنزيل ملف التصدير: +\n +\n1. انتقل إلى عنوان URL هذا: %1$s +\n2. تسجيل الدخول عندما يطلب منك +\n3. يجب أن يبدأ التنزيل (وهذا ملف التصدير) + قم باستيراد ملف تعريف SoundCloud عن طريق كتابة عنوان URL أو معرفك: +\n +\n1. تمكين \"وضع سطح المكتب\" في متصفح الويب (الموقع غير متاح للأجهزة المحمولة) +\n2. انتقل إلى عنوان URL هذا: %1$s +\n3. تسجيل الدخول عندما يطلب منك \n4. انسخ عنوان URL للملف الشخصي الذي تمت إعادة توجيهك إليه. - ضع في اعتبارك أن هذه العملية يمكن أن تكون باهظة الثمن. -\n + ضع في اعتبارك أن هذه العملية يمكن أن تكون مكلفة اذا كنت تستخدم بيانات اشتراك انترنت. +\n \nهل تريد الاستمرار؟ ضوابط سرعة التشغيل سرعة الأداء تردد الصوت - "النزع (قد يسبب تشويه)" + نزع الإرتباط (قد يسبب تشويه) تعديل الايقاع Nightcore هل تريد أيضا استيراد الإعدادات؟ @@ -478,4 +478,18 @@ لا حدود الحد من جودة الفيديو عند استخدام بيانات الهاتف المحمول + تسريع إلى الأمام أثناء الصمت + خطوة + إعادة تعيين + + تصغير عند تبديل التطبيق + الإجراء عند التبديل إلى تطبيق آخر من مشغل الفيديو الرئيسي — %s + لاشيء + تصغير إلى مشغل الخلفية + تصغير إلى مشغل منبثق + +القنوات + قوائم التشغيل + المسارات + المستخدمين diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 8ab7ee9dc..019922f73 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -1,14 +1,14 @@ -Натисни на търсене за да започнеш +Докоснете търсачката, за да започнете %1$s гледания Публикувано на %1$s Не е намерен стрийм плейър. Желаете ли да инсталирате VLC? Инсталирай - Затвори - Отвори в браузъра - Отвори в подпрозорец + Отмени + Отвори в браузър + Отвори в прозорец Сподели - Сваляне + Изтегли Търси Настройки Може би имахте в предвид: %1$s ? @@ -16,81 +16,81 @@ Избери браузър ориентация Използвай външен видео плейър - Някои резолюции няма да имат вграден аудио поток ако изберете тази опция + Някои резолюции няма да имат вграден аудио поток, ако тази опция е избрана Използвай външен аудио плейър - Режим с подпрозорец на NewPipe + NewPipe в прозорец Абониране Абониран Премахнат абонамент за канала - Не мога да променя абонаментите - Не мога да обновя абонаментите + Неуспешна промяна на абонамента + Неуспешно обновление на абонамента Абонаменти - Новости + Обновления - Фон - Подпрозорец + Във фонов режим + В прозорец - Локация за запис на клиповете - Път за съхранение на свалени клипове - Въведи път за съхранение на свалени клипове + Директория за изтегляне на видео + Папка за съхранение на изтеглените видеота + Въведете път за съхранение на изтеглените видеота - Локация за запис на аудио - Път за съхранение на свалено аудио - Въведи път за съхранение на свалено аудио + Директория за изтегляне на аудио + Папка за съхранение на изтеглено аудио + Въведете път за съхранение на изтеглено аудио Автоматично възпроизвеждане - Автоматично въпроизвеждане на видео, когато NewPipe е повикан от друго приложение + Въпроизвежда видео, когато NewPipe е повикан от друго приложение Резолюция по подразбиране - Резолюция по подразбиране на подпрозореца + Резолюция по подразбиране на прозореца Покажи по-високи резолюции Само някои устройства поддържат възпроизвеждане на 2K/4K клипове Въпроизвеждане с Kodi - Приложението Kore не е намерено. Желаете ли да го инсталирате? - Покажи опция \"Въпроизвеждане с Kodi\" - Показване на опция за въпроизвеждане на видео чрез Kodi media center + Приложението „Kore“ не е намерено. Желаете ли да го инсталирате? + Покажи „Възпроизвеждане с Kodi“ + Показване на опция за възпроизвеждане на видео чрез „Kodi media center“ Аудио Аудио формат по подразбиране Видео формат по подразбиране WebM — свободен формат M4A — по-добро качество - Тема + Тема на външния вид Светла Тъмна Черна - Помни размера и позицията на подпрозореца - Запомни последния размер и позиция на подпрозореца + Помни размера и позицията на прозореца + Използвай размера и позицията на прозореца от предишния път Контролиране на плейъра чрез жестове - Използвай жестове за да контролираш яркостта и силата на звука на плейъра + Позволи използване на жестове за контрол на яркостта и силата на звука на плейъра Предложения за търсене - Покажи предположения по време на търсене + Показвай предложения за търсене История на търсенията Съхранявай заявките за търсене локално - История - Следи изгледаните клипове - Поднови клип при възобновяване на фокуса - Продължава въпроизвеждането след прекъсване (например, телефонно обаждане) - Свалени + История и кеш-памет + Запаметявай кои видеота са гледани + Възобнови при връщане на фокус + Продължавай възпроизвеждането след прекъсване (например телефонно обаждане) + Изтегли Следващ клип - Показвай следващия и подобни клипове - Показвай поле със съвет \"Задръж за добавяне\" - Неподдържан URL + Показвай „следващ“ и „подобни“ + Показвай съвет „задръж за добавяне“ + Непознат URL Език на съдържанието по подразбиране Плейър Поведение Видео & Аудио - История - Подпрозорец - Вид + История и кеш-памет + Нов прозорец + Външност Други - Въпроизвеждане във фонов режим - Въпроизвеждане в подпрозорец + Възпроизвеждане във фонов режим + Възпроизвеждане в подпрозорец Включен в опашката на фоновия плейър - Включен в опашката за възпроизвеждане в подпрозорец + Включен в опашката в нов прозорец Възпроизвеждане Съдържание - Покажи съдържание с ограничение във възрастта + Покажи съдържание за възрастни Съдържание за възрастни. Разрешаването на такова съдържание става от Настройки. на живо Изтегляния @@ -111,17 +111,17 @@ Възпроизведи всички Известия от NewPipe - Известия за фоновия плейър и плейъра на подпрозореца на NewPipe + Известия за фоновия плейър и плейъра в отделен прозорец на NewPipe [Неизвестен] Грешка Проблем с мрежата Не мога да заредя всички миниатюри - Не мога да интерпретирам правилно уебсайта - Не мога да интерпретирам изцяло правилно уебсайта + Неуспешно пресъздаване на уебсайта + Не мога да пресъздам изцяло уебсайта Съдържанието не е налично - Блокирано от GEMA + Блокирано от „GEMA“ Не мога да настроя меню за сваляне Това е предаване на живо, което все още не се поддържа. Не мога да достъпя нито един поток @@ -131,7 +131,7 @@ Критичен проблем с плейъра Опит за възстановяване от възникналия проблем с плейъра - Съжаляваме, но това не би трябвало да се случва. + Съжаляваме, това не би трябвало да се случва. Докладвай за грешката чрез имейл Съжаляваме, възникнаха някои грешки. ДОКЛАД @@ -141,23 +141,23 @@ Подробности: - Миниатюра на клипа - Миниатюра на аватаря на качилия клипа + Миниатюра на видео + Миниатюра на аватара на качилия видео Харесвания Нехаресвания - Използвай Tor - (Експериментално) Пренасочвай трафика при сваляне към мрежата на Tor за по-високо ниво на поверителност (стриймването на клипове все още не се поддържа). + Използвай „Tor“ + (Експериментално) Пренасочвай трафика при сваляне към мрежата на „Tor“ за по-високо ниво на поверителност (стриймването на видео все още не се поддържа). Докладвай за грешка Няма резултати - Тук все още няма нищо + Тук няма нищо - Не мога да създам директория за свалянията в \'%1$s\' - Създадох директория за свалянията в \'%1$s\' + Неуспешно създаване на директория за изтеглени в „%1$s“ + Бе създадена директория за свалянията в „%1$s“ Видео Аудио Опитай отново - Отказан достъп до хранилището (провери правата) + Достъпа до хранилището е отказан Използвай стария плейър Използваю стария вграден Mediaframewoek плейър @@ -181,34 +181,286 @@ Пауза Начало Изтрий - Чексума + Контролна сума - Готово + ОК Име на файла Нишки Грешка Сървърът не се поддържа Файлът вече съществува - Неправилен формат на URL-а или няма връзка - NewPipe Сваляне - Натисни за подробности + Неправилен хиперлинк или няма връзка с Интернет + NewPipe Изтегляне + Докосни за подробности Моля, изчакайте… Копирано в клипборда Позволени символи в името на файловете Невалидните символи се заменят с тази стойност - Стойност за замяна + Символ за замяна Букви и цифри За NewPipe Настройки Относно приложението Лицензи от трети страни - Не мога да заредя лицензите + Неуспешно зареждане на лиценза Отвори уебсайта Относно приложението Допринесли - Лицензии - Безплатен и лек YouTube фронтенд за Android. + Лицензи + Безплатно и леко поточно предаване за Android. Виж в GitHub + Изтегли стрийм файл. + Покажи инфо + + Главен + Отметки + + Добави към + + Използвай бързо, но неточно превъртане + Неточното превъртане позволява на плейъра да превърта кадри по-бързо, с намалена прецизност + Зареждай миниатюри + Кеш-паметта с изображения е изтрита + Изтрий кешираните метаданни + Премахни всички метаданни за уебстраници от кеш-паметта + Кеш-паметта с метаданни бе изтрита + Автоматично нареди на опашка следващия + "Автоматично прибавяне на сродно съдържание при неповтарящ се преглед . " + Държава, за която да бъде показвано съдържание + Услуга + Отстраняване на грешки + Винаги + Само веднъж + Файл + + Смени ориентацията + Мини във фонов режим + Мини към нов прозорец + Мини в основен режим + + Импортиране на база данни + Експортиране на база данни + Ще замени текущите история и абонаменти + Експортиране на историята, абонаментите и плейлистите + Изтрий историята с изгледани + Изтрий цялата история с изгледани. + Историята с изгледани е изтрита. + Изтрий историята на търсенията + Изтрива историята с въвежданите за търсене ключови думи. + Изтрий цялата история на търсенията. + Историята на търсенията е изтрита. + URL подписът на клипа не можа да бъде дешифрован + Външните плейъри не поддържат този вид линкове + Невалиден URL + Невалидна директория + Невалиден файл или източник на съдържание + Файлът не съществува или липсва разрешение за четене и/или запис + Името на файла не може да бъде празно + Възникна грешка: %1$s + Не са налични източници за изтегляне + + хил. + млн. + млрд. + + Няма абонати + Създай + Изтрий всички + Откажи + Няма инсталирано приложение, което да изпълни този файл + + © %1$s от %2$s под лиценза %3$s + Съдействайте + За всичко, което се сетите: превод, промени по дизайна, изчистване на кода или много сериозни промени по кода – помощта е винаги добре дошла. Колкото повече развитие, толкова по-добре! + Направете дарение + NewPipe се разработва от доброволци, които отделят от своето време, за да ви доставят най-доброто преживяване. Дайте от себе си в замяна, за да помогнете на разработчиците да направят NewPipe още по-добро приложение, докато се наслаждават на едно кафе от вас. + Дари + Уебсайт + Посетете сайта на NewPipe за повече информация и новини. + Политиката на NewPipe за личните данни + Проектът NewPipe се отнася много сериозно към вашата поверителност. За това, приложението не събира никакви данни без вашето съгласие. +\nНашата политика за личните данни обяснява подробно какви данни изпращате и къде се съхраняват, когато изпращате съобщения за грешки. + Прочетете нашата политика за поверителност + Лицензът на NewPipe + Липсва стрийм плейър (можете да изтеглите VLC, за да пуснете стрийма) + Изключете, за да спрете зареждането на всички миниатюри, спестявайки трафик и памет. При промяна на тази настройка, текущата кеш-памет на изображенията ще бъде изтрита. + Показвай подсказка, когато е избран фонов режим или режим в прозорец на страницата с детайли на съответния клип + Изтрива историята на възпроизвежданите стриймове. + Не са намерени видео стриймове + Не са намерени аудио стриймове + "Какво:\\nЗаявка:\\nЕзик на съдържанието:\\nУслуга:\\nВреме по GMT:\\nПакет:\\nВерсия:\\nОС версия: " + Миниатюра на видео + Потребителски доклад + Пренареди чрез плъзгане + + Начало + Изтрий един + Преименувай + + Нова цел + Моля, изберете достъпна папка за изтегляния + Това разрешение се изисква за +\nвъзпроизвеждане в отделен прозорец + 1 елемент е изтрит. + + reCAPTCHA + reCAPTCHA заявка + Изисква се въвеждане на reCAPTCHA + + Изтегляне + Повечето специални символи + + NewPipe е безплатен „copyleft“ софтуер: Можете да го използвате, изучавате, споделяте и подобрявате по ваше усмотрение. В частност, Вие можете да препубликувате и/или модифицирате приложението според правилата на Главния обществен лиценз на ГНУ, издаден от Фондацията за свободен софтуер – версия 3 на лиценза или по-нова. + Прочетете лиценза + + + История + Търсения + Гледани + Историята е изключена + История + Историята е празна + Историята е изчистена + Елементът е изтрит + Искате ли да изтриете този елемент от историята на търсенията? + Искате ли да изтриете този елемент от историята на гледанията? + Окончателно ли искате да изтриете всички елементи от историята на гледанията? + Последно възпроизвеждани + Най-възпроизвеждани + + Съдържание на главната страница + Празна страница + Страница-павилион + Страница с абонаменти + Страница с акценти от вашите абонаменти + Страница на определен канал + Изберете канал + За момента нямате абонаменти + Изберете павилион + Експортирането приключи + Импортирането приключи + Невалиден ZIP файл + Внимание: не всички файлове бяха импортирани успешно. + Това ще замени текущата Ви инсталация. + Желаете ли също да импортирате настройките? + + Павилион + Набиращи популярност + Топ 50 + Ново и горещо + Във фонов режим + В прозорец + Премахни + Детайли + Аудио настройки + Задръжте, за да поставите на опашката + На опашката във „фонов режим“ + На опашката в „режим в прозорец“ + Възпроизвеждане от тук + Възпроизвеждане от тук във фонов режим + Възпроизвеждане от тук в прозорец + + Отвори навигационната лента + Затвори навигационната лента + Тук нещо ще се появи скоро ;D + + + Действие при повикване от друго приложение + Действие по подразбиране при отваряне на съдържание — %s + + Видео плейър + Във фонов режим + В прозорец + Винаги питай + + Получаване на инфо… + Зареждане на заявеното съдържание + + Създай Нов Плейлист + Изтрий Плейлист + Преименувай Прелист + Име + Добави Към Плейлист + Задай като миниатюра на плейлиста + + Миниатюрата на плейлиста е сменена + Премахни отметката + + Искате ли да изтриете този плейлист? + Плейлистът е създаден + Добавено към плейлист + Миниатюрата на плейлиста е сменена + Плейлистът не можа да бъде изтрит + + Без надписи + + Приспособи + Запълни + Увеличи + + Авто-генерирани + + Надписи + Модифицирай мащаба на текста и фона на надписите. Изисква рестарт на приложението, за да се приложат промените. + + Включи LeakCanary + Следенето за пропускане на памет може да направи приложението нестабилно + + Докладвай за извънредни грешки + Импортиране/експортиране + Импортирай + Импортирай от + Експортирай в + + Импортиране… + Експортиране… + + Файл с данни за импортиране + Предишно експортиране + + Неуспешно импортиране на абонатите + Неуспешно експортиране на абонатите + + Импортирайте YouTube абонаментите, чрез изтегляне на нужния файл: +\n +\n1. Посетете тази връзка: %1$s +\n2. Влезте в акаунта си, когато това се изиска +\n3. Изтеглянето трябва да започне (това е експортирания файл) + Импортирайте SoundCloud профил чрез въвеждане на хипервръзката към него или чрез вашия ID: +\n +\n1. Включете „десктоп режим“ в браузър (сайтът е недостъпен за мобилни устройства) +\n2. Посетете връзката: %1$s +\n3. Влезте в профила си, ако се изисква +\n4. Копирайте хипервръзката на профилната страница, към която сте насочени. + вашиятID, soundcloud.com/вашиятID + + Това действие може да изразходва голямо количество данни от вашия трафик. +\n +\nЖелаете ли да продължите? + + Управление скоростта на възпроизвеждане + Темпо + Височина + Бързо превъртане при тишина + От съображения към Общия европейски регламент относно защитата на данните, Ви привличаме вниманието към политиката за поверителност на NewPipe. Моля, прочетете я внимателно. +\nТрябва да сте съгласни с условията, за да ни изпратите доклада за грешката. + Приеми + Откажи + + Без ограничения + Ограничена резолюция при мобилни данни + Минимизирай при преход към друго приложение + Действие при преминаване към друго приложение от видео плейъра — %s + Без минимизиране + Минимизирай във фонов режим + Минимизирай в прозорец + +Канали + Плейлисти + Песни + Потребители + Възстанови + diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index b8b7dd699..eeea5dffe 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -61,7 +61,7 @@ Importa una base de dades Exporta una base de dades - Exporta l\'historial, les subscripcions i les llistes de reproducció. + Exporta l\'historial, les subscripcions i les llistes de reproducció Error Error de xarxa Vídeo @@ -226,7 +226,7 @@ M\'agrada No m\'agrada Fes servir el Tor - (En proves) Força el trànsit de baixada a través del Tot per a més privadesa (no compatible encara amb les emissions de vídeo). + (En proves) Força el trànsit de baixada a través del Tor per a més privadesa (no compatible encara amb les emissions de vídeo). Informa sobre un error Informe de l\'usuari Cap resultat @@ -310,7 +310,7 @@ Toqueu el botó de cerca per començar En algunes resolucions NO hi haurà àudio quan aquesta opció estigui activada Reproductor d\'àudio extern - Desactiveu-ho per evitar que es carreguin les miniatures i estalviar dades i memòria. Si canvieu aquesta opció, s\'esborrarà la memòria cau d\'imatges tant de la memòria com de l\'emmagatzematge. + Desactiveu-ho per evitar que es carreguin les miniatures i estalviar dades i memòria. Si canvieu aquesta opció, s\'esborrarà la memòria cau d\'imatges tant de la memòria com de l\'emmagatzematge Emmagatzema les cerques localment Registra els vídeos visualitzats Reprèn automàticament @@ -390,7 +390,7 @@ Desvincula (pot provocar distorsió) Nightcore Elimina totes les dades de llocs web de la memòria cau - Afegeix a la cua un vídeo relacionat quan es reprodueix l\'últim vídeo en una cua sense repetició. + Afegeix a la cua un vídeo relacionat quan es reprodueix l\'últim vídeo en una cua sense repetició Mostra els missatges d\'ajuda Mostra un missatge d\'ajuda quan el botó de mode en segon pla o emergent estigui premut a la pàgina de detalls d\'un vídeo Què ha passat:\\nPetició:\\nIdioma del contingut:\\nServei:\\nHora GMT:\\nPaquet:\\nVersió:\\nVersió del SO: @@ -425,17 +425,17 @@ No hi ha vídeos que es puguin baixar Subtítols - Modifica la mida del text i el fons dels subtítols. Cal reiniciar l\'aplicació per aplicar els canvis. + Modifica la mida del text i el fons dels subtítols. Cal reiniciar l\'aplicació per aplicar els canvis No s\'ha trobat cap aplicació que pugui reproduir aquest fitxer Esborra l\'historial de reproduccions - Esborra l\'historial dels vídeos que s\'han reproduït. - Esborra tot l\'historial de reproduccions. + Esborra l\'historial dels vídeos que s\'han reproduït + Esborra tot l\'historial de reproduccions S\'ha esborrat l\'historial de reproduccions. Esborra l\'historial de cerca - Esborra l\'historial de paraules cercades. - Esborra tot l\'historial de cerca. + Esborra l\'historial de paraules cercades + Esborra tot l\'historial de cerca S\'ha esborrat l\'historial de cerca. S\'ha esborrat 1 element. @@ -452,4 +452,18 @@ Rebutja Sense restriccions Restringeix la resolució quan es facin servir dades mòbils + Minimitza en canviar d\'aplicació + Acció en canviar a una altra aplicació des del reproductor de vídeo principal — %s + Cap + Minimitza al reproductor en segon pla + Minimitza al reproductor emergent + +Avança ràpid durant el silenci + Pas + Reinicialitza + + Canals + Llistes de reproducció + Pistes + Usuaris diff --git a/app/src/main/res/values-cmn/strings.xml b/app/src/main/res/values-cmn/strings.xml index a6b3daec9..852ac7f1e 100644 --- a/app/src/main/res/values-cmn/strings.xml +++ b/app/src/main/res/values-cmn/strings.xml @@ -1,2 +1,123 @@ - - \ No newline at end of file + +点按搜索即可开始 + %1$s 意见 + 发布 %1$s + 找不到流播放器。你想安装VLC吗? + 找不到流播放器(您可以安装VLC播放它) + 安装 + 取消 + 在浏览器中打开 + 在弹出模式下打开 + 分享 + 下载 + 下载流文件. + 搜索 + 设置 + 你的意思是: %1$s ? + 与某人分享 + 选择浏览器 + 回转 + 使用外部视频播放器 + 启用此选项时,某些分辨率将不会有音频 + 使用外部音频播放器 + NewPipe弹出模式 + 订阅 + 订阅 + 取消订阅成功 + 无法更改订阅 + 无法更新订阅 + 显示详情 + + 主页 + 订阅 + 书签 + + 新功能 + + 后台 + 弹出窗口 + 添加到 + + 视频下载路径 + 储存视频文件的路径 + 输入视频的下载地址 + + 音频下载路径 + 储存音频的路径 + 输入音频的下载路径 + + 自动播放 + NewPipes被其它程序调用时播放视频 + 默认分辨率 + 默认弹出窗口分辨率 + 显示更高的分辨率 + 只有部分设备支持播放 2K/4K 视频 + 用 Kodi 播放 + 没找到 Kore 应用,需要安装它吗? + 显示“用 Kodi 播放”选项 + 显示用 Kodi媒体中心 播放视频的选项 + 音频 + 默认音频格式 + 默认视频格式 + 主题 + 亮色 + 酷黑 + 黑色 + 记住弹出窗口的尺寸与位置 + M4A — 更好的音质 + 记住上一次弹出窗口的位置以及大小 + 清理照片内存 + 最小化弹出播放器 + +清楚观看历史 + 已删除搜索历史 + 错误 + 网络错误 + 举报错误 + 没有结果 + 开始 + 暂停 + 播放 + 创建 + 删除 + 删除所有 + 新任务 + 好 +\n + + 错误 +\n + 服务器无法支持 + 文件已存在 + NewPipe 下载中 + 请稍等… + 字母与数字 + 最特别的字符 + + 这个文件里没有已下载应用程式 + + 关于NewPipe + 设置 + 关于 + 第三方执照 + 打开网页 + 删除书签 + + 请问你要删除这个列表吗? + 已创建播放列表 + 已加入播放列表 + 步骤 + 重置 + + 为了遵守欧洲通用数据保护法规(GDPR,我们请你注意NewPipe的隐私政策.请仔细阅读. +\n你必须接受它才能将错误报告发送给我们. + 接受 + 拒绝 + + 没有限制 + 使用移动数据时的解析度限制 + 最小化应用程序切换 + 从主视频播放器切换到其他应用程序时的操作 - %s + 没有 + 最小化背景播放器 + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 560b0e8e4..c58ab68e9 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -96,7 +96,7 @@ Live - „Suchen“ antippen um zu beginnen + „Suchen“ antippen, um zu beginnen Downloads Downloads Fehlerbericht @@ -286,7 +286,7 @@ Tipp anzeigen, wenn der Hintergrundwiedergabe- oder Pop-up-Button auf der Videodetailseite gedrückt gehalten wird In der Warteschlange der Hintergrundwiedergabe Neu & Heiß - Zum Anfügen gedrückt halten + Halten, um zur Wiedergabeliste hinzuzufügen \"Gedrückt halten, um Tipp hinzuzufügen\" anzeigen [Unbekannt] @@ -298,8 +298,8 @@ Spenden Zurückgeben Website - Besuche die NewPipe Website für weitere Informationen und Neuigkeiten. - NewPipe wird von Freiwilligen entwickelt, die ihre Freizeit dafür aufbringen, die beste Nutzererfahrung zu bieten. Gib etwas zurück, um Entwicklern zu helfen, NewPipe bei einer Tasse Kaffee noch besser zu machen. + Besuche die NewPipe-Website für weitere Informationen und Neuigkeiten. + NewPipe wird von Freiwilligen entwickelt, die ihre Freizeit dafür verwenden, dir die beste Nutzererfahrung zu bieten. Gib etwas zurück, um den Entwicklern zu helfen, NewPipe noch besser machen zu können, während sie sich an einer Tasse Kaffee erfreuen. Service Keinen Streamplayer gefunden (du kannst VLC installieren, um den Stream abzuspielen) Standard-Land des Inhalts @@ -325,11 +325,11 @@ Immer fragen Informationen werden abgerufen… - Die angeforderten Inhalte werden geladen + Gewünschten Inhalt laden Datenbank importieren Datenbank exportieren Wird deinen Verlauf und deine Abos überschreiben - Verlauf, Abos und Wiedergabelisten exportieren. + Verlauf, Abos und Wiedergabelisten exportieren Keine gültige ZIP-Datei Warnung: Nicht alle Dateien konnten importiert werden. Dies wird deine aktuellen Einstellungen überschreiben. @@ -358,7 +358,7 @@ Wiedergabeliste löschen Wiedergabeliste umbenennen Zur Wiedergabeliste hinzufügen - Als Vorschaubild der Wiedergabeliste festlegen + Als Symbolbild der Wiedergabeliste festlegen Lesezeichen entfernen @@ -407,8 +407,8 @@ Metadatencache gelöscht Debug Ungültige Datei-/Inhaltsquelle - Export fertiggestellt - Import fertiggestellt + Export abgeschlossen + Import abgeschlossen Name Import/Export Import @@ -418,12 +418,12 @@ Wiedergabegeschwindigkeitsregler Geschwindigkeit Tonhöhe - Aushaken (kann zu Verzerrungen führen) + Verknüpfung aufheben (kann zu Verzerrungen führen) Nightcore Standard -Deaktiviere diese Option, um das Laden aller Miniaturansichten zu stoppen und Daten und Speicherverbrauch zu sparen. Wenn du dies änderst, wird sowohl der In-Memory- als auch der On-Disk-Image-Cache gelöscht. +Deaktiviere diese Option, um das Laden aller Miniaturansichten zu stoppen und Daten und Speicherverbrauch zu sparen. Wenn du dies änderst, wird sowohl der In-Memory- als auch der On-Disk-Image-Cache gelöscht Nächsten Stream automatisch einreihen - Automatisches Anhängen eines verwandten Streams beim Abspielen des letzten Streams in einer nicht wiederholten Warteschlange. + Automatisches Anhängen eines verwandten Streams beim Abspielen des letzten Streams in einer nicht wiederholten Warteschlange Hier wird bald etwas stehen ;D @@ -456,17 +456,17 @@ Standardaktion beim Öffnen von Inhalten - %s Untertitel - Ändere Textgröße und Hintergrundstil des Untertitels im Player. Wird erst nach Neustart des App wirksam. + Ändere Textgröße und Hintergrundstil des Untertitels im Player. Wird erst nach Neustart des App wirksam Keine App zum Abspielen dieser Datei installiert Verlauf leeren - Löscht den Verlauf der abgespielten Streams. - Löscht den ganzen Verlauf. + Löscht den Verlauf der abgespielten Streams + Löscht den ganzen Verlauf Verlauf gelöscht. Suchverlauf löschen - Lösche Verlauf der Suchschlüsselwörter. - Lösche gesamten Suchverlauf. + Lösche Verlauf der Suchschlüsselwörter + Lösche gesamten Suchverlauf Suchverlauf gelöscht. 1 Element gelöscht. @@ -479,8 +479,22 @@ Lies die Datenschutzbestimmungen Akzeptieren Ablehnen -Um den Anforderungen der Datenschutz-Grundverordnung (DSGVO) genüge zu tun, möchten wir dich hiermit auf die Datenschutzbedingungen von NewPipe aufmerksam machen. Bitte lies sie sorgfältig durch. -\nDu musst ihr zustimmen, um einen Bugreport an uns zu senden. +Um der europäischen Datenschutz-Grundverordnung (DSGVO) gerecht zu werden, weisen wir hiermit auf NewPipe\'s Datenschutzerklärung hin. Bitte lies sie sorgfältig durch. +\nDu musst den Datenschutzrichtlinien zustimmen, um den Fehlerbericht an uns zu senden. Unbegrenzt Auflösung bei Verwendung mobiler Daten begrenzen + Minimieren beim Anwendungswechsel + Aktion beim Umschalten auf eine andere Anwendung vom Haupt-Videoplayer - %s + Keine + Zum Hintergrund-Player minimieren + Zum Popup-Player minimieren + +Vorspulen während der Stille + Schritt + Zurücksetzen + + Kanäle + Wiedergabelisten + Titel + Nutzer diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index fd89066aa..8f383b580 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -62,4 +62,43 @@ Δημιουργήθηκε ο φάκελος \'%1$s\' Δ + Άνοιγμα σε αναδυόμενο παράθυρο + Εγγραφή + Εγγεγραμμένος + Μόνο μερικές συσκευές υποστηρίζουν την αναπαραγωγή 2K/4K βίντεο + Μαύρο + Προτάσεις αναζήτησης + Ιστορικό αναζήτησης + Λήψεις + Λήψεις + Όλα + Κανάλι + Ναι + Αργότερα + Φίλτρο + Σφάλμα + ΑΝΑΦΟΡΑ + Πληροφορίες: + Τι συνέβη: + Το σχόλιό σας (στα Αγγλικά): + Λεπτομέρειες: + + + Αναφορά Σφάλματος + Βίντεο + Ήχος + Παύση + Διαγραφή + Σφάλμα + Το αρχείο υπάρχει ήδη + Πατήστε για λεπτομέρειες + Λήψη + Γράμματα και ψηφία + Ρυθμίσεις + Άνοιγμα ιστοσελίδας + Άδειες + Ιστορικό + Ιστορικό + Εμφάνιση πληροφοριών + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index bfd0f3e6e..b2aca1df6 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -330,7 +330,7 @@ abrir en modo popup Importar base de datos Exportar base de datos Reemplazará su historial actual y sus suscripciones - Exportar historial, suscripciones y listas de reproducción. + Exportar historial, suscripciones y listas de reproducción Exportación completa Importación completa Archivo ZIP no válido @@ -403,7 +403,7 @@ abrir en modo popup Usar búsqueda rápida inexacta La búsqueda inexacta permite al reproductor buscar posiciones más rápido con menor precisión Auto-encolar la siguiente transmisión - Auto-añadir un vídeo relacionado al reproducir el último vídeo en una cola no repetitiva. + Auto-añadir un vídeo relacionado al reproducir el último vídeo en una cola no repetitiva DIRECTO SINCRONIZAR Archivo @@ -445,7 +445,7 @@ abrir en modo popup
\n \n¿Desea continuar?
Cargar Miniaturas - Descativar todas las miniaturas para evitar que se carguen, guarden datos y usen memoria. Al cambiar esto se borrarán tanto la caché de imágenes en la memoria como en el disco. + Descativar todas las miniaturas para evitar que se carguen, guarden datos y usen memoria. Al cambiar esto se borrarán tanto la caché de imágenes en la memoria como en el disco Caché de imagen limpiado Metadatos eliminados del caché Eliminar todos los datos de la página web en caché @@ -453,7 +453,7 @@ abrir en modo popup
Controles de velocidad de reproducción Tiempo Tono - Desenganchar (puede casusar distorsión) + Desenganchar (puede causar distorsión) Nightcore (tipo de música) Reproducción por defecto No hay streams disponibles para descargar @@ -464,15 +464,15 @@ abrir en modo popup No hay ninguna app instalada para reproducir este archivo Subtítulos - Modificar la escala de texto de los subtítulos y los estilos de fondo del reproductor. Requiere el reinicio de la app para que surta efecto. + Modificar la escala de texto de los subtítulos y los estilos de fondo del reproductor. Requiere el reinicio de la app para que surta efecto Borrar historial de reproducciones - Elimina el historial de las transmisiones reproducidas. - Elimina todo el historial de reproducciones. + Elimina el historial de las transmisiones reproducidas + Elimina todo el historial de reproducciones Historial de reproducciones eliminado. Borrar historial de búsqueda - Elimina el historial de palabras clave de búsqueda. - Elimina todo el historial de búsqueda. + Elimina el historial de palabras clave de búsqueda + Elimina todo el historial de búsqueda Historial de búsquedas eliminado. 1 elemento eliminado. @@ -488,4 +488,18 @@ abrir en modo popup Sin límite Limitar resolución cuando se use Datos Móviles + Mimimizar al cambiar de aplicación + Acción de cambiar a otra aplicación desde el reproductor principal + Ninguna + Minimizar al reproductor de fondo + Minimizar el reproductor emergente + +Avance rápido durante el silencio + Paso + Reiniciar + + Canales + Usuarios + Listas de reproducción + Pistas diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml new file mode 100644 index 000000000..7f08efd38 --- /dev/null +++ b/app/src/main/res/values-et/strings.xml @@ -0,0 +1,421 @@ + +Alustuseks puuduta otsingut + %1$s vaatamist + Avaldatud %1$s + Voogesituseks puudub pleier. Kas paigaldada VLC? + Voogesituseks puudub pleier. (Selleks võib paigaldada VLC) + Paigalda + Tühista + Ava veebilehitsejas + Ava hüpikaknas + Jaga + Laadi alla + Laadi voog alla. + Otsi + Seaded + Kas mõtlesid: %1$s ? + Jaga + Vali veebilehitseja + pööramine + Kasuta välist videopleierit + Kui see valik lubada, puudub osadel lahutustel heli + Kasuta välist audiopleierit + NewPipe hüpikaknarežiim + Telli + Tellitud + Kanali tellimus tühistatud + Tellimuse muutmine nurjus + Tellimuse uuendamine nurjus + Kuva info + + Tellimused + Järjehoidjad + + Mis on uut + + Taust + Hüpikaken + "Lisa " + + Video allalaadimise kaust + Kaust allalaetud videote hoiustamiseks + Sisesta videote allalaadimise rada + + Audio allalaadimise kaust + Kaust allalaetud audio hoiustamiseks + Sisesta audio allalaadimise rada + + Automaatesitus + Esita video, kui NewPipe käivitub teise rakenduse kaudu + Vaikelahutus + Hüpikakna vaikelahutus + Kuva kõrgemaid lahutusi + Kõik seadmed ei toeta 2K/4K videoid + Esita Kodi abil + Kore rakendust ei leitud. Kas paigaldada see? + Kuva valik \"Esita Kodi abil\" + Kuva valik video esitamiseks Kodi meediakeskuse kaudu + Heli + Heli vaikevorming + Video vaikevorming + WebM — libre vorming + M4A — parem kvaliteet + Teema + Hele + Tume + Must + Pea hüpikakna suurus ja asukoht meeles + Pea hüpikakna viimane suurus ja asukoht meeles + Kasuta ebatäpset kerimist + Ebatäpne kerimine lubab pleieril otsida asukohta kiiremini täpsuse arvel + Laadi pisipildid + Keela, peatamaks pisipiltide laadimine ja vähenda andme- ja mälukasutust. Selle muutmine puhastab nii sisemälu kui piltide vahemälu andmekandjal + Pildid kustutati vahemälust + Kustuta metaandmed vahemälust + Kustuta veebilehtede andmed vahemälust + Metaandmed kustutati vahemälust + Järgmine voog automaatselt järjekorda + Lisa seotud voog automaatselt, kui esitusel on viimane voog mittekorduvast järjekorrast + Pleieri juhtimise viiped + Luba viiped helitugevuse ja ereduse juhtimiseks + Kuva soovitused + Kuva otsingu ajal soovitusi + Otsinguajalugu + Salvesta otsinguajalugu kohalikult + Ajalugu ja vahemälu + Jälgi videote vaatamist + Jätka taasesitust fookuse saamisel + Jätka taasesitust pärast katkestamist (nt. telefonikõne) + Laadi alla + Järgmine video + Kuva \'järgmine\' ja \'sarnased\' videod + Kuva vihjet \"lisamiseks hoia\" + Kuva vihje, kui videoandmete lehel vajutatakse tausta või hüpikakna nupule + URL pole toetatud + Sisu vaikimisi riik + Teenus + Sisu vaikimisi keel + Mängija + Käitumine + Heli ja pilt + Ajalugu ja vahemälu + Hüpikaken + Välimus + Muu + Silumine + Taasesitus taustal + Taasesitus hüpikaknas + Lisati taustapleieri järjekorda + Lisati hüpikpleieri järjekorda + Esita + Sisu + Kuva vanusepiiranguga sisu + Vanusepiiranguga sisu. Selle saab lubada seadetes. + Otse + Allalaadimised + Allalaadimised + Vea teatamine + Kõik + Kanal + Pleilist + Jah + Hiljem + Keelatud + Filter + Värskenda + Kustuta + Suuruse muutmine + Parim lahutus + Võta tagasi + Esita kõik + Alati + Üks kord + Fail + + NewPipe teavitus + Teavitused NewPipe tausta- ja hüpikpleierile + + [Tundmatu] + + Vaheta suunda + Lülita taustale + Lülita hüpikpleierile + Impordi andmebaas + Ekspordi andmebaas + Alistab praeguse ajaloo ja tellimused + Ekspordi ajalugu, tellimused ja pleilistid + Puhasta vaatamiste ajalugu + Kustutab vaadatud voogude ajaloo + Kustuta kogu vaatamiste ajalugu + Vaatamiste ajalugu kustutati. + Kustuta otsinguajalugu + Kustutab otsisõnade ajaloo + Kustuta kogu otsinguajalugu + Otsinguajalugu kustutati. + Viga + Võrgu viga + Kõiki pisipilte ei õnnestunud laadida + Video URLi dekrüptimine nurjus + Veebilehe töötlemine nurjus + Veebilehe täielik töötlemine nurjus + Sisu pole saadaval + GEMA poolt blokeeritud + Allalaadimismenüü seadistamine nurjus + See on OTSEVOOG, mis pole veel toetatud. + Ühtegi voogu ei leitud + Pildi laadimine nurjus + Rakendus jooksis kokku + Selle voo esitus nurjus + Ilmnes taastamatu pleieri viga + Pleieri veast taastumine + Välised pleierid ei toeta seda tüüpi linke + Kehtetu URL + Videovooge ei leitud + Helivooge ei leitud + Kehtetu kataloog + Kehtetu faili/sisu allikas + Fail puudub või puuduvad õigused seda lugeda või kirjutada + Tühi failinimi pole lubatud + Ilmnes viga: %1$s + Allalaaditavaid videovooge pole + + Vabandust, seda poleks pidanud juhtuma. + Teata veast e-posti kaudu + Vabandust, ilmnesid mõned vead. + TEATA + Info: + Mis juhtus: + Mis:\\nPäring:\\nSisu Keel:\\nTeenus:\\nGMT aeg:\\nPakett:\\nVersioon:\\nOS versioon: + Oma kommentaar (inglise keeles): + Üksikasjad: + + + Video eelvaate pisipilt + Video eelvaate pisipilt + Üleslaadiaja avatari pisipilt + Meeldib + Ei meeldi + Kasuta Tor võrku + (Eksperimentaalne) Kasuta allalaadimiseks Tor võrku, et suurendada privaatsust (voogesitus ei ole veel toetatud). + Teata veast + Kasutaja raport + Tulemusi pole + Siin pole veel midagi + Lohista järjestuse muutmiseks + + Allalaadimiskataloogi \'%1$s\' loomine nurjus + Loodi allalaadimiskataloog \'%1$s\' + + Video + Audio + Proovi uuesti + Pääsuõigused salvestile puuduvad + Kasuta vana mängijat + Vana sisseehitatud mediaframework pleier + + K + M + B + + Tellijaid pole + + %s tellija + %s tellijat + + + Pole vaadatud + + %s vaatamine + %s vaatamist + + + Videoid pole + + %s video + %s videot + + + "Start " + Paus + Esita + Loo + Kustuta + Kustuta üks + Kustuta kõik + Kontrollsumma + Loobu + Nimeta ümber + + Uus ülesanne + OK + + Faili nimi + Lõimed + Viga + Server pole toetatud + Fail on juba olemas + Vigane URL või internet pole saadaval + NewPipe allalaadimine + Puuduta üksikasjade nägemiseks + Palun oota… + Kopeeriti lõikepuhvrisse + Vali saadaolev allalaadimiste kataloog + Need õigused on vajalikud +\nhüpikakna avamiseks + Kustutati 1 element. + + "reCAPTCHA " + Laadi alla + Lubatud tähemärgid failinimedes + Vigased tähemärgid asendatakse selle väärtusega + Asendustähemärk + + Tähed ja numbrid + Erimärgid + + Selle faili esitamiseks puudub rakendus + + NewPipe rakendusest + Seaded + Programmist + Kolmanda osapoole litsentsid + Litsentsi laadimine nurjus + Ava veebileht + Programmist + Kaasautorid + Litsentsid + Panusta + Vaata GitHubis + Anneta + Veebileht + Enama info saamiseks külasta NewPipe veebilehte. + NewPipe privaatsuspoliitika + Loe privaatsuspoliitikat + NewPipe litsents + Loe litsentsi + + + Ajalugu + Otsitud + Vaadatud + Ajalugu on keelatud + Ajalugu + Ajalugu on tühi + Ajalugu kustutati + Element kustutati + Kas kustutada see kirje otsinguajaloost? + Kas kustutada see kirje vaatamiste ajaloost? + Kas kustutada kõik kirjed ajaloost? + Viimati esitatud + Enim esitatud + + Avalehe sisu + Tühi leht + Kioski leht + Tellimuse leht + Voo leht + Kanali leht + Vali kanal + Ühtki kanalit pole veel tellitud + Vali kiosk + Eksport tehtud + Import tehtud + Korralikku ZIP faili pole + Hoiatus: Kõiki faile ei õnnestunud importida. + See alistab praeguse seadistuse. + Kas importida ka seadistused? + + "Kiosk " + Trendid + "Top 50 " + Uus ja kuum + Taustapleier + Hüpikpleier + Eemalda + Üksikasjad + Heli seaded + Hoia järjekorda lisamiseks + Lisa järjekorda taustal + Lisa järjekorda hüpikaknaga + Alusta taasesitust siit + Alusta siit taustal + Alusta siit hüpikaknaga + + Ava sahtel + Sulge sahtel + Siia ilmub varsti midagi ;D + + + Eelistatud \'ava\' toiming + Vaikimisi tegevus sisu avamisel — %s + + Videopleier + Taustapleier + Hüpikpleier + Küsi alati + + Info hankimine… + Soovitud sisu laadimine + + Loo uus pleilist + Kustuta pleilist + Nimeta pleilist ümber + Nimi + Lisa pleilisti + Määra pleilisti pisipildiks + + Lisa pleilist järjehoidjaks + Eemalda järjehoidja + + Kas kustutada see pleilist? + Pleilist loodud + Lisati pleilisti + Pleilisti pisipilt muudetud + Pleilisti kustutamine nurjus + + Subtiitriteta + + Mahuta + Täida + Suumi + + Automaatselt loodud + + Subtiitrid + Kohanda pleieri subtiitrite teksti suurust ja tausta. Jõustamiseks tuleb rakendus taaskäivitada + + Mälulekke seire võib põhjustada rakenduse hangumise + + Import/eksport + Import + Impordi asukohast + Ekspordi asukohta + + Import… + Eksport… + + Impordi fail + Eelmine eksport + + Tellimuste import nurjus + Tellimuste eksport nurjus + + Impordi YouTube tellimused eksportfaili abil: +\n +\n1. Ava URL: %1$s +\n2. Logi sisse +\n3. Allalaadimine peaks algama (see on eksportfail) + See toiming võib põhjustada suurt võrguliiklust. +\n +\nKas jätkata? + + Taasesituse kiiruse juhtimine + "Tempo " + Vaikimisi + + Nõustu + Keeldu + + Piiranguta + Piira lahutust mobiilse andmeside kasutamisel + diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index d0da3d364..9f9fceb8b 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -328,21 +328,21 @@ Kendu cache-ko wegbuneen datu guztiak Metadatuen cache-a ezabatuta Gehitu ilarara hurrengo jarioa - Gehitu erlazionatutako jario bat azken jarioa jo bitartean errepikapenik gabeko ilara batean. + Gehitu erlazionatutako jario bat azken jarioa jo bitartean errepikapenik gabeko ilara batean Arazketa Fitxategia Inportatu datu-basea Esportatu datu-basea Zure uneko historiala eta harpidetzak gainidatziko ditu - Esportatu historiala, harpidetzak eta erreprodukzio-zerrendak. + Esportatu historiala, harpidetzak eta erreprodukzio-zerrendak Garbitu ikusitakoaren historiala - Jotako jarioen historiala ezabatzen du. - Ezabatu ikusitakoaren historial osoa. + Jotako jarioen historiala ezabatzen du + Ezabatu ikusitakoaren historial osoa Ikusitakoaren historiala ezabatuta. Garbitu bilaketa historiala - Ezabatu bilaketa gakoen historiala. - Ezabatu bilaketen historial osoa. + Ezabatu bilaketa gakoen historiala + Ezabatu bilaketen historial osoa Bilaketen historiala ezabatuta. Direktorio baliogabea Fitxategi edo edukiaren iturri baliogabea @@ -407,7 +407,7 @@ Automatikoki sortuak Azpitituluak - Aldatu azpitituluen testuaren eskala eta atzealdeko estiloa. Aplikazioa berrabiarazi behar da aldaketak aplikatzeko. + Aldatu azpitituluen testuaren eskala eta atzealdeko estiloa. Aplikazioa berrabiarazi behar da aldaketak aplikatzeko Gaitu LeakCanary Memoria galeren monitorizazioa. Aplikazioak agian ez du erantzungo memoriaren aitortza egin bitartean @@ -453,7 +453,7 @@ Ezarpenak ere inportatu nahi dituzu? Bilaketa ez zehatzak posizioak azkarrago baina prezisio gutxiagoz bilatzea ahalbidetzen du - "Desgaitu iruditxoak kargatzea gelditzeko eta datuak eta memoria aurrezteko. Hau aldatzean memoria eta diskoko irudien cache-ak garbituko dira." + "Desgaitu iruditxoak kargatzea gelditzeko eta datuak eta memoria aurrezteko. Hau aldatzean memoria eta diskoko irudien cache-ak garbituko dira" NewPipe Software Librea eta Copyleft da: Nahi eran erabili, ikertu, partekatu eta hobetu dezakezu. Zehazki, elkarbanatzea eta aldatzea Free Software Foundation-ek argitaratutako GNU General Public License-ren 3. bertsioa edo berriagoren baten terminoen arabera egiteko baimena duzu. Behartu aktibitatearen bizitza ziklotik kanpo baztertu eta gero entregatu ezin diren Rx salbuespenen inguruko txostena @@ -468,4 +468,18 @@ Mugagabea Mugatu bereizmena datu mugikorrak erabiltzean + Aurreratu azkar isilunea dagoenean + Urratsa + Leheneratu + + Minimizatu aplikazioa aldatzean + Ekintza bideo erreproduzigailu nagusitik beste aplikazio batera aldatzean — %s + Bat ere ez + Minimizatu bigarren planoko erreproduzigailura + Minimizatu laster-liho erreproduzigailura + +Kanalak + Erreprodukzio-zerrendak + Pistak + Erabiltzaileak diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 3ab89ec54..e74c948fd 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -384,7 +384,7 @@ Recherche rapide approximative Permettre au lecteur d\'accéder plus rapidement à une position au détriment de la précision Charger miniatures - Désactiver pour arrêter le chargement de toutes les miniatures et sauvegarder les données et l\'utilisation de la mémoire. Modifier cela effacera à la fois le cache d\'image en mémoire et sur disque. + Désactiver pour arrêter le chargement de toutes les miniatures et sauvegarder les données et l\'utilisation de la mémoire. Modifier cela effacera à la fois le cache d\'image en mémoire et sur disque Images en cache effacées Effacer les données en cache Effacer toutes les pages web mises en cache @@ -403,7 +403,7 @@ Télécharger le fichier de flux Vidéo suivante en file d\'attente - Ajout automatique d\'un flux connexe lors de la lecture du dernier flux dans une file d\'attente non répétitive. + Ajout automatique d\'un morceau suggéré lors de la lecture du dernier morceau dans une file d\'attente non bouclée. Débogage Remplir Générés automatiquement @@ -434,11 +434,11 @@ \n3. Un téléchargement va démarrer (ce sera celui du fichier nécessaire à l\'importation des abonnements)" 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 : +\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. Suivez ce : %1$s -\n3. Connectez-vous à votre compte +\n1. Activer le \"mode bureau\" dans votre navigateur (le site n\'est pas accesible en mode mobile) +\n2. Suivez cette URL : %1$s +\n3. Connectez-vous à votre compte \n4. Copier l\'URL vers lequel vous venez d\'être redirigé (qui est l\'URL de votre profil) votreID, soundcloud.com/votreid @@ -448,7 +448,7 @@ Vitesse de lecture Cadence - Unhook (déformations possibles) + Détacher (déformations possibles) Défaut Ouvrir de préférence avec Action par défaut lors de l\'ouverture de contenu - %s @@ -485,4 +485,18 @@ \nVous devez l\'accepter pour nous envoyer le rapport de bug. Pas de limite Limiter la résolution en données mobile - + Chaînes + Listes de lecture + Pistes + Utilisateurs + Accélérer pendant les silences + Étape + Réinitialiser + + Minimiser lors du changement d\'application + Action lors du changement d\'applications depuis le lecteur vidéo —%s + Aucune + Lecture en arrière plan + Mettre en lecteur popup + + diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 7480fdbff..6468079f4 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -32,7 +32,7 @@ הכנס נתיב לשמירת קבצי שמע נגן אוטומטית - הפעל סרטון אוטומטית כאשר NewPipe נפתח דרך אפליקציה אחרת + מנגן סרטון כאשר NewPipe נפתח דרך אפליקציה אחרת רזולוציית ברירת המחדל רזולוציית ברירת המחדל לחלון צץ הצג רזולוציות גבוהות יותר @@ -119,7 +119,7 @@ היסטוריית חיפושים שמור חיפושים מקומית - היסטוריה + היסטוריה ומטמון המשך לעקוב אחר סרטונים שנצפו המשך לנגן בעת חזרת המיקוד ליישום המשך לנגן לאחר הפרעות (לדוגמה: שיחות טלפון) @@ -386,4 +386,16 @@ \n3. ההורדה אמורה להתחיל (זהו קובץ היצוא) קצב ברירת מחדל - +השתמש בחיפוש מהיר שאינו מדויק + חיפוש לא מדויק מאפשר לנגן לחפש נקודת זמן מהר יותר, עם דיוק מופחת + טען תמונות ממוזערות + "השבת כדי לעצור את טעינת כל התמונות הממוזערות וחסוך בשימוש בנתונים ובזכרון. השינוי ימחק את המטמון בזכרון ובדיסק. " + הסר את כל נתוני העמודים במטמון + אוטומטית הכנס לתור את ההזרמה הבאה + "אוטומטית הוסף הזרמה קשורה כאשר ההזרמה האחרונה לא נמצאת במצב הזרמה חוזרת של התור. " + כפתור כיוון + החלף לראשי + + פעולה זו תדרוס את ההיסטוריה ורשימת המנויים הקיימת. + "מחק הסטוריה של ניגונים קודמים. " + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index fb5d9405b..18bbc6d3e 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -186,7 +186,7 @@ Informazioni Licenze di terze parti © %1$s di %2$s protetto da licenza %3$s - Impossible caricare il contratto di licenza + Impossible caricare la licenza Visita il sito Informazioni Contributori @@ -218,7 +218,7 @@ Scarica Caratteri ammessi nei nomi dei file - I caratteri non validi vengono sostituiti con questo + I caratteri non validi vengono sostituiti con Carattere sostitutivo Lettere e cifre @@ -325,9 +325,9 @@ Nessun flusso video trovato Nessun flusso audio trovato - Riproduttore video - Riproduttore di fondo - Riproduttore a comparsa + Lettore video + Riproduzione in sottofondo + Riproduzione in modalità popup Chiedi sempre Raccogliendo informazioni… @@ -418,7 +418,7 @@ Importa/Esporta Importa Importa da - Esporta a + Esporta in Importando… Esportando… @@ -454,7 +454,7 @@ Controlli della velocità di riproduzione Tempo Tono - Slega (può causare distorsione) + Scollega (può causare distorsione) Nightcore Valore predefinito Nessun flusso disponibile per il download @@ -472,7 +472,7 @@ Elimina l\'intera cronologia visualizzazioni. Cronologia visualizzazioni eliminata. Pulisci cronologia delle ricerche - Elimina la cronologia delle ricerche effettuate. + Cancella la cronologia dei termini di ricerca. Elimina l\'intera cronologia delle ricerche. Cronologia delle ricerche eliminata. 1 elemento eliminato. @@ -491,4 +491,14 @@ Senza limiti Limita la risoluzione quando si utilizzano dati mobili - + Avanzamento veloce durante il silenzio + Step + Reset + + Minimizza al cambio applicazione + Azione quando si passa ad un\'altra applicazione dal lettore video principale — %s + Nessuna + Minimizza al lettore in sottofondo + Minimizza al lettore popup + + diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index f93ce3119..1ae02a0f2 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -444,4 +444,15 @@ Преводи Смени ја големината и стилот на преводот. Потребен е рестарт за промена. + NewPipe - политика за приватност + Проектот NewPipe сериозно ја сфаќа вашата приватност. Затоа апликацијата не собира ваши податоци без ваша дозвола. +\nПолитиката за приватност на NewPipe детално објаснува кои податоци се зачувани и пратени кога праќате извештај за грешка во апликацијата. + Прочитај ја политиката за приватност + За да постапуваме соодветно со регулацијата за заштита на податоци (GDPR) на ЕУ, вараме да обрнете внимание на политиката за приватност на NewPipe. +\nВе молиме прочитајте ја внимателно. Мора да ја прифатите за да ни го испратите извештајот за грешка во апликацијата. + Прифати + Отфрли + + Неограничено + Ограничи резолуција при користење мобилен интернет diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index af771c971..0f8d9b55a 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -477,4 +477,18 @@ Avslå Ubegrenset Begrens oppløsning når mobildata brukes + Minimer ved programbytte + Handling ved bytting til annet program fra hovedspiller — %s + Ingen + Minimer til bakgrunnsspiller + Minimer til oppsprettsspiller + +Kanaler + Spillelister + Spor + Brukere + Hurtig foroverspoling ved stillhet + Steg + Tilbakestill + diff --git a/app/src/main/res/values-nl-rBE/strings.xml b/app/src/main/res/values-nl-rBE/strings.xml index b4c378fc4..3c15cf226 100644 --- a/app/src/main/res/values-nl-rBE/strings.xml +++ b/app/src/main/res/values-nl-rBE/strings.xml @@ -448,4 +448,14 @@ Weigeren Onbeperkt Resolutie beperken bij gebruik van mobiele gegevens + Minimaliseren bij overschakelen naar anderen app + Actie bij overschakelen van videospeler naar anderen app — %s + Geen + Afspelen in achtergrond + Afspelen in pop-up + +Doorspoelen tijdens stilte + Stap + Standaardwaarden herstellen + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 3d0b552af..358e76fde 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -331,7 +331,7 @@ te openen in pop-upmodus Database importeren Database exporteren Dit zal je huidige geschiedenis en abonnementen overschrijven - Exporteer geschiedenis, abonnementen en speellijsten. + Exporteer geschiedenis, abonnementen en speellijsten Export voltooid Import voltooid Geen geldig ZIP-bestand @@ -399,7 +399,7 @@ te openen in pop-upmodus Snelle, minder exact spoelen gebruiken Minder exact spoelen laat de speler sneller posities zoeken met verminderde precisie Volgende stream automatisch in wachtrij plaatsen - Automatisch een gerelateerde stream toekennen bij het afspelen van de laatste stream in een niet-herhalende afspeelwachtlijst. + Automatisch een gerelateerde stream toekennen bij het afspelen van de laatste stream in een niet-herhalende afspeelwachtlijst SYNCHRONISEREN Bestand @@ -441,7 +441,7 @@ te openen in pop-upmodus \n \nWil je doorgaan? Miniatuurvoorbeelden laden - Schakel dit uit om alle miniatuurvoorbeelden niet meer te laden; dit bespaart gegevens en geheugen. Het wijzigen van deze instelling wist het geheugen en de afbeeldingscache. + Schakel dit uit om alle miniatuurvoorbeelden niet meer te laden; dit bespaart gegevens en geheugen. Het wijzigen van deze instelling wist het geheugen en de afbeeldingscache Afbeeldingscache gewist Gecachete metagegevens wissen Alle gecachete webpagina-gegevens wissen @@ -449,7 +449,7 @@ te openen in pop-upmodus Afspeelsnelheidbesturing Tempo Toon - Ontkoppelen (kan ruis veroorzaken) + Ontlinken (kan ruis veroorzaken) Nightcore Standaard Voorkeursactie voor openen @@ -458,17 +458,17 @@ te openen in pop-upmodus Geen streams beschikbaar voor downloaden Bijschriften - Bijschriftgrootte en achtergrondstijlen wijzigen. Vereist een herstart van de app. + Bijschriftgrootte en achtergrondstijlen wijzigen. Vereist een herstart van de app Er is geen app geïnstalleerd die dit bestand kan afspelen Kijkgeschiedenis wissen - Verwijdert de geschiedenis van afgespeelde streams. - Verwijdert de gehele kijkgeschiedenis. + Verwijdert de geschiedenis van afgespeelde streams + Verwijdert de gehele kijkgeschiedenis Kijkgeschiedenis verwijderd. Zoekgeschiedenis wissen - Verwijdert de gebruikte zoektermen. - Verwijdert de gehele geschiedenis. + Verwijdert de gebruikte zoektermen + Verwijdert de gehele geschiedenis Zoekgeschiedenis verwijderd. 1 item verwijderd. @@ -485,4 +485,18 @@ te openen in pop-upmodus Weigeren Ongelimiteerd Resolutie beperken bij gebruik van mobiele gegevens + Minimaliseren bij overschakelen naar andere app + Actie bij overschakelen van videospeler naar andere app — %s + Geen + Afspelen op achtergrond + Afspelen in pop-upvenster + +Vooruitspoelen tijdens stilte + Stap + Standaardwaarden + + Kanalen + Afspeellijsten + Nummers + Gebruikers diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index ca31bab9e..0889fa9e3 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -266,7 +266,7 @@ Większość znaków specjalnych Dotacja - NewPipe rozwijane jest przez wolontariuszy, którzy poświęcają swój wolny czas, by zapewnić ci jak najlepsze wrażenia podczas korzystania z aplikacji. To dobry moment, aby wesprzeć programistów i sprawić, by program był jeszcze lepszy, nie przerywając przy tym popijania kawy! + NewPipe rozwijane jest przez wolontariuszy, którzy poświęcają swój wolny czas, by zapewnić ci jak najlepsze wrażenia podczas korzystania z aplikacji. To dobry moment, aby wesprzeć programistów i sprawić, by program był jeszcze lepszy, nie przerywając przy tym popijania kawy. Daj od siebie Witryna By otrzymać więcej informacji oraz najnowsze wiadomości o NewPipe, odwiedź naszą stronę. @@ -473,4 +473,14 @@ NewPipe jest wolnym i bezpłatnym oprogramowaniem: Możesz używać, brać udział oraz udoskonalać je według własnego uznania. W szczególności możesz redystrybuować lub/i modyfikować pod zasadami GNU General Public License opublikowanej przez Free Software Fundation w wersji 3 lub (w zależności od ciebie) dowolnej późniejszej wersji. Czy chcesz zaimportować również ustawienia? + Polityka prywatności programu NewPipe + Projekt NewPipe bardzo poważnie traktuję politykę prywatności. Aplikacja nie zbiera żadnych danych bez Twojej zgody. Polityka prywatności programu NewPipe szczegółowo wyjaśnia, jakie dane są zbierane i przesyłane, kiedy wysyłasz raport o błędach aplikacji. + Przeczytaj politykę prywatności + Ze względu na wprowadzenie Ogólnego Rozporządzenia o ochronie danych (RODO), chcielibyśmy zwrócić Twoją uwagę na politykę prywatności NewPipe. Proszę przeczytać ją uważnie. +\nMusisz ją zaakceptować, aby przesyłać raporty o błędach. + Akceptuj + Odrzuć + + Brak limitu + Limit przy użyciu danych mobilnych diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index b958caaa5..3ec466bba 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -100,7 +100,7 @@ Threads URL inválida ou internet indisponível Selecione uma pasta para download - Nenhum player de stream encontrado. Deseja Instalar o VLC? + Nenhum player de stream encontrado. Deseja instalar o VLC? Não foi possível interpretar o site Áudio Reproduzir @@ -310,7 +310,7 @@ abrir em modo popup Importar base de dados Exportar base de dados Isso irá sobrescrever seu histórico e inscrições - Exportar histórico, inscrições e listas de reprodução. + Exportar histórico, inscrições e listas de reprodução Exportação completa Importação completa Não há nenhum arquivo ZIP válido @@ -379,7 +379,7 @@ abrir em modo popup Usar índice de indexação rápido porém não preciso Usar índice de indexação inexato Adicionar a próxima stream à fila automaticamente - Auto anexar uma stream relacionada quando a reprodução iniciar na última stream em uma fila não repetitiva. + Auto anexar uma stream relacionada quando a reprodução iniciar na última stream em uma fila não repetitiva Sincronizar Arquivo @@ -427,9 +427,9 @@ abrir em modo popup Cache de metadados foi limpo Controles de Velocidade de Reprodução "Tempo " - Desabilite para para de carregar todas as miniaturas e economizar dados e uso de memória. Alterar esta configuração irá limpar o cache de imagens na memória e em disco. + Desabilite para para de carregar todas as miniaturas e economizar dados e uso de memória. Alterar esta configuração irá limpar o cache de imagens na memória e em disco Passo - Desenganchar + Desvincular (pode causar distorção) Padrão Ação de \'abrir\' preferida Ação padrão quando abrir conteúdo — %s @@ -439,18 +439,18 @@ abrir em modo popup Abrir gaveta Fechar gaveta Legendas - Modifique o tamanho da legenda e o estilo da tela de fundo. Necessário reiniciar o aplicativo para ter efeito. + Modifique o tamanho da legenda e o estilo da tela de fundo. Necessário reiniciar o aplicativo para ter efeito Night-core Nenhum player instalado para reproduzir este arquivo "Limpar histórico de já assistidos " - Deleta o histórico de videos já reproduzidos. - Deleta todo o histórico de já reproduzidos. + Deleta o histórico de videos já reproduzidos + Deleta todo o histórico de já reproduzidos Histórico de já assistidos deletado. Limpar histórico de pesquisa - Deleta histórico de palavras chave pesquisadas. - Deletar todo o histórico de pesquisa. + Deleta histórico de palavras chave pesquisadas + Deletar todo o histórico de pesquisa Histórico de pesquisa deletado. 1 item deletado. @@ -467,4 +467,19 @@ abrir em modo popup Recusar Ilimitado Limitar resolução quando dados móveis estiverem em uso + Minimizar ao trocar de aplicativo + Ação ao trocar de aplicativo quando estiver no reprodutor de vídeo principal — %s + "Nenhuma " + Minimizar para reprodutor em plano de fundo + Minimizar para reprodutor popup + +Avançar rapidamente durante silêncio + Parar +\n + Reiniciar + + Canais + Listas de reprodução + Faixas + Usuários diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 96c9674cd..4fc30e90e 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -2,36 +2,36 @@ %1$s просмотров Опубликовано %1$s - Ни одного потокового проигрывателя не было найдено. Установить VLC? + Потоковый плеер не найден. Установить VLC? Установить Отмена Открыть в браузере Поделиться Скачать - Найти + Поиск Настройки Возможно, вы имели в виду: %1$s? Поделиться с помощью Выбрать браузер поворот - Папка для загрузки видео + Путь загрузки видео Папка для хранения загруженных видео Введите путь к папке для загрузки видео Разрешение по умолчанию - Воспроизвести с помощью Kodi + Воспроизвести в Kodi Приложение Kore не найдено. Установить его? - Показывать опцию «Воспроизвести с помощью Kodi» - Показать опцию воспроизведения видео через Kodi media center + \"Воспроизвести в Kodi\" + Показать опцию воспроизведения видео через медиацентр Kodi Аудио Формат аудио по умолчанию - WebM — свободный формат - M4A — лучше качество + WebM — свободный + M4A — выше качество Скачать Следующее видео URL не поддерживается - Показывать \'следующее\' и \'предложенные\' видео + \"Следующее\" и \"Предложенные\" Язык контента по умолчанию - Видео и Аудио + Видео и аудио Внешний вид Другое @@ -40,8 +40,8 @@ Миниатюра аватара пользователя Не понравилось Понравилось - Использовать внешний видеоплеер - Использовать внешний аудиоплеер + Внешний видеоплеер + Внешний аудиоплеер Воспроизведение в фоновом режиме Тема Тёмная @@ -51,7 +51,7 @@ Ошибка сети Использовать Tor - Папка для загрузки аудио + Путь загрузки аудио Папка для хранения загруженных аудио Введите путь к папке для загрузки аудио @@ -59,7 +59,7 @@ Подождите… Файл уже существует Потоки - OK + ОК Начать Пауза Удалить @@ -69,17 +69,17 @@ Ошибка Сервер не поддерживается NewPipe скачивает - Неправильный URL или нет доступа к интернету - Нажмите для деталей + Неверный URL или нет доступа к интернету + Подробности Скопировано в буфер обмена Выберите доступную папку для загрузки - Показывать контент с ограничением по возрасту + Контент 18+ Ошибка - Ваш комментарий (на английском): - Невозможно создать папку для загрузки \'%1$s\' - Воспроизводить автоматически + Ваш комментарий (English): + Не удалось создать папку для загрузки \"%1$s\" + Автовоспроизведение Воспроизводить видео при вызове NewPipe из другого приложения Контент Видео с возрастными ограничениями. Разрешить подобный контент можно в настройках. @@ -95,29 +95,29 @@ Не удалось создать меню загрузки Это прямая трансляция, они пока не поддерживаются. Не удалось загрузить изображение - "Падение приложения/пользовательского интерфейса " + Падение приложения/UI Простите, это не должно было произойти. - Отправить отчёт об ошибке по электронной почте + Отправить отчёт по e-mail Простите, произошли ошибки. ОТЧЁТ Информация: Что произошло: - Детали: + Подробности: (Экспериментально) Загружать через Tor для повышения конфиденциальности (прямые трансляции пока не поддерживаются). Сообщить об ошибке Сообщить о нарушении - Создана папка для загрузок \'%1$s\' + Создана папка для загрузок \"%1$s\" Видео Аудио Повторить попытку - В доступе к хранилищу было отказано + Нет доступа к накопителю Не удалось загрузить все миниатюры Не удалось расшифровать подпись URL у видео - Не удалось найти ни один поток + Не удалось найти ни одного потока Воспроизвести @@ -125,19 +125,19 @@ В фоне В окне - Только некоторые устройства могут воспроизводить 2K/4K видео + Только некоторые устройства могут воспроизводить видео в 2K/4K Формат видео по умолчанию Чёрная - Запоминать размер и положение всплывающего окна - Использовать жесты для изменения яркости и громкости + Восстановить окно + Изменять яркость и громкость жестами Всплывающее окно Воспроизведение во всплывающем окне Канал Да Обновить Очистить - Использовать старый плеер - Контроль жестов + Старый плеер + Управление жестами Всё Фильтр @@ -149,27 +149,27 @@ reCAPTCHA Открыть в отдельном окне - Показывать подсказки в поиске + Отображать подсказки при поиске Позже Отключено Изменение размера - Для некоторых видео может отсутствовать звук, если включена эта опция + В некоторых разрешениях НЕ будет звука, если эта опция выбрана  млн.  млрд.  тыс. - Разрешение в режиме всплывающего окна - Запоминать последний размер и положение всплывающего окна - Поисковые подсказки + Разрешение всплывающего окна + Запоминать размер и положение всплывающего окна + Варианты поиска Лучшее разрешение Старый встроенный плеер на Mediaframework Запрос reCAPTCHA Запрошен ввод reCAPTCHA - Показывать более высокое разрешение + Высокие разрешения NewPipe в окне О NewPipe Настройки @@ -182,7 +182,7 @@ О приложении Участники Прочитать лицензию - Библиотека лёгкого потокового воспроизведения на Android. + Свободное легковесное потоковое воспроизведение на Android. Открыть на GitHub Приветствуется всё — идеи, перевод, изменения дизайна, чистка кода или огромные изменения в коде. Чем больше сделано, тем лучше! © %1$s %2$s под лицензией %3$s @@ -198,9 +198,9 @@ История поиска Хранить поисковые запросы локально - История и Кэш + История и кэш Запоминать просмотренные видео - Возобновлять при возврате фокуса + Возобновить при фокусе Возобновлять воспроизведение после перерывов (например, телефонных звонков) @@ -208,12 +208,12 @@ Уведомления для NewPipe в фоне и во всплывающем окне Загрузки - Допустимые символы в именах файлов + Допустимые символы имён файлов Недопустимые символы заменяются на этот Символ для замены Буквы и цифры - Большинство специальных символов + Большинство спецсимволов История История поиска @@ -225,12 +225,12 @@ Плеер Поведение - История и кеш + История и кэш Плейлист Отменить Нет результатов - Тут только сверчки + Ничего нет Нет подписчиков @@ -277,74 +277,74 @@ Настройки аудио Пока нет подписок на каналы Удалить - Отписаться + Вы подписаны Подписка отменена - Показать \"держать, чтобы добавить\" подсказку - Показывать подсказку при нажатии на иконку «В окне» или «В фоне» на странице сведений о видео + \"Зажмите, чтобы добавить\" + Показать подсказку при нажатии \"В окне\" или \"В фоне\" на странице сведений о видео [Неизвестно] - Восстановление после ошибки проигрывателя + Восстановление после ошибки плеера В фоне В окне Зажмите, чтобы добавить в очередь - Добавить в очередь «В фоне» - Добавить в очередь «В окне» - Воспроизвести тут - Воспроизвести в фоне - Воспроизвести в окне -Ни одного потокового проигрывателя не было найдено (вы можете установить VLC) + В очередь \"В фоне\" + В очередь \"В окне\" + Видеоплеер + Фоновый плеер + Плеер в окне +Потоковый плеер не найден (вы можете установить VLC) Страна контента по умолчанию Сервис Всегда - Только один раз + Только сейчас Переключить ориентацию Перейти в фон Перейти в окно Перейти в главное окно - Ошибка проигрывателя без возможности восстановления - Внешние проигрыватели не поддерживают ссылки этих типов + Ошибка плеера без возможности восстановления + Внешние плееры не поддерживают ссылки этих типов Неверная ссылка Потоки видео не найдены Потоки аудио не найдены Пожертвовать - NewPipe разрабатывается волонтерами, которые проводят время, принося вам лучший опыт. Вернитесь, чтобы помочь разработчикам сделать NewPipe еще лучше, наслаждаясь чашечкой кофе. + Разработчики NewPipe ценой своего свободного времени делают вашу жизнь чуть удобнее. Отплатите им тем же — наслаждаясь чашечкой кофе, они смогут сделать NewPipe ещё лучше. Воздать должное Веб-сайт - Посетите сайт NewPipe для большей информации и новостей. - Открыть лоток - Закрыть лоток + Для получения более подробной информации и последних новостей о NewPipe посетите наш веб-сайт. + Открыть боковую панель + Закрыть боковую панель - Проигрыватель видео - Фоновый проигрыватель - Проигрыватель в окне + Видеоплеер + Фоновый плеер + Плеер в окне Всегда спрашивать - Получение информации… + Получение сведений… Загрузка запрошенного контента Загрузка файла прямой трансляции. - Показать информацию + Показать сведения Закладки Добавить к - Использовать быстрый, но неточный поиск - Неточный поиск позволяет плееру искать позицию быстрее, но с пониженной точностью - Автоматическая очередь следующего стрима - Автоматически добавлять связанные видео, при воспроизведении с последнего видео в неповторяющейся очереди. + Быстрый поиск позиции + Неточный поиск позволяет плееру искать позицию быстрее, но менее точно + Автодополнение очереди + Добавлять похожие потоки в очередь при воспроизведении последнего, если не включён повтор Отладка Файл Импорт данных Экспорт данных Ваша текущая история и подписки будут перезаписаны - Экспорт истории, подписок и плейлистов. - Неправильная директория - Неправильный файл/контент источника + Экспорт истории, подписок и плейлистов + Неверная папка + Неверный файл или источник контента Файл не существует или нет разрешения на его чтение или запись Имя файла не может быть пустым Произошла ошибка: %1$s @@ -357,41 +357,41 @@ Отклонить Переименовать - Вы хотите удалить этот элемент из истории поиска? - Вы уверены, что хотите удалить все элементы из истории? - Последнее проигрывание - Наиболее проигрываемые + Удалить этот элемент из истории поиска? + Удалить все элементы из истории? + Недавно проигранные + Часто проигрываемые Экспорт завершён Импорт завершён - Нет верного Zip файла - Предупреждение: нет возможности импорта всех файлов. - Это перезапишет вашу текущую установку. + Нет верного Zip-файла + Внимание: не все файлы были импортированы. + Текущие данные будут заменены. - Скоро что-то тут появится ;D + Скоро здесь кое-что появится ;D Всегда спрашивать - Создать новый плейлист + Создать плейлист Удалить плейлист Переименовать плейлист Имя Добавить в плейлист - Установить как иконку плейлиста + На миниатюру плейлиста Добавить плейлист в закладки Удалить закладку - Вы хотите удалить этот плейлист? + Удалить этот плейлист? Плейлист создан Добавлено в плейлист - Иконка плейлиста изменена - Нельзя удалить плейлист + Миниатюра плейлиста изменена + Не удалось удалить плейлист Без титров - Пригнать + Подогнать Заполнить Приблизить @@ -406,8 +406,8 @@ Включить LeakCanary Мониторинг утечки памяти может привести к зависанию приложения - Ошибки отчёта вне очереди - Принудительная отчетность об исключении исключаемых Rx исключений за пределами фрагмента или жизненного цикла деятельности после удаления + Сообщать об ошибках жизненного цикла + Принудительно сообщать о недоставляемых Rx-исключениях вне фрагмента или жизненного цикла после удаления Импорт/Экспорт Импорт @@ -420,58 +420,83 @@ Импорт файла Предыдущий экспорт - Нельзя импортировать подписки - Нельзя экспортировать подписки + Не удалось импортировать подписки + Не удалось экспортировать подписки Импорт подписок из YouTube загрузкой файла экспорта: \n \n1. Перейдите на: %1$s \n2. Войдите, если необходимо \n3. Должна начаться загрузка (это будет файл экспорта) - Импорт подписок из SoundCloud набрав либо URL или ваш ID: -\n + Импорт подписок из SoundCloud набрав либо URL, либо ваш ID: +\n \n1. Включите \"режим рабочего стола\" в браузере (сайт недоступен на телефоне) \n2. Пройдите на: %1$s \n3. Войдите, если надо \n4. Скопируйте адрес из адресной строки. вашID, soundcloud.com/вашID - Эта операция может использовать большие объемы сетевого трафика. + Это действие может вызвать большой расход трафика. \n \nПродолжить? Загружать миниатюры - Отключите, чтобы перестать загружать миниатюры и начать экономить трафик и память. Изменение настройки очистит кеш изображений в памяти и на диске. - Кеш изображений очищен - Очистить кеш метаданных - Кеш метаданных очищен + Отключите, чтобы не загружать миниатюры и сэкономить трафик и память. Изменение настройки очистит кэш изображений в памяти и накопителе + Кэш изображений очищен + Очистить кэш метаданных + Кэш метаданных очищен Управление скоростью воспроизведения Темп Тон - Независимо (может вызвать искажение) + Независимо (искажения) Nightcore По умолчанию Удалить все загруженные данные веб-страниц - Действие по умолчанию при открытии контента — %s + При открытии ссылки на контент — %s Нет потоков, доступных для загрузки Титры - Нет приложения, установленного для воспроизведения этого файла + Приложение для воспроизведения этого файла не установлено - Изменить размер заголовка и фоновые стили. Требуется перезапуск приложения для изменений. + Изменить размер и фон титров. Изменения вступят в силу после перезапуска Очистить историю просмотров - Удалить всю историю просмотров. - История просмотров была удалена. + Вся история просмотров будет удалена + История просмотров удалена. Очистить историю поиска - Удалить историю поисковых запросов. - Удаляет историю воспроизведенных потоков. - Удалить всю историю поиска. - Удалить историю поиска. + Удалить историю поисковых запросов + Удалить историю воспроизведённых потоков + Вся история поиска будет удалена + История поиска удалена. 1 элемент удалён. - NewPipe - программное обеспечение copyleft libre: вы можете использовать, изучать и улучшать его по своему усмотрению. В частности, вы можете распространять и/или изменять его в соответствии с GNU General Public License, опубликованным Free Software Foundation, либо версией 3 License, либо (по вашему выбору) любой более поздней версии. - Предпочтительное действие \'открытие\' - Желаете ли вы также импортировать настройки? + NewPipe — свободное программное обеспечение: вы можете использовать, изучать и улучшать его по своему усмотрению. В частности, вы можете распространять и/или изменять его в соответствии с условиями GNU General Public License, опубликованной Free Software Foundation, либо версии 3, либо (по вашему выбору) любой более поздней версии. + При открытии контента + Хотите импортировать настройки? - + Политика конфиденциальности NewPipe + Проект NewPipe очень серьёзно относится к вашей конфиденциальности. Поэтому приложение не собирает никаких данных без вашего согласия. +\nПолитика конфиденциальности NewPipe подробно объясняет, какие данные отправляются и хранятся при отправке отчёта о сбоях. + Прочитать политику + В соответствии с Общим регламентом по защите данных ЕС (GDPR), обращаем ваше внимание на политику конфиденциальности NewPipe. Пожалуйста, внимательно ознакомьтесь с ней. +\nВам необходимо принять её условия, чтобы отправить нам отчёт об ошибке. + Принять + Отклонить + + Без ограничений + Предел разрешения в мобильной сети + Каналы + Плейлисты + Дорожки + Пользователи + Пропускать тишину + Шаг + Сброс + + При сворачивании плеера + При переключении со стандартного плеера на другое приложение — %s + Ничего не делать + Фоновый плеер + Плеер в окне + + diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 6358190ef..6204e5fba 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -361,7 +361,7 @@ Vždy sa opýtať Získavajú sa informácie… - Požadovaný obsah sa načítava + Načítanie požadované obsahu Vytvoriť nový zoznam skladieb Vymazať zoznam skladieb @@ -374,7 +374,7 @@ Odstrániť Záložku Chcete odstrániť tento zoznam skladieb? - Zoznam skladieb bol vytvorený + Zoznam skladieb vytvorený Pridané do zoznamu skladieb Miniatúra zoznamu skladieb bola zmenená Nemožno odstrániť zoznam skladieb @@ -414,18 +414,18 @@ Čoskoro sa tu niečo objaví ;D - Preferovaná akcia pri otvorení + Preferovaná akcia \'otvoriť\' Predvolená akcia pri otváraní obsahu — %s Automaticky vygenerované Nastavenie titulkov - Upravte mierku textu titulkov a štýly pozadia. Vyžaduje reštartovanie prehrávača. + Upravte mierku textu titulkov a štýly pozadia. Vyžaduje reštart prehrávača. Povoliť službu LeakCanary Monitorovanie pretečenia pamäte môže spôsobiť, že aplikácia nebude reagovať - Nahlásiť mimocyklické chyby + Nahlásiť mimo-cyklické chyby Vynútenie hlásenia nedodržateľných výnimiek Rx, ktoré sa vyskytnú mimo časového cyklu fragmentu alebo aktivity po zlikvidovaní Import/Export @@ -451,14 +451,14 @@ \n2. Po výzve sa prihláste do svojho účtu \n3. Sťahovanie by malo začať (to je exportovaný zoznam) \n - "Pre importovanie vašich SoundCloud dát, musíte poznať adresu URL alebo ID vašeho profilu. Ak poznáte jedno alebo druhé, zadajte ho do nižšie uvedeného vstupu a ste pripravení ísť. -\n -\nAk nepoznáte ani URL ani ID vašeho profilu, môžete postupovať nasledovne: -\n -\n1. V niektorom prehliadači povoľte režim \"desktop\" (web nie je dostupný pre mobilné zariadenia) -\n2. Prejdite na túto adresu URL: %1$s -\n3. Po výzve sa prihláste do svojho účtu -\n4. Skopírujte adresu URL, na ktorú ste boli presmerovaní (to je adresa vášho profilu) + "Importovať SoundCloud profil zadaním URL adresy alebo vášho ID: +\n +\nAk nepoznáte ani URL ani ID vašeho profilu, môžete postupovať nasledovne: +\n +\n1. V niektorom prehliadači povoľte režim \"desktop\" (web nie je dostupný pre mobilné zariadenia) +\n2. Prejdite na túto adresu URL: %1$s +\n3. Po výzve sa prihláste do svojho účtu +\n4. Skopírujte adresu URL, na ktorú ste boli presmerovaní (to je adresa vášho profilu). \n" ID,soundcloud.com/ID @@ -483,4 +483,16 @@ 1 položka bola vymazaná. "NewPipe je slobodný softvér pod licenciou copyleft. Môžete ho používať, študovať a vylepšovať ako len chcete. Konkrétne ho môžete šíriť a/alebo upravovať pod podmienkami Všeobecnej verejnej licencie GNU, ako ju publikuje Free Software Foundation, buď verzia 3 licencie, alebo (podľa vašej voľby) ktorákoľvek neskoršia verzia." + Ochrana osobných údajov v NewPipe + NewPipe projekt berie vaše súkromie vážne. Preto aplikácia nezhromažďuje žiadne údaje bez vášho súhlasu. +\nNewPipe v ochrane súkromia podrobne vysvetľuje, aké údaje budú odoslané a uložené pri hlásení o páde. + Prečítajte si pravidlá ochrany osobných údajov + Chcete zároveň importovať aj nastavenia? + + V súlade s Európskym Všeobecným Nariadením o Ochrane Údajov (GDPR), chceme upriamiť vašu pozornosť na ochranu osobných údajov v NewPipe. Starostlivo si ich prečítajte. Musíte ich prijať pred nahlásením chyby. + Prijať + Odmietnuť + + Bez limitu + Limitovať rozlíšenie pri použití mobilných dát diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 77dd115a9..675514f17 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -4,7 +4,7 @@ Ingen strömspelare hittades. Vill du installera VLC? Installera Avbryt - Öppna i bläddrare + Öppna i webbläsare Öppna i popup-läge Dela Ladda ner @@ -12,7 +12,7 @@ Inställningar Menade du: %1$s ? Dela med - Välj bläddrare + Välj webbläsare rotering Använd extern videospelare Några upplösningar kommer INTE ha ljud när det här alternativet är aktiverat @@ -25,11 +25,11 @@ Genväg för att lagra nerladdade videor i Fyll i genvägen som videor ska laddas ner till - Plats för att ladda ner ljud till - Plats för att lagra nerladdat ljud i - Ange nerladdningsplats för ljudfiler + Genväg för nerladdning av ljud + Genväg för att lagra nerladdade ljudfiler i + Fyll i genvägen som ljudfiler ska laddas ner till - Spelar automatiskt upp en video när NewPipe öppnas av en annan app + Automatiskt spelar en video upp när NewPipe öppnas av en annan app Standardupplösning Standardupplösning för popup Visa högre upplösningar @@ -41,7 +41,7 @@ Ljud Standardformat för ljud Videoformat som föredras - WebM — fritt format + WebM — öppet format M4A — bättre kvalité Tema Ljus @@ -57,7 +57,7 @@ Ladda ner Nästa video - Visa nästkommande och liknande videor + Visa \'nästkommande\' och \'liknande\' videor Webbadressen stöds inte Standard innehållsspråk Video & Ljud @@ -120,18 +120,18 @@ Vad är nytt - Autospela + Spela upp automatiskt Sök historik Spara sökfrågor lokalt - Historik + Historik & Cacheminne Håll koll på videor som du tittat på Fortsätt då fokus fås Fortsätta spela efter avbrott (t.ex. telefonsamtal) - Visa \"Håll för att lägga till\" tips + Visa \"håll för att lägga till\" tips Visa tips när bakgrunds- eller popup-knappen trycks på sidan för videodetaljer Spelare Beteende - Historik + Historik & Cacheminne Tillagd till bakgrunds-spelar kön Tillagd till popup-spelar kön Spellista @@ -189,7 +189,7 @@ Inga videor %s video - %s videon + %s videor Start @@ -237,7 +237,7 @@ Om Medverkande Licenser - Gratis och enkel YouTube-app för Android. + Öppet och enkel Android app för mediastreaming. Visa på GitHub NewPipes licens Vad du än har för idéer gällande översättning, designändringar, kod rengöring eller riktigt tunga så är hjälp alltid välkommet. Ju mer som görs desto bättre blir det! @@ -280,8 +280,193 @@ Börja här i bakgrunden Börja här i popup Donera - NewPipe utvecklas av frivilliga som spenderar sin fritid på att ge dig den bästa användarupplevelsen. Nu är det tid att ge tillbaka för att säkerställa att utvecklarna kan göra NewPipe ännu bättre medan de njuter av en kopp kaffe! + NewPipe utvecklas av frivilliga som spenderar sin fritid på att ge dig den bästa användarupplevelsen. Nu är det tid att ge tillbaka för att säkerställa att utvecklarna kan göra NewPipe ännu bättre medan de njuter av en kopp kaffe. Ge tillbaka Webbplats För att få mer information och de senaste nyheterna om NewPipe, besök vår webbplats. - + Visa info + + Bokmärken + + Lägga till + + Använda snabb inexact sökning + Ladda miniatyrer + Inaktivera för att stoppa alla miniatyrbilder från att ladda och spara på data och minnesanvändning. Ändring av detta kommer att rensa cache-minnet + Bild cacheminnet var rensad + Tjänst + Debug + Alltid + Bara en gång + Fil + + Byta orientationen + Rensa metadatan i cacheminnet + Ingen strömspelare hittades (du kan installera VLC för att spela upp) + Ladda ned sändning. + "Ej exakt sökning tillåter spelaren att söka positioner snabbare med minskad precision " + Ta bort alla cachade webbsidor + Metadata cache rensad + "Köa nästa ström automatiskt " + Lägg automatiskt till en relaterad ström när du spelar den sista strömmen i en ej upprepad kö + Standard innehåll land + Kanaler + Spellistor + Spår + Användare + Växla till Bakgrunden + Växla till popup + Växla till main + + Importera databas + Exportera databas + Kommer att skriva över din nuvarande historik och prenumerationer + Exportera historik, prenumerationer och spellistor + Rensa visningshistorik + Tar bort historiken för spelade videoklipp + Ta bort hela visningshistoriken + Visningshistorik borttagen. + Rensa sökhistorik + Tar bort historiken för sökta nyckelord + Ta bort hela sökhistoriken + Sökhistorik borttagen. + Externa spelare stöder inte dessa typer av länkar + Ogiltig URL + Inga videoströmmar hittades + Inga ljudspår hittades + Ogiltig katalog + Ogiltig fil/innehålls källa + Filen finns inte eller otillräcklig behörighet att läsa eller skriva till den + Filnamnet får inte vara tomt + Ett fel uppstod: %1$s + Inga strömmar är tillgängliga för nedladdning + + Dra för att ändra ordning + + Skapa + Ta bort en + Ta bort alla + Avfärda + Döp om + + 1 objekt borttaget. + + Ingen app installerad för att spela upp filen + + NewPipes Sekretesspolicy + "NewPipe projektet tar din integritet på största allvar. Appen samlar därför inte in några uppgifter utan ditt medgivande. NewPipes Sekretesspolicy förklarar i detalj vad för data som skickas och lagras när du skickar en kraschrapport." + Läs sekretesspolicy + NewPipe är copyleft fri programvara: Du kan använda, studera, dela och förbättra den som du vill. Specifikt kan du distribuera och/eller modifiera det under villkoren för GNU General Public License som publicerats av Free Software Foundation, antingen version 3 av licensen, eller (om du så önskar) en senare version. + Vill du ta bort det här föremålet från visningshistoriken? + Är du säker du vill ta bort alla föremål från historiken? + Senast spelade + Mest spelade + + Exporteringen har slutförts + Importeringen har slutförts + Ingen giltig ZIP-fil + Varning: det gick inte att importera alla filer. + Det här kommer skriva över dina nuvarande inställningar. + Vill du också importera inställningar? + + Öppna navigationspanelen + Stäng navigationspanelen + Något kommer att visas här snart ;D + + + Föredra \'öppna\' åtgärden + Standardåtgärden när du öppnar innehåll — %s + + Videospelare + Bakgrundsspelare + Popup-spelare + Fråga alltid + + Hämtar information… + Laddar begärt innehåll + + Skapa ny spellista + Radera spellista + Byt namn på spellista + Namn + Lägg till i spellistan + Använd som spellistans miniatyrbild + + Bokmärk spellistan + Ta bort bokmärke + + Vill du ta bort den här spellistan? + Spellistan skapades + Tillagad i spellistan + "Spellistans miniatyrbild förändrades " + Kunde inte ta bort spellistan + + Ingen textning + + Passa + Fyll + Zoom + + Auto-genererade + + Textning + Ändra spelaren textskala och bakgrundsstil. Kräver att appen startar om för att träder i kraft + + Aktivera LeakCanary + Minnesläcka övervakning kan orsaka att appen inte svarar under heap dumpning + + Rapportera Out-of-lifecycle fel + Tvinga rapportering av otillåtna Rx-undantag utanför fragment eller aktivitetslivscykel efter uppstädning + + Importera/Exportera + Importera + Importera från + Exportera till + + Importera… + Exporterar… + + Importera fil + Föregående export + + Kunde inte importera prenumerationer + kunde inte exportera prenumerationer + + Importera YouTube-prenumerationer genom att hämta export-filen: +\n +\n1. Gå till följande webbadress: %1$s +\n2. Logga in när du blir tillfrågad +\n3. En nedladdning ska starta (det är export-filen) + Importera en SoundCloud profil genom att skriva in webbadressen eller ditt ID: +\n +\n1. Aktivera \"skrivbordsläge\" i en webbläsare (sidan är inte tillgänglig för mobila enheter) +\n2. Gå till följande webbadress: %1$s +\n3. Logga in när du blir tillfrågad +\n4. Kopiera profilens webbadress som du blev omdirigerad till. + dittID, soundcloud.com/dittid + + Tänk på att operationer kan vara nätverks intensiv. +\n +\nVill du fortsätta? + + Uppspelningshastighet Kontroller + Tempo + Pitch + Avlänka (kan orsaka förvrängning) + Snabbspola vid frånvaro av ljud + Steg + Återställ + + För att uppfylla den Europeiska dataskyddsförordningen (GDPR), uppmärksammar vi NewPipes sekretesspolicy. Läs den noggrant. Du måste acceptera det om du vill skicka felrapporten. + Acceptera + Avböj + + Ingen gräns + Begränsa upplösningen när du använder mobil data + "Minimera vid växling av applikationen " + Åtgärd när du växlar till andra applikationer från huvudvideospelare — %s + Ingen + Minimera till Bakgrunds-spelare + Minimera till popup-spelare + + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 924b2c6bf..15fbc4130 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -119,16 +119,16 @@ İndirme menüsü ayarlanamadı - Açılır pencere kipinde aç - NewPipe açılır pencere kipi + Açılır oynatıcı kipinde aç + NewPipe açılır oynatıcı kipi - Öntanımlı açılır pencere çözünürlüğü + Öntanımlı açılır oynatıcı çözünürlüğü Daha yüksek çözünürlükleri göster Yalnızca bazı aygıtlar 2K/4K video oynatmayı destekler Öntanımlı video biçimi Siyah - Açılır pencere kipinde oynatılıyor + Açılır oynatıcı kipinde oynatılıyor Tümü Kanal Evet @@ -148,7 +148,7 @@ M B - Bu izin açılır pencere kipinde + Bu izin açılır oynatıcı kipinde \naçmak için gerekli reCAPTCHA @@ -156,16 +156,16 @@ reCAPTCHA Formu istendi Arka Plan - Açılır Pencere + Açılır Oynatıcı Süz Yenile Temizle - Açılır pencere boyutunu ve konumunu hatırla - Açılır pencerenin ayarlandığı son boyutu ve konumu hatırla + Açılır oynatıcı boyutunu ve konumunu hatırla + Açılır oynatıcının ayarlandığı son boyutu ve konumu hatırla - Açılır Pencere + Açılır Oynatıcı Boyutlandırılıyor Bu seçenek etkinken bazı çözünürlüklerin sesi olmayacaktır @@ -224,7 +224,7 @@ Geri Al NewPipe Bildirimi - NewPipe Arka Plan ve Açılır Pencere Oynatıcıları için Bildirimler + NewPipe Arka Plan ve Açılır Oynatıcılar için Bildirimler Sonuç yok Burada Cırcır Böceklerinden Başka Şey Yok @@ -257,9 +257,9 @@ Öge silindi Bu içeriği arama geçmişinden silmek istiyor musunuz? \"Kuyruğa almak İçin bas\" ipucunu göster - Video ayrıntıları sayfasında arka plan veya açılır pencere düğmesine basıldığında ipucu göster + Video ayrıntıları sayfasında arka plan veya açılır oynatıcı düğmesine basıldığında ipucu göster Arka plan oynatıcıda kuyruğa eklendi - Açılır pencere oynatıcıda kuyruğa eklendi + Açılır oynatıcıda kuyruğa eklendi Tümünü Oynat [Bilinmeyen] @@ -283,16 +283,16 @@ En İyi 50 Yeni ve sıcak Arka Plan Oynatıcı - Açılır Pencere Oynatıcı + Açılır Oynatıcı Kaldır Ayrıntılar Ses Ayarları Kuyruğa Almak İçin Bas Arka Planda Kuyruğa Al - Açılır Pencerede Kuyruğa Al + Açılır Oynatıcıda Kuyruğa Al Burada Oynatmaya Başla Burada Arka Planda Başlat - Burada Açılır Pencerede Başlat + Burada Açılır Oynatıcıda Başlat Bağış yapın NewPipe, size en iyi deneyimi sunmak için zamanını harcayan gönüllüler tarafından geliştirilmiştir. Bir fincan kahveyi yudumlarken NewPipe\'ı daha da iyi yapmaları için geliştiricilere yardımcı olun. Geri ver @@ -302,7 +302,7 @@ Hizmet Yönelimi Değiştir Arka Plana Geç - Açılır Pencereye Geç + Açılır Oynatıcıya Geç Ana Görünüme Geç Çekmeceyi Aç @@ -318,7 +318,7 @@ Video oynatıcı Arka plan oynatıcı - Açılır pencere oynatıcı + Açılır oynatıcı Her zaman sor Bilgi alınıyor… @@ -326,7 +326,7 @@ Veri tabanını içe aktar Veri tabanını dışa aktar Şu anki geçmişinizi ve aboneliklerinizi geçersiz kılar - Geçmişi, abonelikleri ve oynatma listelerini dışa aktar. + Geçmişi, abonelikleri ve oynatma listelerini dışa aktar Dışa aktarım bitti İçe aktarım bitti Geçerli ZIP dosyası yok @@ -395,7 +395,7 @@ Hızlı isabetsiz konumlama kullan İsabetsiz konumlama, oynatıcının azaltılmış kesinlikle daha hızlı konumlama yapmasını sağlar Sonraki akışı kendiliğinden kuyruğa ekle - Yinelemeyen bir kuyruktaki son akış oynatılırken ilgili bir akışı kendiliğinden sonuna ekle. + Yinelemeyen bir kuyruktaki son akış oynatılırken ilgili bir akışı kendiliğinden sonuna ekle EŞZAMANLA Dosya @@ -437,7 +437,7 @@ \n \nDevam etmek istiyor musunuz? Küçük resimleri yükle - Küçük resimlerin hepsinin yüklenmesini engellemek ve bellek ve veri kullanımını azaltmak için devre dışı bırakın. Bunu değiştirmek, hem bellekteki hem de diskteki resim önbelleğini temizler. + Küçük resimlerin hepsinin yüklenmesini engellemek ve bellek ve veri kullanımını azaltmak için devre dışı bırakın. Bunu değiştirmek, hem bellekteki hem de diskteki resim önbelleğini temizler Resim önbelleği temizlendi Önbelleklenmiş üst veriyi temizle Önbelleklenmiş tüm web sayfası verisini kaldır @@ -446,7 +446,7 @@ Hız Öntanımlı Ses titreşimi - Çengeli Çıkar (bozukluğa neden olabilir) + Bağlantıyı kes (bozulmalara neden olabilir) Nightcore İndirmeye uygun akış yok @@ -454,17 +454,17 @@ İçerik açılırken öntanımlı eylem — %s Ek Açıklamalar - Oynatıcı ek açıklamalar metin boyutunu ve arka plan biçimlerini düzenle. Etki için uygulamayı yeniden başlatmak gerekir. + Oynatıcı ek açıklamalar metin boyutunu ve arka plan biçimlerini düzenle. Etki için uygulamayı yeniden başlatmak gerekir Bu dosyayı oynatmak için kurulan uygulama yok İzleme geçmişini temizle - Oynatılan akışların geçmişini siler. - Tüm izleme geçmişini sil. + Oynatılan akışların geçmişini siler + Tüm izleme geçmişini sil İzleme geçmişi silindi. Arama geçmişini temizle - Aranan anahtar sözcüklerin geçmişini siler. - Tüm arama geçmişini sil. + Aranan anahtar sözcüklerin geçmişini siler + Tüm arama geçmişini sil Arama geçmişi silindi. 1 öge silindi. @@ -481,4 +481,18 @@ Kabul etme Sınırsız Mobil veri kullanırken çözünürlüğü sınırla + Uygulama geçişinde küçült + Ana video oynatıcıdan başka bir uygulamaya geçişteki eylem — %s + Yok + Arka plan oynatıcıya küçült + Açılır oynatıcıya küçült + +Sessizlik sırasında hızlı ileri + Adım + Sıfırla + + Kanallar + Oynatma Listeleri + Parçalar + Kullanıcılar diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 755b447c9..313448323 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -192,7 +192,7 @@ Імпортувати базу Експортувати базу Це перепише чинну історію та підписання - Експортувати історію, підписання та плейлисти. + Експортувати історію, підписання та плейлисти Неможливо відтворити цей стрим Відновлююсь після помилки програвача Помилкова URL @@ -387,7 +387,7 @@ Використовувати неточне шукання Неточне шукання дозволяє програвачеві рухатися позиціями швидше, проте з меншою точністю Автоматично додати до черги наступний стрим - Автоматично додавати пов\'язаний стрим під час відтворення останнього у неповторювальній черзі. + Автоматично додавати пов\'язаний стрим під час відтворення останнього у неповторювальній черзі СИНХРОНІЗАЦІЯ Теки @@ -429,7 +429,7 @@ \n \nПродовжуватимете? Завантажити ескізи - Відключити аби зупинити завантаження ескізів та заощадити використання ресурсів та пам\'яті. Увімкнення функції призведе до повного вичищення кешу зображень. + Відключити аби зупинити завантаження ескізів та заощадити використання ресурсів та пам\'яті. Увімкнення функції призведе до повного вичищення кешу зображень Кеш зображень стерто Стерти кеш метаданих "Усунути всі кешовані дані веб-сторінки " @@ -446,17 +446,17 @@ Типова дія під час відкриття вмісту — %s Субтитри - Змінення маштабу тексту субтитрів та фонових стилів. Увімкнення функції потребує перезавантаження застосунку. + Змінення маштабу тексту субтитрів та фонових стилів. Увімкнення функції потребує перезавантаження застосунку Не знайдено відповідного застосунку для відтворення цього файла Очистити історію переглядів - Видаляє історію відтворень. - Видалити всю історію переглядів. + Видаляє історію відтворень + Видалити всю історію переглядів Історію переглядів було видалено. Очистити історію пошуків - Видаляє історію шуканих ключових слів. - Видалити всю пошукову історію. + Видаляє історію шуканих ключових слів + Видалити всю пошукову історію Пошукову історію було видалено. Видалено один фраґмент. @@ -473,4 +473,18 @@ Відхилити Безмежно Обмежити роздільну здатність під час користування мобільним інтернетом + Перемотувати підчас тиші + Крок + Скинути + + Зменшити при перемкненні застосунку + Дія при перемкненні до інших застосунків з головного відео-програвачу — %s + Жодних + Зменшити до фонового програвачу + Зменшити до віконного програвачу + +Канали + Плейлисти + Стежки + Користувачі diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index 74e45bad2..c653cf6e6 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -4,10 +4,19 @@ کوشائع ہوا انسٹال منسوخ کریں - اشتراک کریں + بانٹیں ڈاؤن لوڈکریں تلاش کریں - نتائج دکھارہاہےبرائے + کیا آپ کا مطلب تھا انٹرنیٹ میں کھولیں ترتیبات + کوئیstream پلیئر نہیں ملا.کیا آپ VLC انسٹال کرنا چاہتے ہیں؟ + کوئیstream پلیئر نہیں ملا.(آپ VLC انسٹال کر سکتے ہیے) + PopUp موڈ میں کھولیں + سٹریم فائل ڈاؤن لوڈ کریں. + کے ساتھ بانٹیں + Internet Browser منتخب کریں +\n + rotation کرو + external video player استعمال کریں diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 1d3121a00..5118b42d6 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -34,7 +34,7 @@ 下载 即将播放 - 显示下一部和相近的视频 + 显示下一部和相似的视频 不支援此网址 默认内容语言 视频和音频 @@ -88,7 +88,7 @@ 重试 无权访问存储空间 自动播放 - NewPipe被其他应用调用时,自动播放视频 + 当NewPipe被其他应用调用时,自动播放视频 直播 点击搜索开始NewPipe @@ -155,7 +155,7 @@ 在搜索时显示搜索建议 搜索记录 在本地存储搜索请求 - 历史 + 历史和缓存 记住观看的视频 焦点恢复 在打扰(例如来电)过后恢复视频播放 @@ -204,7 +204,7 @@ 关于 贡献者 许可 - 一款开源、轻量级的安卓 YouTube 客户端。 + 开源的轻量级流媒体Android客户端。 在 GitHub 上查看 NewPipe 许可 无论你有什么想法:翻译程序,改进设计,优化代码或是想做出大量修改——我们都随时欢迎。做得越多它将变得越好! @@ -230,7 +230,7 @@ [未知] - 播放此流媒体失败 + 无法播放此流媒体 发生无法解决的播放器错误 没有结果 空空如也 @@ -268,8 +268,8 @@ 移除所有网页的缓存数据 已清除元数据缓存 自动播放队列中下一个视频 - 如果播放到非循环列表中的最后一个视频,则自动加入一个相关视频到列表中。 - 显示长按以扩展的提示 + 当播放完非循环列表中的最后一个视频时,自动加入一个相关视频到播放列表。 + 显示\"长按添加\"提示 当视频详情页中的背景或悬浮按钮被按下的时候显示提示 默认内容国家 服务 @@ -308,10 +308,10 @@ 重命名 捐赠 - NewPipe 是一群志愿者花费业余时间开发的,目的是给你们带来最佳体验。你的一点心意,可以让开发者们可以享受一杯 Java 的咖啡的同时让 NewPipe 变得更好用! + NewPipe 是一群志愿者花费业余时间开发的,目的是为您带来最佳体验。你的一点心意,可以让开发者们在享受一杯 Java 的咖啡的同时让 NewPipe 变得更好用。 捐赠 网页 - 为了获得 NewPipe 的更多信息和最新消息请访问我们的网站。 + 访问 NewPipe 网站以获取更多的信息和新闻。 你想从搜索历史中删除此项吗? 你想从观看历史中删除此项吗? 确实要删除历史记录的所有项目吗? @@ -335,7 +335,7 @@ Kiosk 流行的 - Top 50 + 前50 最新 & 最热 后台播放器 悬浮窗播放器 @@ -351,10 +351,10 @@ 打开侧栏菜单 关闭侧栏菜单 - 马上就好 ;D + 精彩内容即将呈现 ;D - 偏好的打开动作 + 偏好\"打开\"动作 打开内容的默认动作 — %s 视频播放器 @@ -363,7 +363,7 @@ 总是询问 获取信息中… - 正在加载请求的内容 + 正在加载请求内容 创建新播放列表 删除播放列表 @@ -379,7 +379,7 @@ 播放列表已创建 加入播放列表 播放列表缩略图已更改 - 删除播放列表失败 + 无法删除播放列表 无字幕 @@ -394,10 +394,10 @@ 大字体 启用 LeakCanary - heap dumping 的时候,内存泄露监测可能会导致应用未响应 + 内存泄露监测可能会在heap dumping时导致应用失去响应 报告生命周期外的错误 - 处理之后,强制报告无法送达的、发生在Fragment或activity生命周期之外的Rx异常 + 处理完无法送达的、发生在Fragment或activity生命周期之外的Rx异常后强制报告 导入/导出 导入 @@ -410,22 +410,22 @@ 导入文件 之前的导出 - 订阅导入失败 - 订阅导出失败 + 无法导入订阅 + 无法导出订阅 - 为了导入你的 YouTube 订阅,你需要一个导出文件,按照下面的步骤来下载此文件: -\n1. 浏览器打开链接:%1$s -\n2. 登录你的账户 -\n3. 下载应该马上开始(这个就是你的导出文件) - 为了导入你的 SoundCloud,你需要知道你的个人页链接或者id,如果你知道,只要在下面的输入框中输入任意一种就OK了。 -\n如果你不知道,你需要这么做: -\n1. 浏览器打开链接:%1$s -\n2. 在某些手机浏览器中你得开启桌面模式(desktop mode) -\n3. 复制你跳转到的链接(这个就是你的个人页链接) + 通过下载导出文件导入 YouTube 订阅: +\n1. 在浏览器打开URL:%1$s +\n2. 登录账户 +\n3. 下载应该会马上开始(这个就是导出文件) + 通过输入URL或您的ID导入SoundCloud配置: +\n1. 在浏览器中开启\"桌面模式\"(该网站不适用于移动设备) +\n2. 打开URL:%1$s +\n3. 登录账号 +\n3. 复制重定向后的URL。 你的ID 或 soundcloud.com/你的ID - 请注意这个操作可能耗费大量流量。 -\n你想继续吗? + 请注意该操作可能消耗大量网络流量。 +\n您希望继续吗? 播放速度控制 速度 @@ -433,9 +433,35 @@ Unhook(可能导致失真) Nightcore 默认 -找不到能播放此文件的播放器 +未安装能播放此文件的应用 字幕 - 修改播放器的字幕文本大小和背景样式。需要重启播放器以生效。 + 修改播放器的字幕文本大小和背景样式。需要重启应用程序以生效。 + 清除观看记录 + 删除视频观看记录。 + 删除全部观看记录。 + 观看记录已删除。 + 清除搜索记录 + 删除搜索关键词记录。 + 删除全部搜索记录。 + 搜索记录已删除。 + 已删除1项。 + + NewPipe的隐私策略 + NewPipe 项目非常重视您的隐私。因此, 未经您的同意,应用程序不会收集任何您的数据。 NewPipe 的隐私策略详细解释了您在发送崩溃报告时会发送和存储的哪些数据。 + 阅读隐私策略 + NewPipe 是 copyleft 的自由软件: 你可以按照自己的意愿使用、学习、分享和改进它。具体地说, 您可以根据自由软件基金会发布的 GNU 通用公共许可证的条款(第3版或者任何更高版本), 重新发布和/或修改本软件。 + 您是否希望同时导入设置? + + 为了遵守欧洲通用数据保护条例(GDPR),我们提醒您注意NewPipe的隐私政策。 请仔细阅读。 +\n您必须接受它才能向我们发送错误报告。 + 接受 + 拒绝 + + 不限制 + 使用移动数据时限制分辨率 + 更多频道 + 更多频道 + 用户们 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 1d0ce7ac0..2e6610381 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -271,7 +271,7 @@ 匯入資料庫 匯出資料庫 將覆蓋您目前的歷史記錄和訂閱 - 匯出歷史記錄、訂閱和播放清單。 + 匯出歷史記錄、訂閱和播放清單 返回 欲了解更多關於 NewPipe 的資訊和新聞,請造訪我們的網站。 首頁內容 @@ -382,7 +382,7 @@ 使用粗略但快速的尋找 粗略尋找讓播放器更快找到影片的進度位置 自動播放隊列中下一部影片 - 在非重複播放佇列中的最後一個串流上開始播放時,自動附上相關串流。 + 在非重複播放佇列中的最後一個串流上開始播放時,自動附上相關串流 同步 檔案 @@ -424,7 +424,7 @@ \n3. 詢問時登入到您的帳號 \n4. 複製您被重新導向到的網址。 載入縮圖 - 停用後,NewPipe將不再載入縮圖,減少數據使用與騰空儲存空間,亦會清除記憶體和磁碟上的縮圖快取。 + 停用後,NewPipe將不再載入縮圖,減少數據使用與騰空儲存空間,亦會清除記憶體和磁碟上的縮圖快取 已清除圖片快取 抹除快取中介資料 移除所有網頁的快取資料 @@ -432,7 +432,7 @@ 播放速度控制 節拍 間距 - 解除 (可能導致失真) + 解除連結(可能導致失真) Nightcore 預設 偏好的「開啟」動作 @@ -441,17 +441,17 @@ 沒有可供下載的串流 字幕 - 調整播放器字幕大小與背景樣式。必須重新啟動應用程式才會生效。 + 調整播放器字幕大小與背景樣式。必須重新啟動應用程式才會生效 未安裝可播放此檔案的應用程式 清除觀看歷史 - 刪除播放過的串流歷史。 - 刪除全部的觀看歷史。 + 刪除播放過的串流歷史 + 刪除全部的觀看歷史 觀看歷史已刪除。 清除觀看歷史 - 刪除搜尋關鍵字的歷史。 - 刪除全部的搜尋歷史。 + 刪除搜尋關鍵字的歷史 + 刪除全部的搜尋歷史 搜尋歷史已刪除。 已刪除 1 個項目。 @@ -468,4 +468,18 @@ 下降 沒有限制 當您使用行動網路時限制解析度 + 在應用程式切換時最小化 + 當從主影片播放器切換到其他應用程式時要進行的動作 — %s + + 最小化為背景播放器 + 最小化為彈出式播放器 + +在靜音時快轉 + 步進 + 重設 + + 頻道 + 播放清單 + + 使用者 diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index fc7cfd1b4..3e73738ed 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -25,6 +25,22 @@ screen_brightness_key screen_brightness_timestamp_key + minimize_on_exit_key + @string/minimize_on_exit_none_key + minimize_on_exit_none_key + minimize_on_exit_background_key + minimize_on_exit_popup_key + + @string/minimize_on_exit_none_key + @string/minimize_on_exit_background_key + @string/minimize_on_exit_popup_key + + + @string/minimize_on_exit_none_description + @string/minimize_on_exit_background_description + @string/minimize_on_exit_popup_description + + default_resolution 360p show_higher_resolutions @@ -178,6 +194,7 @@ video_player background_player popup_player + download always_ask_player @@ -185,6 +202,7 @@ @string/video_player @string/background_player @string/popup_player + @string/download @string/always_ask_open_action @@ -192,6 +210,7 @@ @string/video_player_key @string/background_player_key @string/popup_player_key + @string/download_key @string/always_ask_open_action_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5ee80536f..502483667 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -125,7 +125,12 @@ Error report All Channel + Channels Playlist + Playlists + Videos + Tracks + Users Yes Later Disabled @@ -479,9 +484,10 @@ Playback Speed Controls Tempo Pitch - Unhook (may cause distortion) - Nightcore - Default + Unlink (may cause distortion) + Fast-forward during silence + Step + Reset In order to comply with the European General Data Protection Regulation (GDPR), we herby draw your attention to NewPipe\'s privacy policy. Please read it carefully.\nYou must accept it to send us the bug report. @@ -504,4 +510,11 @@ 144p + + Minimize on application switch + Action when switching to other application from main video player — %s + None + Minimize to background player + Minimize to popup player + diff --git a/app/src/main/res/xml/video_audio_settings.xml b/app/src/main/res/xml/video_audio_settings.xml index 6ec0da215..a547ffaf2 100644 --- a/app/src/main/res/xml/video_audio_settings.xml +++ b/app/src/main/res/xml/video_audio_settings.xml @@ -90,6 +90,14 @@ android:summary="@string/preferred_open_action_settings_summary" android:title="@string/preferred_open_action_settings_title"/> + +